From 22a6437b431db1a98b49d033ec58c1db79772490 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 14 Jan 2025 07:55:05 +0900 Subject: [PATCH] [js-legacy] Add scaled ui amount extension (#60) --- .../js-legacy/src/extensions/extensionType.ts | 8 ++ clients/js-legacy/src/extensions/index.ts | 1 + .../src/extensions/scaledUiAmount/actions.ts | 47 +++++++ .../src/extensions/scaledUiAmount/index.ts | 3 + .../extensions/scaledUiAmount/instructions.ts | 115 ++++++++++++++++++ .../src/extensions/scaledUiAmount/state.ts | 29 +++++ .../src/instructions/setAuthority.ts | 1 + clients/js-legacy/src/instructions/types.ts | 2 + .../test/e2e-2022/scaledUiAmount.test.ts | 110 +++++++++++++++++ 9 files changed, 316 insertions(+) create mode 100644 clients/js-legacy/src/extensions/scaledUiAmount/actions.ts create mode 100644 clients/js-legacy/src/extensions/scaledUiAmount/index.ts create mode 100644 clients/js-legacy/src/extensions/scaledUiAmount/instructions.ts create mode 100644 clients/js-legacy/src/extensions/scaledUiAmount/state.ts create mode 100644 clients/js-legacy/test/e2e-2022/scaledUiAmount.test.ts diff --git a/clients/js-legacy/src/extensions/extensionType.ts b/clients/js-legacy/src/extensions/extensionType.ts index f3a33889..b98b365b 100644 --- a/clients/js-legacy/src/extensions/extensionType.ts +++ b/clients/js-legacy/src/extensions/extensionType.ts @@ -17,6 +17,7 @@ import { METADATA_POINTER_SIZE } from './metadataPointer/state.js'; import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js'; import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js'; import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js'; +import { SCALED_UI_AMOUNT_CONFIG_SIZE } from './scaledUiAmount/index.js'; import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js'; import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js'; import { TOKEN_2022_PROGRAM_ID } from '../constants.js'; @@ -47,6 +48,8 @@ export enum ExtensionType { TokenGroup = 21, GroupMemberPointer = 22, TokenGroupMember = 23, + // ConfidentialMintBurn, // Not implemented yet + ScaledUiAmountConfig = 25, } export const TYPE_SIZE = 2; @@ -111,6 +114,8 @@ export function getTypeLen(e: ExtensionType): number { return TOKEN_GROUP_SIZE; case ExtensionType.TokenGroupMember: return TOKEN_GROUP_MEMBER_SIZE; + case ExtensionType.ScaledUiAmountConfig: + return SCALED_UI_AMOUNT_CONFIG_SIZE; case ExtensionType.TokenMetadata: throw Error(`Cannot get type length for variable extension type: ${e}`); default: @@ -134,6 +139,7 @@ export function isMintExtension(e: ExtensionType): boolean { case ExtensionType.GroupMemberPointer: case ExtensionType.TokenGroup: case ExtensionType.TokenGroupMember: + case ExtensionType.ScaledUiAmountConfig: return true; case ExtensionType.Uninitialized: case ExtensionType.TransferFeeAmount: @@ -174,6 +180,7 @@ export function isAccountExtension(e: ExtensionType): boolean { case ExtensionType.GroupMemberPointer: case ExtensionType.TokenGroup: case ExtensionType.TokenGroupMember: + case ExtensionType.ScaledUiAmountConfig: return false; default: throw Error(`Unknown extension type: ${e}`); @@ -208,6 +215,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType { case ExtensionType.GroupMemberPointer: case ExtensionType.TokenGroup: case ExtensionType.TokenGroupMember: + case ExtensionType.ScaledUiAmountConfig: return ExtensionType.Uninitialized; } } diff --git a/clients/js-legacy/src/extensions/index.ts b/clients/js-legacy/src/extensions/index.ts index cfa585b9..341f44a7 100644 --- a/clients/js-legacy/src/extensions/index.ts +++ b/clients/js-legacy/src/extensions/index.ts @@ -8,6 +8,7 @@ export * from './immutableOwner.js'; export * from './interestBearingMint/index.js'; export * from './memoTransfer/index.js'; export * from './metadataPointer/index.js'; +export * from './scaledUiAmount/index.js'; export * from './tokenGroup/index.js'; export * from './tokenMetadata/index.js'; export * from './mintCloseAuthority.js'; diff --git a/clients/js-legacy/src/extensions/scaledUiAmount/actions.ts b/clients/js-legacy/src/extensions/scaledUiAmount/actions.ts new file mode 100644 index 00000000..10668a9d --- /dev/null +++ b/clients/js-legacy/src/extensions/scaledUiAmount/actions.ts @@ -0,0 +1,47 @@ +import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; +import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; +import { getSigners } from '../../actions/internal.js'; +import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; +import { createUpdateMultiplierDataInstruction } from './instructions.js'; + +/** + * Update scaled UI amount multiplier + * + * @param connection Connection to use + * @param payer Payer of the transaction fees + * @param mint The token mint + * @param owner Owner of the scaled UI amount mint + * @param multiplier New multiplier + * @param effectiveTimestamp Effective time stamp for the new multiplier + * @param multiSigners Signing accounts if `owner` is a multisig + * @param confirmOptions Options for confirming the transaction + * @param programId SPL Token program account + * + * @return Signature of the confirmed transaction + */ +export async function updateMultiplier( + connection: Connection, + payer: Signer, + mint: PublicKey, + owner: Signer | PublicKey, + multiplier: number, + effectiveTimestamp: bigint, + multiSigners: Signer[] = [], + confirmOptions?: ConfirmOptions, + programId = TOKEN_2022_PROGRAM_ID, +): Promise { + const [ownerPublicKey, signers] = getSigners(owner, multiSigners); + + const transaction = new Transaction().add( + createUpdateMultiplierDataInstruction( + mint, + ownerPublicKey, + multiplier, + effectiveTimestamp, + multiSigners, + programId, + ), + ); + + return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); +} diff --git a/clients/js-legacy/src/extensions/scaledUiAmount/index.ts b/clients/js-legacy/src/extensions/scaledUiAmount/index.ts new file mode 100644 index 00000000..5e28fd6b --- /dev/null +++ b/clients/js-legacy/src/extensions/scaledUiAmount/index.ts @@ -0,0 +1,3 @@ +export * from './actions.js'; +export * from './instructions.js'; +export * from './state.js'; diff --git a/clients/js-legacy/src/extensions/scaledUiAmount/instructions.ts b/clients/js-legacy/src/extensions/scaledUiAmount/instructions.ts new file mode 100644 index 00000000..e02c915e --- /dev/null +++ b/clients/js-legacy/src/extensions/scaledUiAmount/instructions.ts @@ -0,0 +1,115 @@ +import { struct, u8, f64 } from '@solana/buffer-layout'; +import { publicKey, u64 } from '@solana/buffer-layout-utils'; +import { TokenInstruction } from '../../instructions/types.js'; +import type { Signer } from '@solana/web3.js'; +import { TransactionInstruction, PublicKey } from '@solana/web3.js'; +import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; +import { TokenUnsupportedInstructionError } from '../../errors.js'; +import { addSigners } from '../../instructions/internal.js'; + +export enum ScaledUiAmountInstruction { + Initialize = 0, + UpdateMultiplier = 1, +} + +export interface InitializeScaledUiAmountConfigData { + instruction: TokenInstruction.ScaledUiAmountExtension; + scaledUiAmountInstruction: ScaledUiAmountInstruction.Initialize; + authority: PublicKey | null; + multiplier: number; +} + +export const initializeScaledUiAmountConfigInstructionData = struct([ + u8('instruction'), + u8('scaledUiAmountInstruction'), + publicKey('authority'), + f64('multiplier'), +]); + +/** + * Construct an InitializeScaledUiAmountConfig instruction + * + * @param mint Token mint account + * @param authority Optional authority that can update the multipliers + * @param signers The signer account(s) + * @param programId SPL Token program account + * + * @return Instruction to add to a transaction + */ +export function createInitializeScaledUiAmountConfigInstruction( + mint: PublicKey, + authority: PublicKey | null, + multiplier: number, + programId: PublicKey = TOKEN_2022_PROGRAM_ID, +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; + + const data = Buffer.alloc(initializeScaledUiAmountConfigInstructionData.span); + initializeScaledUiAmountConfigInstructionData.encode( + { + instruction: TokenInstruction.ScaledUiAmountExtension, + scaledUiAmountInstruction: ScaledUiAmountInstruction.Initialize, + authority: authority ?? PublicKey.default, + multiplier: multiplier, + }, + data, + ); + + return new TransactionInstruction({ keys, programId, data }); +} + +export interface UpdateMultiplierData { + instruction: TokenInstruction.ScaledUiAmountExtension; + scaledUiAmountInstruction: ScaledUiAmountInstruction.UpdateMultiplier; + multiplier: number; + effectiveTimestamp: bigint; +} + +export const updateMultiplierData = struct([ + u8('instruction'), + u8('scaledUiAmountInstruction'), + f64('multiplier'), + u64('effectiveTimestamp'), +]); + +/** + * Construct an UpdateMultiplierData instruction + * + * @param mint Token mint account + * @param authority Optional authority that can update the multipliers + * @param multiplier New multiplier + * @param effectiveTimestamp Effective time stamp for the new multiplier + * @param multiSigners Signing accounts if `owner` is a multisig + * @param programId SPL Token program account + * + * @return Instruction to add to a transaction + */ +export function createUpdateMultiplierDataInstruction( + mint: PublicKey, + authority: PublicKey, + multiplier: number, + effectiveTimestamp: bigint, + multiSigners: (Signer | PublicKey)[] = [], + programId: PublicKey = TOKEN_2022_PROGRAM_ID, +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners); + + const data = Buffer.alloc(updateMultiplierData.span); + updateMultiplierData.encode( + { + instruction: TokenInstruction.ScaledUiAmountExtension, + scaledUiAmountInstruction: ScaledUiAmountInstruction.UpdateMultiplier, + multiplier, + effectiveTimestamp, + }, + data, + ); + + return new TransactionInstruction({ keys, programId, data }); +} diff --git a/clients/js-legacy/src/extensions/scaledUiAmount/state.ts b/clients/js-legacy/src/extensions/scaledUiAmount/state.ts new file mode 100644 index 00000000..36b00d65 --- /dev/null +++ b/clients/js-legacy/src/extensions/scaledUiAmount/state.ts @@ -0,0 +1,29 @@ +import { f64, struct } from '@solana/buffer-layout'; +import { publicKey, u64 } from '@solana/buffer-layout-utils'; +import type { PublicKey } from '@solana/web3.js'; +import type { Mint } from '../../state/mint.js'; +import { ExtensionType, getExtensionData } from '../extensionType.js'; + +export interface ScaledUiAmountConfig { + authority: PublicKey; + multiplier: number; + newMultiplierEffectiveTimestamp: bigint; + newMultiplier: number; +} + +export const ScaledUiAmountConfigLayout = struct([ + publicKey('authority'), + f64('multiplier'), + u64('newMultiplierEffectiveTimestamp'), + f64('newMultiplier'), +]); + +export const SCALED_UI_AMOUNT_CONFIG_SIZE = ScaledUiAmountConfigLayout.span; + +export function getScaledUiAmountConfig(mint: Mint): ScaledUiAmountConfig | null { + const extensionData = getExtensionData(ExtensionType.ScaledUiAmountConfig, mint.tlvData); + if (extensionData !== null) { + return ScaledUiAmountConfigLayout.decode(extensionData); + } + return null; +} diff --git a/clients/js-legacy/src/instructions/setAuthority.ts b/clients/js-legacy/src/instructions/setAuthority.ts index d09bd69b..ca81a7f4 100644 --- a/clients/js-legacy/src/instructions/setAuthority.ts +++ b/clients/js-legacy/src/instructions/setAuthority.ts @@ -30,6 +30,7 @@ export enum AuthorityType { MetadataPointer = 12, GroupPointer = 13, GroupMemberPointer = 14, + ScaledUiAmountConfig = 15, } /** TODO: docs */ diff --git a/clients/js-legacy/src/instructions/types.ts b/clients/js-legacy/src/instructions/types.ts index ca176452..26b693c3 100644 --- a/clients/js-legacy/src/instructions/types.ts +++ b/clients/js-legacy/src/instructions/types.ts @@ -42,4 +42,6 @@ export enum TokenInstruction { MetadataPointerExtension = 39, GroupPointerExtension = 40, GroupMemberPointerExtension = 41, + // ConfidentialMintBurnExtension = 42, + ScaledUiAmountExtension = 43, } diff --git a/clients/js-legacy/test/e2e-2022/scaledUiAmount.test.ts b/clients/js-legacy/test/e2e-2022/scaledUiAmount.test.ts new file mode 100644 index 00000000..99a6a2c1 --- /dev/null +++ b/clients/js-legacy/test/e2e-2022/scaledUiAmount.test.ts @@ -0,0 +1,110 @@ +import { expect } from 'chai'; +import type { Connection, Signer } from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; +import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; +import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; + +import { + ExtensionType, + createInitializeMintInstruction, + createInitializeScaledUiAmountConfigInstruction, + getMint, + getMintLen, + getScaledUiAmountConfig, + updateMultiplier, + setAuthority, + AuthorityType, +} from '../../src'; + +const TEST_TOKEN_DECIMALS = 2; +const MINT_EXTENSIONS = [ExtensionType.ScaledUiAmountConfig]; + +describe('scaledUiAmount', () => { + let connection: Connection; + let payer: Signer; + let owner: Keypair; + let mint: PublicKey; + let mintAuthority: Keypair; + let multiplier: number; + before(async () => { + connection = await getConnection(); + payer = await newAccountWithLamports(connection, 1000000000); + owner = Keypair.generate(); + multiplier = 5.0; + }); + + beforeEach(async () => { + const mintKeypair = Keypair.generate(); + mint = mintKeypair.publicKey; + mintAuthority = Keypair.generate(); + const mintLen = getMintLen(MINT_EXTENSIONS); + const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint, + space: mintLen, + lamports: mintLamports, + programId: TEST_PROGRAM_ID, + }), + createInitializeScaledUiAmountConfigInstruction(mint, owner.publicKey, multiplier, TEST_PROGRAM_ID), + createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), + ); + await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); + }); + + it('initialize mint', async () => { + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo); + expect(scaledUiAmountConfig).to.not.equal(null); + if (scaledUiAmountConfig !== null) { + expect(scaledUiAmountConfig.authority).to.eql(owner.publicKey); + expect(scaledUiAmountConfig.multiplier).to.eql(multiplier); + } + }); + + it('update authority', async () => { + await setAuthority( + connection, + payer, + mint, + owner, + AuthorityType.ScaledUiAmountConfig, + null, + [], + undefined, + TEST_PROGRAM_ID, + ); + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo); + expect(scaledUiAmountConfig).to.not.equal(null); + if (scaledUiAmountConfig !== null) { + expect(scaledUiAmountConfig.authority).to.eql(PublicKey.default); + } + }); + + it('update multiplier', async () => { + const newMultiplier = 10.0; + const effectiveTimestamp = BigInt(1000); + + await updateMultiplier( + connection, + payer, + mint, + owner, + newMultiplier, + effectiveTimestamp, + [], + undefined, + TEST_PROGRAM_ID, + ); + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo); + expect(scaledUiAmountConfig).to.not.equal(null); + if (scaledUiAmountConfig !== null) { + expect(scaledUiAmountConfig.multiplier).to.eql(newMultiplier); + expect(scaledUiAmountConfig.newMultiplierEffectiveTimestamp).to.eql(effectiveTimestamp); + expect(scaledUiAmountConfig.newMultiplier).to.eql(newMultiplier); + } + }); +});