Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[js-legacy] Add scaled ui amount extension #60

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
add scaled ui amount extension to js-legacy client
  • Loading branch information
samkim-crypto committed Jan 13, 2025
commit 2a960338f542627b75041001d3863470cc138040
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);
}
});
});
Loading