From 24844a297fd2df8043bfba6f743bf1e9dcf7f0dd Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:38:56 -0700 Subject: [PATCH 1/7] WIP: amount to ui amount helpers and tests --- clients/js/README.md | 5 +- clients/js/package.json | 4 +- clients/js/pnpm-lock.yaml | 3 + clients/js/src/getAmountToUiAmount.ts | 250 +++++++++++++++ clients/js/src/index.ts | 1 + .../getAmountToUiAmount.test.ts | 297 ++++++++++++++++++ 6 files changed, 557 insertions(+), 3 deletions(-) create mode 100644 clients/js/src/getAmountToUiAmount.ts create mode 100644 clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts diff --git a/clients/js/README.md b/clients/js/README.md index 2458cf643..54130cad5 100644 --- a/clients/js/README.md +++ b/clients/js/README.md @@ -17,8 +17,9 @@ This will start a new local validator, if one is not already running, and run th Alternatively, you can go into the client directory and run the tests directly. ```sh -# Build your programs and start the validator. -pnpm programs:build +# Build your programs +pnpm build +# Start the validator (from the root of the repository) pnpm validator:restart # Go into the client directory and run the tests. diff --git a/clients/js/package.json b/clients/js/package.json index 87395df65..0a55faf6d 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -48,6 +48,7 @@ "@ava/typescript": "^4.1.0", "@solana-program/system": "^0.6.1", "@solana/eslint-config-solana": "^3.0.3", + "@solana/sysvars": "^2.0.0", "@solana/web3.js": "^2.0.0", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^7.16.1", @@ -69,7 +70,8 @@ "rewritePaths": { "test/": "dist/test/" } - } + }, + "verbose": true }, "packageManager": "pnpm@9.1.0" } diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 3437e9a1d..5fc39615f 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@solana/eslint-config-solana': specifier: ^3.0.3 version: 3.0.3(@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-simple-import-sort@10.0.0(eslint@8.57.0))(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3) + '@solana/sysvars': + specifier: ^2.0.0 + version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@solana/web3.js': specifier: ^2.0.0 version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)(ws@8.17.0) diff --git a/clients/js/src/getAmountToUiAmount.ts b/clients/js/src/getAmountToUiAmount.ts new file mode 100644 index 000000000..d9f1cfaf0 --- /dev/null +++ b/clients/js/src/getAmountToUiAmount.ts @@ -0,0 +1,250 @@ +import { + type GetAccountInfoApi, + type Rpc, + Address, + UnixTimestamp, + unwrapOption, +} from '@solana/web3.js'; +import { fetchSysvarClock } from '@solana/sysvars'; +import { fetchMint } from './generated'; + +/** + * Calculates the exponent for the interest rate formula. + * @param t1 - The start time in seconds. + * @param t2 - The end time in seconds. + * @param r - The interest rate in basis points. + * @returns The calculated exponent. + */ +function calculateExponentForTimesAndRate(t1: number, t2: number, r: number) { + const ONE_IN_BASIS_POINTS = 10000; + const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24; + const timespan = t2 - t1; + const numerator = r * timespan; + const exponent = numerator / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS); + return Math.exp(exponent); +} + +/** + * Retrieves the current timestamp from the Solana clock sysvar. + * @param rpc - The Solana rpc object. + * @returns A promise that resolves to the current timestamp in seconds. + * @throws An error if the sysvar clock cannot be fetched or parsed. + */ +async function getSysvarClockTimestamp( + rpc: Rpc +): Promise { + const info = await fetchSysvarClock(rpc); + if (!info) { + throw new Error('Failed to fetch sysvar clock'); + } + return info.unixTimestamp; +} + +/** + * Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction + * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs + * In general to calculate compounding interest over a period of time, the formula is: + * A = P * e^(r * t) where + * A = final amount after interest + * P = principal amount (initial investment) + * r = annual interest rate (as a decimal, e.g., 5% = 0.05) + * t = time in years + * e = mathematical constant (~2.718) + * + * In this case, we are calculating the total scale factor for the interest bearing extension which is the product of two exponential functions: + * totalScale = e^(r1 * t1) * e^(r2 * t2) + * where r1 and r2 are the interest rates before and after the last update, and t1 and t2 are the times in years between + * the initialization timestamp and the last update timestamp, and between the last update timestamp and the current timestamp. + * + * @param amount Amount of tokens to be converted + * @param decimals Number of decimals of the mint + * @param currentTimestamp Current timestamp in seconds + * @param lastUpdateTimestamp Last time the interest rate was updated in seconds + * @param initializationTimestamp Time the interest bearing extension was initialized in seconds + * @param preUpdateAverageRate Interest rate in basis points (1 basis point = 0.01%) before last update + * @param currentRate Current interest rate in basis points + * + * @return Amount scaled by accrued interest as a string with appropriate decimal places + */ +export function amountToUiAmountWithoutSimulation( + amount: bigint, + decimals: number, + currentTimestamp: number, // in seconds + lastUpdateTimestamp: number, + initializationTimestamp: number, + preUpdateAverageRate: number, + currentRate: number +): string { + // Calculate pre-update exponent + // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) + const preUpdateExp = calculateExponentForTimesAndRate( + initializationTimestamp, + lastUpdateTimestamp, + preUpdateAverageRate + ); + + // Calculate post-update exponent + // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) + const postUpdateExp = calculateExponentForTimesAndRate( + lastUpdateTimestamp, + currentTimestamp, + currentRate + ); + + // Calculate total scale + const totalScale = preUpdateExp * postUpdateExp; + // Scale the amount by the total interest factor + const scaledAmount = Number(amount) * totalScale; + + // Calculate the decimal factor (e.g. 100 for 2 decimals) + const decimalFactor = Math.pow(10, decimals); + + // Convert to UI amount by: + // 1. Truncating to remove any remaining decimals + // 2. Dividing by decimal factor to get final UI amount + // 3. Converting to string + return (Math.trunc(scaledAmount) / decimalFactor).toString(); +} + +/** + * Convert amount to UiAmount for a mint without simulating a transaction + * This implements the same logic as `process_amount_to_ui_amount` in /token/program-2022/src/processor.rs + * and `process_amount_to_ui_amount` in /token/program/src/processor.rs + * + * @param rpc rpc to use + * @param mint Mint to use for calculations + * @param amount Amount of tokens to be converted to Ui Amount + * + * @return Ui Amount generated + */ +export async function amountToUiAmountForMintWithoutSimulation( + rpc: Rpc, + mint: Address, + amount: bigint +): Promise { + console.log('CONSOLE amountToUiAmountForMintWithoutSimulation', rpc.getAccountInfo(mint)); + const accountInfo = await fetchMint(rpc, mint); + const extensions = unwrapOption(accountInfo.data.extensions); + const interestBearingMintConfigState = extensions?.find( + (ext) => ext.__kind === 'InterestBearingConfig' + ); + if (!interestBearingMintConfigState) { + const amountNumber = Number(amount); + const decimalsFactor = Math.pow(10, accountInfo.data.decimals); + return (amountNumber / decimalsFactor).toString(); + } + + const timestamp = await getSysvarClockTimestamp(rpc); + + return amountToUiAmountWithoutSimulation( + amount, + accountInfo.data.decimals, + Number(timestamp), + Number(interestBearingMintConfigState.lastUpdateTimestamp), + Number(interestBearingMintConfigState.initializationTimestamp), + interestBearingMintConfigState.preUpdateAverageRate, + interestBearingMintConfigState.currentRate + ); +} + +/** + * Convert an amount with interest back to the original amount without interest + * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs + * + * @param uiAmount UI Amount (principal plus continuously compounding interest) to be converted back to original principal + * @param decimals Number of decimals for the mint + * @param currentTimestamp Current timestamp in seconds + * @param lastUpdateTimestamp Last time the interest rate was updated in seconds + * @param initializationTimestamp Time the interest bearing extension was initialized in seconds + * @param preUpdateAverageRate Interest rate in basis points (hundredths of a percent) before the last update + * @param currentRate Current interest rate in basis points + * + * In general to calculate the principal from the UI amount, the formula is: + * P = A / (e^(r * t)) where + * P = principal + * A = UI amount + * r = annual interest rate (as a decimal, e.g., 5% = 0.05) + * t = time in years + * + * 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: + * totalScale = e^(r1 * t1) * e^(r2 * t2) + * 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, + * and t2 is the time in years between the last update timestamp and the current timestamp. + * then to calculate the principal, we divide the UI amount by the total scale factor: + * P = A / totalScale + * + * @return Original amount (principal) without interest + */ +export function uiAmountToAmountWithoutSimulation( + uiAmount: string, + decimals: number, + currentTimestamp: number, // in seconds + lastUpdateTimestamp: number, + initializationTimestamp: number, + preUpdateAverageRate: number, + currentRate: number +): bigint { + const uiAmountNumber = parseFloat(uiAmount); + const decimalsFactor = Math.pow(10, decimals); + const uiAmountScaled = uiAmountNumber * decimalsFactor; + + // Calculate pre-update exponent + const preUpdateExp = calculateExponentForTimesAndRate( + initializationTimestamp, + lastUpdateTimestamp, + preUpdateAverageRate + ); + + // Calculate post-update exponent + const postUpdateExp = calculateExponentForTimesAndRate( + lastUpdateTimestamp, + currentTimestamp, + currentRate + ); + + // Calculate total scale + const totalScale = preUpdateExp * postUpdateExp; + + // Calculate original principal by dividing the UI amount (principal + interest) by the total scale + const originalPrincipal = uiAmountScaled / totalScale; + return BigInt(Math.trunc(originalPrincipal)); +} + +/** + * Convert a UI amount back to the raw amount + * + * @param rpc rpc to use + * @param mint Mint to use for calculations + * @param uiAmount UI Amount to be converted back to raw amount + * + * + * @return Raw amount + */ +export async function uiAmountToAmountForMintWithoutSimulation( + rpc: Rpc, + mint: Address, + uiAmount: string +): Promise { + const accountInfo = await fetchMint(rpc, mint); + const extensions = unwrapOption(accountInfo.data.extensions); + const interestBearingMintConfigState = extensions?.find( + (ext) => ext.__kind === 'InterestBearingConfig' + ); + if (!interestBearingMintConfigState) { + const uiAmountScaled = + parseFloat(uiAmount) * Math.pow(10, accountInfo.data.decimals); + return BigInt(Math.trunc(uiAmountScaled)); + } + + const timestamp = await getSysvarClockTimestamp(rpc); + + return uiAmountToAmountWithoutSimulation( + uiAmount, + accountInfo.data.decimals, + Number(timestamp), + Number(interestBearingMintConfigState.lastUpdateTimestamp), + Number(interestBearingMintConfigState.initializationTimestamp), + interestBearingMintConfigState.preUpdateAverageRate, + interestBearingMintConfigState.currentRate + ); +} diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 026d5f32d..4b4c5ad6c 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,5 +1,6 @@ export * from './generated'; +export * from './getAmountToUiAmount'; export * from './getInitializeInstructionsForExtensions'; export * from './getTokenSize'; export * from './getMintSize'; diff --git a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts new file mode 100644 index 000000000..7fa70eab2 --- /dev/null +++ b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts @@ -0,0 +1,297 @@ +import test from 'ava'; +import type { + GetAccountInfoApi, + Lamports, + Rpc, + Base64EncodedBytes, + AccountInfoWithJsonData, + Commitment, +} from '@solana/web3.js'; +import { address, Address } from '@solana/web3.js'; +import { + amountToUiAmountForMintWithoutSimulation, + uiAmountToAmountForMintWithoutSimulation, + TOKEN_2022_PROGRAM_ADDRESS, + getMintEncoder, +} from '../../../src'; + +const ONE_YEAR_IN_SECONDS = 31556736; + +type AccountInfo = Readonly<{ + executable: boolean; + lamports: Lamports; + owner: Address; + rentEpoch: bigint; + data: Buffer | AccountInfoWithJsonData; +}>; + +function getMockRpc( + accounts: Record +): Rpc { + const getAccountInfo = ( + address: Address, + _config?: { commitment?: Commitment } + ) => ({ + send: async () => ({ + context: { slot: 0n }, + value: accounts[address] + ? { + executable: accounts[address].executable, + lamports: accounts[address].lamports, + owner: accounts[address].owner, + rentEpoch: accounts[address].rentEpoch, + data: + accounts[address].data instanceof Uint8Array + ? ([ + Buffer.from(accounts[address].data).toString('base64'), + 'base64', + ] as [Base64EncodedBytes, 'base64']) + : accounts[address].data, + } + : null, + }), + }); + return { getAccountInfo } as unknown as Rpc; +} + +function createMockMintAccountInfo( + decimals = 2, + hasInterestBearingConfig = false, + config: { preUpdateAverageRate?: number; currentRate?: number } = {} +) { + const mintEncoder = getMintEncoder(); + const bufferData = Buffer.from( + mintEncoder.encode({ + mintAuthority: address('11111111111111111111111111111111'), + supply: BigInt(1000000), + decimals: decimals, + isInitialized: true, + freezeAuthority: address('11111111111111111111111111111111'), + extensions: hasInterestBearingConfig + ? [ + { + __kind: 'InterestBearingConfig', + rateAuthority: address('11111111111111111111111111111111'), + initializationTimestamp: BigInt(0), + preUpdateAverageRate: config.preUpdateAverageRate || 500, + lastUpdateTimestamp: BigInt(ONE_YEAR_IN_SECONDS), + currentRate: config.currentRate || 500, + }, + ] + : [], + }) + ); + return { + owner: TOKEN_2022_PROGRAM_ADDRESS, + lamports: 1000000n as Lamports, + executable: false, + rentEpoch: 0n, + data: bufferData, + }; +} + +const createMockClockAccountInfo = (unixTimestamp: number) => ({ + owner: TOKEN_2022_PROGRAM_ADDRESS, + lamports: 1000000n as Lamports, + executable: false, + rentEpoch: 0n, + data: { + data: { + parsed: { + info: { + epoch: 0, + epochStartTimestamp: 0, + leaderScheduleEpoch: 0, + slot: 0, + unixTimestamp, + }, + type: 'clock', + }, + program: 'sysvar', + space: 40n, + }, + }, +}); + +const mint = address('So11111111111111111111111111111111111111112'); +const clock = address('SysvarC1ock11111111111111111111111111111111'); + +test('should return the correct UiAmount when interest bearing config is not present', async (t) => { + + const testCases = [ + { decimals: 0, amount: BigInt(100), expected: '100' }, + { decimals: 2, amount: BigInt(100), expected: '1' }, + { decimals: 9, amount: BigInt(1000000000), expected: '1' }, + { decimals: 10, amount: BigInt(1), expected: '1e-10' }, + { decimals: 10, amount: BigInt(1000000000), expected: '0.1' }, + ]; + + for (const { decimals, amount, expected } of testCases) { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(decimals, false), + }); + const result = await amountToUiAmountForMintWithoutSimulation( + connection, + mint, + amount + ); + t.is(result, expected); + } +}); + +test('should return the correct UiAmount for constant 5% rate', async (t) => { + const testCases = [ + { decimals: 0, amount: BigInt(1), expected: '1' }, + { decimals: 1, amount: BigInt(1), expected: '0.1' }, + { decimals: 10, amount: BigInt(1), expected: '1e-10' }, + { decimals: 10, amount: BigInt(10000000000), expected: '1.0512710963' }, + ]; + + for (const { decimals, amount, expected } of testCases) { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(decimals, true), + }); + + const result = await amountToUiAmountForMintWithoutSimulation( + connection, + mint, + amount + ); + t.is(result, expected); + } +}); + +test('should return the correct UiAmount for constant -5% rate', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(10, true, { + preUpdateAverageRate: -500, + currentRate: -500, + }), + }); + + const result = await amountToUiAmountForMintWithoutSimulation( + connection, + mint, + BigInt(10000000000) + ); + t.is(result, '0.9512294245'); +}); + +test('should return the correct UiAmount for netting out rates', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), + [mint]: createMockMintAccountInfo(10, true, { + preUpdateAverageRate: -500, + currentRate: 500, + }), + }); + + const result = await amountToUiAmountForMintWithoutSimulation( + connection, + mint, + BigInt(10000000000) + ); + t.is(result, '1'); +}); + +test('should handle huge values correctly', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), + [mint]: createMockMintAccountInfo(6, true), + }); + + const result = await amountToUiAmountForMintWithoutSimulation( + connection, + mint, + BigInt('18446744073709551615') + ); + t.is(result, '20386805083448.098'); +}); + +test('should return the correct amount for constant 5% rate', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(0, true), + }); + + const result = await uiAmountToAmountForMintWithoutSimulation( + connection, + mint, + '1.0512710963760241' + ); + t.is(result, 1n); +}); + +test('should handle decimal places correctly', async (t) => { + const testCases = [ + { decimals: 1, uiAmount: '0.10512710963760241', expected: 1n }, + { decimals: 10, uiAmount: '0.00000000010512710963760242', expected: 1n }, + { decimals: 10, uiAmount: '1.0512710963760241', expected: 10000000000n }, + ]; + + for (const { decimals, uiAmount, expected } of testCases) { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(decimals, true), + }); + + const result = await uiAmountToAmountForMintWithoutSimulation( + connection, + mint, + uiAmount + ); + t.is(result, expected); + } +}); + +test('should return the correct amount for constant -5% rate', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), + [mint]: createMockMintAccountInfo(10, true, { + preUpdateAverageRate: -500, + currentRate: -500, + }), + }); + + const result = await uiAmountToAmountForMintWithoutSimulation( + connection, + mint, + '0.951229424500714' + ); + t.is(result, 9999999999n); // calculation truncates to avoid floating point precision issues in transfers +}); + +test('should return the correct amount for netting out rates', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), + [mint]: createMockMintAccountInfo(10, true, { + preUpdateAverageRate: -500, + currentRate: 500, + }), + }); + + const result = await uiAmountToAmountForMintWithoutSimulation( + connection, + mint, + '1' + ); + t.is(result, 10000000000n); +}); + +test('should handle huge values correctly for amount to ui amount', async (t) => { + const connection = getMockRpc({ + [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), + [mint]: createMockMintAccountInfo(0, true), + }); + + + const result = await uiAmountToAmountForMintWithoutSimulation( + connection, + mint, + '20386805083448100000' + ); + t.is(result, 18446744073709551616n); +}); From 0e16f245a9ed4c2616e44187db6b89524aabfe71 Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:29:16 -0700 Subject: [PATCH 2/7] clean up helper functions --- clients/js/src/getAmountToUiAmount.ts | 115 ++++++++----- .../getAmountToUiAmount.test.ts | 159 +++++++++--------- 2 files changed, 153 insertions(+), 121 deletions(-) diff --git a/clients/js/src/getAmountToUiAmount.ts b/clients/js/src/getAmountToUiAmount.ts index d9f1cfaf0..980cb554a 100644 --- a/clients/js/src/getAmountToUiAmount.ts +++ b/clients/js/src/getAmountToUiAmount.ts @@ -12,18 +12,68 @@ import { fetchMint } from './generated'; * Calculates the exponent for the interest rate formula. * @param t1 - The start time in seconds. * @param t2 - The end time in seconds. - * @param r - The interest rate in basis points. + * @param r - The interest rate in basis points. + * * @returns The calculated exponent. */ function calculateExponentForTimesAndRate(t1: number, t2: number, r: number) { const ONE_IN_BASIS_POINTS = 10000; const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24; const timespan = t2 - t1; + if (timespan < 0) { + throw new Error('Invalid timespan: end time before start time'); + } + const numerator = r * timespan; const exponent = numerator / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS); return Math.exp(exponent); } +/** + * Calculates the total scale factor for an interest bearing token by combining two exponential functions: + * One for the period between initialization and last update using the pre-update average rate, + * and another for the period between last update and current time using the current rate. + * + * @param currentTimestamp Current timestamp in seconds + * @param lastUpdateTimestamp Last time the interest rate was updated in seconds + * @param initializationTimestamp Time the interest bearing extension was initialized in seconds + * @param preUpdateAverageRate Interest rate in basis points before last update + * @param currentRate Current interest rate in basis points + * + * @returns The total scale factor as a product of the two exponential functions + */ +function calculateTotalScale({ + currentTimestamp, + lastUpdateTimestamp, + initializationTimestamp, + preUpdateAverageRate, + currentRate, +}: { + currentTimestamp: number; + lastUpdateTimestamp: number; + initializationTimestamp: number; + preUpdateAverageRate: number; + currentRate: number; +}): number { + // Calculate pre-update exponent + // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) + const preUpdateExp = calculateExponentForTimesAndRate( + initializationTimestamp, + lastUpdateTimestamp, + preUpdateAverageRate + ); + + // Calculate post-update exponent + // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) + const postUpdateExp = calculateExponentForTimesAndRate( + lastUpdateTimestamp, + currentTimestamp, + currentRate + ); + + return preUpdateExp * postUpdateExp; +} + /** * Retrieves the current timestamp from the Solana clock sysvar. * @param rpc - The Solana rpc object. @@ -75,24 +125,13 @@ export function amountToUiAmountWithoutSimulation( preUpdateAverageRate: number, currentRate: number ): string { - // Calculate pre-update exponent - // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) - const preUpdateExp = calculateExponentForTimesAndRate( - initializationTimestamp, - lastUpdateTimestamp, - preUpdateAverageRate - ); - - // Calculate post-update exponent - // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) - const postUpdateExp = calculateExponentForTimesAndRate( - lastUpdateTimestamp, + const totalScale = calculateTotalScale({ currentTimestamp, - currentRate - ); - - // Calculate total scale - const totalScale = preUpdateExp * postUpdateExp; + lastUpdateTimestamp, + initializationTimestamp, + preUpdateAverageRate, + currentRate, + }); // Scale the amount by the total interest factor const scaledAmount = Number(amount) * totalScale; @@ -108,12 +147,13 @@ export function amountToUiAmountWithoutSimulation( /** * Convert amount to UiAmount for a mint without simulating a transaction - * This implements the same logic as `process_amount_to_ui_amount` in /token/program-2022/src/processor.rs - * and `process_amount_to_ui_amount` in /token/program/src/processor.rs + * This implements the same logic as `process_amount_to_ui_amount` in + * solana-labs/solana-program-library/token/program-2022/src/processor.rs + * and `process_amount_to_ui_amount` in solana-labs/solana-program-library/token/program/src/processor.rs * - * @param rpc rpc to use - * @param mint Mint to use for calculations - * @param amount Amount of tokens to be converted to Ui Amount + * @param rpc Rpc to use + * @param mint Mint to use for calculations + * @param amount Amount of tokens to be converted to Ui Amount * * @return Ui Amount generated */ @@ -122,7 +162,6 @@ export async function amountToUiAmountForMintWithoutSimulation( mint: Address, amount: bigint ): Promise { - console.log('CONSOLE amountToUiAmountForMintWithoutSimulation', rpc.getAccountInfo(mint)); const accountInfo = await fetchMint(rpc, mint); const extensions = unwrapOption(accountInfo.data.extensions); const interestBearingMintConfigState = extensions?.find( @@ -188,22 +227,13 @@ export function uiAmountToAmountWithoutSimulation( const decimalsFactor = Math.pow(10, decimals); const uiAmountScaled = uiAmountNumber * decimalsFactor; - // Calculate pre-update exponent - const preUpdateExp = calculateExponentForTimesAndRate( - initializationTimestamp, - lastUpdateTimestamp, - preUpdateAverageRate - ); - - // Calculate post-update exponent - const postUpdateExp = calculateExponentForTimesAndRate( - lastUpdateTimestamp, + const totalScale = calculateTotalScale({ currentTimestamp, - currentRate - ); - - // Calculate total scale - const totalScale = preUpdateExp * postUpdateExp; + lastUpdateTimestamp, + initializationTimestamp, + preUpdateAverageRate, + currentRate, + }); // Calculate original principal by dividing the UI amount (principal + interest) by the total scale const originalPrincipal = uiAmountScaled / totalScale; @@ -213,10 +243,9 @@ export function uiAmountToAmountWithoutSimulation( /** * Convert a UI amount back to the raw amount * - * @param rpc rpc to use - * @param mint Mint to use for calculations - * @param uiAmount UI Amount to be converted back to raw amount - * + * @param rpc Rpc to use + * @param mint Mint to use for calculations + * @param uiAmount UI Amount to be converted back to raw amount * * @return Raw amount */ diff --git a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts index 7fa70eab2..1e109b448 100644 --- a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts +++ b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts @@ -4,10 +4,11 @@ import type { Lamports, Rpc, Base64EncodedBytes, - AccountInfoWithJsonData, Commitment, + UnixTimestamp, } from '@solana/web3.js'; import { address, Address } from '@solana/web3.js'; +import { getSysvarClockEncoder, SYSVAR_CLOCK_ADDRESS } from '@solana/sysvars'; import { amountToUiAmountForMintWithoutSimulation, uiAmountToAmountForMintWithoutSimulation, @@ -22,7 +23,7 @@ type AccountInfo = Readonly<{ lamports: Lamports; owner: Address; rentEpoch: bigint; - data: Buffer | AccountInfoWithJsonData; + data: Buffer; }>; function getMockRpc( @@ -31,27 +32,45 @@ function getMockRpc( const getAccountInfo = ( address: Address, _config?: { commitment?: Commitment } - ) => ({ - send: async () => ({ - context: { slot: 0n }, - value: accounts[address] - ? { - executable: accounts[address].executable, - lamports: accounts[address].lamports, - owner: accounts[address].owner, - rentEpoch: accounts[address].rentEpoch, - data: - accounts[address].data instanceof Uint8Array - ? ([ - Buffer.from(accounts[address].data).toString('base64'), - 'base64', - ] as [Base64EncodedBytes, 'base64']) - : accounts[address].data, - } - : null, - }), - }); - return { getAccountInfo } as unknown as Rpc; + ) => { + const account = accounts[address]; + if (!account) { + throw new Error(`Account not found for address: ${address}`); + } + if (!(account.data instanceof Uint8Array)) { + throw new Error( + `Account data is not a Uint8Array for address: ${address}` + ); + } + return { + send: async () => ({ + context: { slot: 0n }, + value: account + ? { + executable: account.executable, + lamports: account.lamports, + owner: account.owner, + rentEpoch: account.rentEpoch, + data: [ + Buffer.from(account.data).toString('base64'), + 'base64', + ] as [Base64EncodedBytes, 'base64'], + } + : null, + }), + }; + }; + return { getAccountInfo } as unknown as Rpc +} + +function populateMockAccount(data: Buffer) { + return { + executable: false, + lamports: 1000000n as Lamports, + owner: TOKEN_2022_PROGRAM_ADDRESS, + rentEpoch: 0n, + data, + }; } function createMockMintAccountInfo( @@ -59,19 +78,20 @@ function createMockMintAccountInfo( hasInterestBearingConfig = false, config: { preUpdateAverageRate?: number; currentRate?: number } = {} ) { + const defaultAddress = address('11111111111111111111111111111111'); const mintEncoder = getMintEncoder(); const bufferData = Buffer.from( mintEncoder.encode({ - mintAuthority: address('11111111111111111111111111111111'), + mintAuthority: defaultAddress, supply: BigInt(1000000), decimals: decimals, isInitialized: true, - freezeAuthority: address('11111111111111111111111111111111'), + freezeAuthority: defaultAddress, extensions: hasInterestBearingConfig ? [ { __kind: 'InterestBearingConfig', - rateAuthority: address('11111111111111111111111111111111'), + rateAuthority: defaultAddress, initializationTimestamp: BigInt(0), preUpdateAverageRate: config.preUpdateAverageRate || 500, lastUpdateTimestamp: BigInt(ONE_YEAR_IN_SECONDS), @@ -81,43 +101,27 @@ function createMockMintAccountInfo( : [], }) ); - return { - owner: TOKEN_2022_PROGRAM_ADDRESS, - lamports: 1000000n as Lamports, - executable: false, - rentEpoch: 0n, - data: bufferData, - }; + return populateMockAccount(bufferData); } -const createMockClockAccountInfo = (unixTimestamp: number) => ({ - owner: TOKEN_2022_PROGRAM_ADDRESS, - lamports: 1000000n as Lamports, - executable: false, - rentEpoch: 0n, - data: { - data: { - parsed: { - info: { - epoch: 0, - epochStartTimestamp: 0, - leaderScheduleEpoch: 0, - slot: 0, - unixTimestamp, - }, - type: 'clock', - }, - program: 'sysvar', - space: 40n, - }, - }, -}); +const createMockClockAccountInfo = (unixTimestamp: number) => { + const clockEncoder = getSysvarClockEncoder(); + const bufferData = Buffer.from( + clockEncoder.encode({ + epoch: 0n, + epochStartTimestamp: BigInt(0) as UnixTimestamp, + leaderScheduleEpoch: 0n, + slot: 0n, + unixTimestamp: BigInt(unixTimestamp) as UnixTimestamp, + }) + ); + return populateMockAccount(bufferData); +}; const mint = address('So11111111111111111111111111111111111111112'); -const clock = address('SysvarC1ock11111111111111111111111111111111'); +const clock = SYSVAR_CLOCK_ADDRESS; test('should return the correct UiAmount when interest bearing config is not present', async (t) => { - const testCases = [ { decimals: 0, amount: BigInt(100), expected: '100' }, { decimals: 2, amount: BigInt(100), expected: '1' }, @@ -127,12 +131,12 @@ test('should return the correct UiAmount when interest bearing config is not pre ]; for (const { decimals, amount, expected } of testCases) { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(decimals, false), }); const result = await amountToUiAmountForMintWithoutSimulation( - connection, + rpc, mint, amount ); @@ -149,13 +153,13 @@ test('should return the correct UiAmount for constant 5% rate', async (t) => { ]; for (const { decimals, amount, expected } of testCases) { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(decimals, true), }); const result = await amountToUiAmountForMintWithoutSimulation( - connection, + rpc, mint, amount ); @@ -164,7 +168,7 @@ test('should return the correct UiAmount for constant 5% rate', async (t) => { }); test('should return the correct UiAmount for constant -5% rate', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(10, true, { preUpdateAverageRate: -500, @@ -173,7 +177,7 @@ test('should return the correct UiAmount for constant -5% rate', async (t) => { }); const result = await amountToUiAmountForMintWithoutSimulation( - connection, + rpc, mint, BigInt(10000000000) ); @@ -181,7 +185,7 @@ test('should return the correct UiAmount for constant -5% rate', async (t) => { }); test('should return the correct UiAmount for netting out rates', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), [mint]: createMockMintAccountInfo(10, true, { preUpdateAverageRate: -500, @@ -190,7 +194,7 @@ test('should return the correct UiAmount for netting out rates', async (t) => { }); const result = await amountToUiAmountForMintWithoutSimulation( - connection, + rpc, mint, BigInt(10000000000) ); @@ -198,13 +202,13 @@ test('should return the correct UiAmount for netting out rates', async (t) => { }); test('should handle huge values correctly', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), [mint]: createMockMintAccountInfo(6, true), }); const result = await amountToUiAmountForMintWithoutSimulation( - connection, + rpc, mint, BigInt('18446744073709551615') ); @@ -212,13 +216,13 @@ test('should handle huge values correctly', async (t) => { }); test('should return the correct amount for constant 5% rate', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(0, true), }); const result = await uiAmountToAmountForMintWithoutSimulation( - connection, + rpc, mint, '1.0512710963760241' ); @@ -233,13 +237,13 @@ test('should handle decimal places correctly', async (t) => { ]; for (const { decimals, uiAmount, expected } of testCases) { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(decimals, true), }); const result = await uiAmountToAmountForMintWithoutSimulation( - connection, + rpc, mint, uiAmount ); @@ -248,7 +252,7 @@ test('should handle decimal places correctly', async (t) => { }); test('should return the correct amount for constant -5% rate', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS), [mint]: createMockMintAccountInfo(10, true, { preUpdateAverageRate: -500, @@ -257,7 +261,7 @@ test('should return the correct amount for constant -5% rate', async (t) => { }); const result = await uiAmountToAmountForMintWithoutSimulation( - connection, + rpc, mint, '0.951229424500714' ); @@ -265,7 +269,7 @@ test('should return the correct amount for constant -5% rate', async (t) => { }); test('should return the correct amount for netting out rates', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), [mint]: createMockMintAccountInfo(10, true, { preUpdateAverageRate: -500, @@ -274,7 +278,7 @@ test('should return the correct amount for netting out rates', async (t) => { }); const result = await uiAmountToAmountForMintWithoutSimulation( - connection, + rpc, mint, '1' ); @@ -282,14 +286,13 @@ test('should return the correct amount for netting out rates', async (t) => { }); test('should handle huge values correctly for amount to ui amount', async (t) => { - const connection = getMockRpc({ + const rpc = getMockRpc({ [clock]: createMockClockAccountInfo(ONE_YEAR_IN_SECONDS * 2), [mint]: createMockMintAccountInfo(0, true), }); - const result = await uiAmountToAmountForMintWithoutSimulation( - connection, + rpc, mint, '20386805083448100000' ); From 56196762fd6c7f92e3ac115922cbdc229f5790de Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:32:06 -0700 Subject: [PATCH 3/7] remove ava verbose mode --- clients/js/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/clients/js/package.json b/clients/js/package.json index 0a55faf6d..bfbc0e364 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -70,8 +70,7 @@ "rewritePaths": { "test/": "dist/test/" } - }, - "verbose": true + } }, "packageManager": "pnpm@9.1.0" } From 56d17ea4d5efe01c3a1e0b12e44be3bcad37414e Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:36:47 -0700 Subject: [PATCH 4/7] prettier fix --- .../interestBearingMint/getAmountToUiAmount.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts index 1e109b448..e17583cb2 100644 --- a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts +++ b/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts @@ -60,7 +60,7 @@ function getMockRpc( }), }; }; - return { getAccountInfo } as unknown as Rpc + return { getAccountInfo } as unknown as Rpc; } function populateMockAccount(data: Buffer) { @@ -277,11 +277,7 @@ test('should return the correct amount for netting out rates', async (t) => { }), }); - const result = await uiAmountToAmountForMintWithoutSimulation( - rpc, - mint, - '1' - ); + const result = await uiAmountToAmountForMintWithoutSimulation(rpc, mint, '1'); t.is(result, 10000000000n); }); From 613bc6a5f76c13265b5c371f6f75b07b75bf3bff Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:19:33 -0700 Subject: [PATCH 5/7] address pr comments --- clients/js/README.md | 5 ++- clients/js/package.json | 4 +-- clients/js/pnpm-lock.yaml | 7 ++-- ...mountToUiAmount.ts => amountToUiAmount.ts} | 8 ++--- clients/js/src/index.ts | 2 +- ...mount.test.ts => amountToUiAmount.test.ts} | 34 +++++++++---------- 6 files changed, 29 insertions(+), 31 deletions(-) rename clients/js/src/{getAmountToUiAmount.ts => amountToUiAmount.ts} (97%) rename clients/js/test/extensions/interestBearingMint/{getAmountToUiAmount.test.ts => amountToUiAmount.test.ts} (93%) diff --git a/clients/js/README.md b/clients/js/README.md index 54130cad5..2458cf643 100644 --- a/clients/js/README.md +++ b/clients/js/README.md @@ -17,9 +17,8 @@ This will start a new local validator, if one is not already running, and run th Alternatively, you can go into the client directory and run the tests directly. ```sh -# Build your programs -pnpm build -# Start the validator (from the root of the repository) +# Build your programs and start the validator. +pnpm programs:build pnpm validator:restart # Go into the client directory and run the tests. diff --git a/clients/js/package.json b/clients/js/package.json index bfbc0e364..e7cd7042a 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -42,13 +42,13 @@ }, "homepage": "https://github.com/solana-program/token-2022#readme", "peerDependencies": { - "@solana/web3.js": "^2.0.0" + "@solana/web3.js": "^2.0.0", + "@solana/sysvars": "^2.0.0" }, "devDependencies": { "@ava/typescript": "^4.1.0", "@solana-program/system": "^0.6.1", "@solana/eslint-config-solana": "^3.0.3", - "@solana/sysvars": "^2.0.0", "@solana/web3.js": "^2.0.0", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^7.16.1", diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 5fc39615f..221d1d690 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@solana/sysvars': + specifier: ^2.0.0 + version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) devDependencies: '@ava/typescript': specifier: ^4.1.0 @@ -17,9 +21,6 @@ importers: '@solana/eslint-config-solana': specifier: ^3.0.3 version: 3.0.3(@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-simple-import-sort@10.0.0(eslint@8.57.0))(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3) - '@solana/sysvars': - specifier: ^2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@solana/web3.js': specifier: ^2.0.0 version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3)(ws@8.17.0) diff --git a/clients/js/src/getAmountToUiAmount.ts b/clients/js/src/amountToUiAmount.ts similarity index 97% rename from clients/js/src/getAmountToUiAmount.ts rename to clients/js/src/amountToUiAmount.ts index 980cb554a..a8664d4b7 100644 --- a/clients/js/src/getAmountToUiAmount.ts +++ b/clients/js/src/amountToUiAmount.ts @@ -116,7 +116,7 @@ async function getSysvarClockTimestamp( * * @return Amount scaled by accrued interest as a string with appropriate decimal places */ -export function amountToUiAmountWithoutSimulation( +export function amountToUiAmountForInterestBearingMintWithoutSimulation( amount: bigint, decimals: number, currentTimestamp: number, // in seconds @@ -175,7 +175,7 @@ export async function amountToUiAmountForMintWithoutSimulation( const timestamp = await getSysvarClockTimestamp(rpc); - return amountToUiAmountWithoutSimulation( + return amountToUiAmountForInterestBearingMintWithoutSimulation( amount, accountInfo.data.decimals, Number(timestamp), @@ -214,7 +214,7 @@ export async function amountToUiAmountForMintWithoutSimulation( * * @return Original amount (principal) without interest */ -export function uiAmountToAmountWithoutSimulation( +export function uiAmountToAmountForInterestBearingMintWithoutSimulation( uiAmount: string, decimals: number, currentTimestamp: number, // in seconds @@ -267,7 +267,7 @@ export async function uiAmountToAmountForMintWithoutSimulation( const timestamp = await getSysvarClockTimestamp(rpc); - return uiAmountToAmountWithoutSimulation( + return uiAmountToAmountForInterestBearingMintWithoutSimulation( uiAmount, accountInfo.data.decimals, Number(timestamp), diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 4b4c5ad6c..58f554768 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,6 +1,6 @@ export * from './generated'; -export * from './getAmountToUiAmount'; +export * from './amountToUiAmount'; export * from './getInitializeInstructionsForExtensions'; export * from './getTokenSize'; export * from './getMintSize'; diff --git a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts similarity index 93% rename from clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts rename to clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts index e17583cb2..456953fc3 100644 --- a/clients/js/test/extensions/interestBearingMint/getAmountToUiAmount.test.ts +++ b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts @@ -6,8 +6,9 @@ import type { Base64EncodedBytes, Commitment, UnixTimestamp, + ReadonlyUint8Array, } from '@solana/web3.js'; -import { address, Address } from '@solana/web3.js'; +import { address, Address, getBase64Decoder, getBase64Encoder } from '@solana/web3.js'; import { getSysvarClockEncoder, SYSVAR_CLOCK_ADDRESS } from '@solana/sysvars'; import { amountToUiAmountForMintWithoutSimulation, @@ -23,7 +24,7 @@ type AccountInfo = Readonly<{ lamports: Lamports; owner: Address; rentEpoch: bigint; - data: Buffer; + data: ReadonlyUint8Array; }>; function getMockRpc( @@ -52,7 +53,7 @@ function getMockRpc( owner: account.owner, rentEpoch: account.rentEpoch, data: [ - Buffer.from(account.data).toString('base64'), + getBase64Decoder().decode(account.data), 'base64', ] as [Base64EncodedBytes, 'base64'], } @@ -63,7 +64,7 @@ function getMockRpc( return { getAccountInfo } as unknown as Rpc; } -function populateMockAccount(data: Buffer) { +function populateMockAccount(data: ReadonlyUint8Array) { return { executable: false, lamports: 1000000n as Lamports, @@ -80,8 +81,8 @@ function createMockMintAccountInfo( ) { const defaultAddress = address('11111111111111111111111111111111'); const mintEncoder = getMintEncoder(); - const bufferData = Buffer.from( - mintEncoder.encode({ + + const data = mintEncoder.encode({ mintAuthority: defaultAddress, supply: BigInt(1000000), decimals: decimals, @@ -100,22 +101,19 @@ function createMockMintAccountInfo( ] : [], }) - ); - return populateMockAccount(bufferData); + return populateMockAccount(data); } const createMockClockAccountInfo = (unixTimestamp: number) => { const clockEncoder = getSysvarClockEncoder(); - const bufferData = Buffer.from( - clockEncoder.encode({ - epoch: 0n, - epochStartTimestamp: BigInt(0) as UnixTimestamp, - leaderScheduleEpoch: 0n, - slot: 0n, - unixTimestamp: BigInt(unixTimestamp) as UnixTimestamp, - }) - ); - return populateMockAccount(bufferData); + const data = clockEncoder.encode({ + epoch: 0n, + epochStartTimestamp: BigInt(0) as UnixTimestamp, + leaderScheduleEpoch: 0n, + slot: 0n, + unixTimestamp: BigInt(unixTimestamp) as UnixTimestamp, + }); + return populateMockAccount(data); }; const mint = address('So11111111111111111111111111111111111111112'); From cb51df9ce1dda6fa838280a60ee5ff141a5202bb Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:44:00 -0800 Subject: [PATCH 6/7] pnpm format fix --- .../amountToUiAmount.test.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts index 456953fc3..8961e06ea 100644 --- a/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts +++ b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts @@ -8,7 +8,11 @@ import type { UnixTimestamp, ReadonlyUint8Array, } from '@solana/web3.js'; -import { address, Address, getBase64Decoder, getBase64Encoder } from '@solana/web3.js'; +import { + address, + Address, + getBase64Decoder, +} from '@solana/web3.js'; import { getSysvarClockEncoder, SYSVAR_CLOCK_ADDRESS } from '@solana/sysvars'; import { amountToUiAmountForMintWithoutSimulation, @@ -52,10 +56,10 @@ function getMockRpc( lamports: account.lamports, owner: account.owner, rentEpoch: account.rentEpoch, - data: [ - getBase64Decoder().decode(account.data), + data: [getBase64Decoder().decode(account.data), 'base64'] as [ + Base64EncodedBytes, 'base64', - ] as [Base64EncodedBytes, 'base64'], + ], } : null, }), @@ -81,26 +85,26 @@ function createMockMintAccountInfo( ) { const defaultAddress = address('11111111111111111111111111111111'); const mintEncoder = getMintEncoder(); - + const data = mintEncoder.encode({ - mintAuthority: defaultAddress, - supply: BigInt(1000000), - decimals: decimals, - isInitialized: true, - freezeAuthority: defaultAddress, - extensions: hasInterestBearingConfig - ? [ - { - __kind: 'InterestBearingConfig', - rateAuthority: defaultAddress, - initializationTimestamp: BigInt(0), - preUpdateAverageRate: config.preUpdateAverageRate || 500, - lastUpdateTimestamp: BigInt(ONE_YEAR_IN_SECONDS), - currentRate: config.currentRate || 500, - }, - ] - : [], - }) + mintAuthority: defaultAddress, + supply: BigInt(1000000), + decimals: decimals, + isInitialized: true, + freezeAuthority: defaultAddress, + extensions: hasInterestBearingConfig + ? [ + { + __kind: 'InterestBearingConfig', + rateAuthority: defaultAddress, + initializationTimestamp: BigInt(0), + preUpdateAverageRate: config.preUpdateAverageRate || 500, + lastUpdateTimestamp: BigInt(ONE_YEAR_IN_SECONDS), + currentRate: config.currentRate || 500, + }, + ] + : [], + }); return populateMockAccount(data); } From 337d4fe5ad37535a2be083195cff472371063dc2 Mon Sep 17 00:00:00 2001 From: Ilan Gitter <8359193+gitteri@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:03:53 -0800 Subject: [PATCH 7/7] re-run formatting --- .../extensions/interestBearingMint/amountToUiAmount.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts index 8961e06ea..b21c88598 100644 --- a/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts +++ b/clients/js/test/extensions/interestBearingMint/amountToUiAmount.test.ts @@ -8,11 +8,7 @@ import type { UnixTimestamp, ReadonlyUint8Array, } from '@solana/web3.js'; -import { - address, - Address, - getBase64Decoder, -} from '@solana/web3.js'; +import { address, Address, getBase64Decoder } from '@solana/web3.js'; import { getSysvarClockEncoder, SYSVAR_CLOCK_ADDRESS } from '@solana/sysvars'; import { amountToUiAmountForMintWithoutSimulation,