Skip to content

Commit

Permalink
[js-legacy] Add scaled ui amount extension (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
samkim-crypto authored Jan 13, 2025
1 parent 61658b9 commit 22a6437
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 0 deletions.
8 changes: 8 additions & 0 deletions clients/js-legacy/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,6 +48,8 @@ export enum ExtensionType {
TokenGroup = 21,
GroupMemberPointer = 22,
TokenGroupMember = 23,
// ConfidentialMintBurn, // Not implemented yet
ScaledUiAmountConfig = 25,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
47 changes: 47 additions & 0 deletions clients/js-legacy/src/extensions/scaledUiAmount/actions.ts
Original file line number Diff line number Diff line change
@@ -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<TransactionSignature> {
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);
}
3 changes: 3 additions & 0 deletions clients/js-legacy/src/extensions/scaledUiAmount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './actions.js';
export * from './instructions.js';
export * from './state.js';
115 changes: 115 additions & 0 deletions clients/js-legacy/src/extensions/scaledUiAmount/instructions.ts
Original file line number Diff line number Diff line change
@@ -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<InitializeScaledUiAmountConfigData>([
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<UpdateMultiplierData>([
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 });
}
29 changes: 29 additions & 0 deletions clients/js-legacy/src/extensions/scaledUiAmount/state.ts
Original file line number Diff line number Diff line change
@@ -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<ScaledUiAmountConfig>([
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;
}
1 change: 1 addition & 0 deletions clients/js-legacy/src/instructions/setAuthority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum AuthorityType {
MetadataPointer = 12,
GroupPointer = 13,
GroupMemberPointer = 14,
ScaledUiAmountConfig = 15,
}

/** TODO: docs */
Expand Down
2 changes: 2 additions & 0 deletions clients/js-legacy/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ export enum TokenInstruction {
MetadataPointerExtension = 39,
GroupPointerExtension = 40,
GroupMemberPointerExtension = 41,
// ConfidentialMintBurnExtension = 42,
ScaledUiAmountExtension = 43,
}
110 changes: 110 additions & 0 deletions clients/js-legacy/test/e2e-2022/scaledUiAmount.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});

0 comments on commit 22a6437

Please sign in to comment.