Skip to content

Commit 22a6437

Browse files
[js-legacy] Add scaled ui amount extension (#60)
1 parent 61658b9 commit 22a6437

File tree

9 files changed

+316
-0
lines changed

9 files changed

+316
-0
lines changed

clients/js-legacy/src/extensions/extensionType.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { METADATA_POINTER_SIZE } from './metadataPointer/state.js';
1717
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
1818
import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js';
1919
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
20+
import { SCALED_UI_AMOUNT_CONFIG_SIZE } from './scaledUiAmount/index.js';
2021
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
2122
import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
2223
import { TOKEN_2022_PROGRAM_ID } from '../constants.js';
@@ -47,6 +48,8 @@ export enum ExtensionType {
4748
TokenGroup = 21,
4849
GroupMemberPointer = 22,
4950
TokenGroupMember = 23,
51+
// ConfidentialMintBurn, // Not implemented yet
52+
ScaledUiAmountConfig = 25,
5053
}
5154

5255
export const TYPE_SIZE = 2;
@@ -111,6 +114,8 @@ export function getTypeLen(e: ExtensionType): number {
111114
return TOKEN_GROUP_SIZE;
112115
case ExtensionType.TokenGroupMember:
113116
return TOKEN_GROUP_MEMBER_SIZE;
117+
case ExtensionType.ScaledUiAmountConfig:
118+
return SCALED_UI_AMOUNT_CONFIG_SIZE;
114119
case ExtensionType.TokenMetadata:
115120
throw Error(`Cannot get type length for variable extension type: ${e}`);
116121
default:
@@ -134,6 +139,7 @@ export function isMintExtension(e: ExtensionType): boolean {
134139
case ExtensionType.GroupMemberPointer:
135140
case ExtensionType.TokenGroup:
136141
case ExtensionType.TokenGroupMember:
142+
case ExtensionType.ScaledUiAmountConfig:
137143
return true;
138144
case ExtensionType.Uninitialized:
139145
case ExtensionType.TransferFeeAmount:
@@ -174,6 +180,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
174180
case ExtensionType.GroupMemberPointer:
175181
case ExtensionType.TokenGroup:
176182
case ExtensionType.TokenGroupMember:
183+
case ExtensionType.ScaledUiAmountConfig:
177184
return false;
178185
default:
179186
throw Error(`Unknown extension type: ${e}`);
@@ -208,6 +215,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
208215
case ExtensionType.GroupMemberPointer:
209216
case ExtensionType.TokenGroup:
210217
case ExtensionType.TokenGroupMember:
218+
case ExtensionType.ScaledUiAmountConfig:
211219
return ExtensionType.Uninitialized;
212220
}
213221
}

clients/js-legacy/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './immutableOwner.js';
88
export * from './interestBearingMint/index.js';
99
export * from './memoTransfer/index.js';
1010
export * from './metadataPointer/index.js';
11+
export * from './scaledUiAmount/index.js';
1112
export * from './tokenGroup/index.js';
1213
export * from './tokenMetadata/index.js';
1314
export * from './mintCloseAuthority.js';
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
2+
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
3+
import { getSigners } from '../../actions/internal.js';
4+
import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
5+
import { createUpdateMultiplierDataInstruction } from './instructions.js';
6+
7+
/**
8+
* Update scaled UI amount multiplier
9+
*
10+
* @param connection Connection to use
11+
* @param payer Payer of the transaction fees
12+
* @param mint The token mint
13+
* @param owner Owner of the scaled UI amount mint
14+
* @param multiplier New multiplier
15+
* @param effectiveTimestamp Effective time stamp for the new multiplier
16+
* @param multiSigners Signing accounts if `owner` is a multisig
17+
* @param confirmOptions Options for confirming the transaction
18+
* @param programId SPL Token program account
19+
*
20+
* @return Signature of the confirmed transaction
21+
*/
22+
export async function updateMultiplier(
23+
connection: Connection,
24+
payer: Signer,
25+
mint: PublicKey,
26+
owner: Signer | PublicKey,
27+
multiplier: number,
28+
effectiveTimestamp: bigint,
29+
multiSigners: Signer[] = [],
30+
confirmOptions?: ConfirmOptions,
31+
programId = TOKEN_2022_PROGRAM_ID,
32+
): Promise<TransactionSignature> {
33+
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);
34+
35+
const transaction = new Transaction().add(
36+
createUpdateMultiplierDataInstruction(
37+
mint,
38+
ownerPublicKey,
39+
multiplier,
40+
effectiveTimestamp,
41+
multiSigners,
42+
programId,
43+
),
44+
);
45+
46+
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
47+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './actions.js';
2+
export * from './instructions.js';
3+
export * from './state.js';
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { struct, u8, f64 } from '@solana/buffer-layout';
2+
import { publicKey, u64 } from '@solana/buffer-layout-utils';
3+
import { TokenInstruction } from '../../instructions/types.js';
4+
import type { Signer } from '@solana/web3.js';
5+
import { TransactionInstruction, PublicKey } from '@solana/web3.js';
6+
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js';
7+
import { TokenUnsupportedInstructionError } from '../../errors.js';
8+
import { addSigners } from '../../instructions/internal.js';
9+
10+
export enum ScaledUiAmountInstruction {
11+
Initialize = 0,
12+
UpdateMultiplier = 1,
13+
}
14+
15+
export interface InitializeScaledUiAmountConfigData {
16+
instruction: TokenInstruction.ScaledUiAmountExtension;
17+
scaledUiAmountInstruction: ScaledUiAmountInstruction.Initialize;
18+
authority: PublicKey | null;
19+
multiplier: number;
20+
}
21+
22+
export const initializeScaledUiAmountConfigInstructionData = struct<InitializeScaledUiAmountConfigData>([
23+
u8('instruction'),
24+
u8('scaledUiAmountInstruction'),
25+
publicKey('authority'),
26+
f64('multiplier'),
27+
]);
28+
29+
/**
30+
* Construct an InitializeScaledUiAmountConfig instruction
31+
*
32+
* @param mint Token mint account
33+
* @param authority Optional authority that can update the multipliers
34+
* @param signers The signer account(s)
35+
* @param programId SPL Token program account
36+
*
37+
* @return Instruction to add to a transaction
38+
*/
39+
export function createInitializeScaledUiAmountConfigInstruction(
40+
mint: PublicKey,
41+
authority: PublicKey | null,
42+
multiplier: number,
43+
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
44+
): TransactionInstruction {
45+
if (!programSupportsExtensions(programId)) {
46+
throw new TokenUnsupportedInstructionError();
47+
}
48+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
49+
50+
const data = Buffer.alloc(initializeScaledUiAmountConfigInstructionData.span);
51+
initializeScaledUiAmountConfigInstructionData.encode(
52+
{
53+
instruction: TokenInstruction.ScaledUiAmountExtension,
54+
scaledUiAmountInstruction: ScaledUiAmountInstruction.Initialize,
55+
authority: authority ?? PublicKey.default,
56+
multiplier: multiplier,
57+
},
58+
data,
59+
);
60+
61+
return new TransactionInstruction({ keys, programId, data });
62+
}
63+
64+
export interface UpdateMultiplierData {
65+
instruction: TokenInstruction.ScaledUiAmountExtension;
66+
scaledUiAmountInstruction: ScaledUiAmountInstruction.UpdateMultiplier;
67+
multiplier: number;
68+
effectiveTimestamp: bigint;
69+
}
70+
71+
export const updateMultiplierData = struct<UpdateMultiplierData>([
72+
u8('instruction'),
73+
u8('scaledUiAmountInstruction'),
74+
f64('multiplier'),
75+
u64('effectiveTimestamp'),
76+
]);
77+
78+
/**
79+
* Construct an UpdateMultiplierData instruction
80+
*
81+
* @param mint Token mint account
82+
* @param authority Optional authority that can update the multipliers
83+
* @param multiplier New multiplier
84+
* @param effectiveTimestamp Effective time stamp for the new multiplier
85+
* @param multiSigners Signing accounts if `owner` is a multisig
86+
* @param programId SPL Token program account
87+
*
88+
* @return Instruction to add to a transaction
89+
*/
90+
export function createUpdateMultiplierDataInstruction(
91+
mint: PublicKey,
92+
authority: PublicKey,
93+
multiplier: number,
94+
effectiveTimestamp: bigint,
95+
multiSigners: (Signer | PublicKey)[] = [],
96+
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
97+
): TransactionInstruction {
98+
if (!programSupportsExtensions(programId)) {
99+
throw new TokenUnsupportedInstructionError();
100+
}
101+
const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners);
102+
103+
const data = Buffer.alloc(updateMultiplierData.span);
104+
updateMultiplierData.encode(
105+
{
106+
instruction: TokenInstruction.ScaledUiAmountExtension,
107+
scaledUiAmountInstruction: ScaledUiAmountInstruction.UpdateMultiplier,
108+
multiplier,
109+
effectiveTimestamp,
110+
},
111+
data,
112+
);
113+
114+
return new TransactionInstruction({ keys, programId, data });
115+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { f64, struct } from '@solana/buffer-layout';
2+
import { publicKey, u64 } from '@solana/buffer-layout-utils';
3+
import type { PublicKey } from '@solana/web3.js';
4+
import type { Mint } from '../../state/mint.js';
5+
import { ExtensionType, getExtensionData } from '../extensionType.js';
6+
7+
export interface ScaledUiAmountConfig {
8+
authority: PublicKey;
9+
multiplier: number;
10+
newMultiplierEffectiveTimestamp: bigint;
11+
newMultiplier: number;
12+
}
13+
14+
export const ScaledUiAmountConfigLayout = struct<ScaledUiAmountConfig>([
15+
publicKey('authority'),
16+
f64('multiplier'),
17+
u64('newMultiplierEffectiveTimestamp'),
18+
f64('newMultiplier'),
19+
]);
20+
21+
export const SCALED_UI_AMOUNT_CONFIG_SIZE = ScaledUiAmountConfigLayout.span;
22+
23+
export function getScaledUiAmountConfig(mint: Mint): ScaledUiAmountConfig | null {
24+
const extensionData = getExtensionData(ExtensionType.ScaledUiAmountConfig, mint.tlvData);
25+
if (extensionData !== null) {
26+
return ScaledUiAmountConfigLayout.decode(extensionData);
27+
}
28+
return null;
29+
}

clients/js-legacy/src/instructions/setAuthority.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export enum AuthorityType {
3030
MetadataPointer = 12,
3131
GroupPointer = 13,
3232
GroupMemberPointer = 14,
33+
ScaledUiAmountConfig = 15,
3334
}
3435

3536
/** TODO: docs */

clients/js-legacy/src/instructions/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ export enum TokenInstruction {
4242
MetadataPointerExtension = 39,
4343
GroupPointerExtension = 40,
4444
GroupMemberPointerExtension = 41,
45+
// ConfidentialMintBurnExtension = 42,
46+
ScaledUiAmountExtension = 43,
4547
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { expect } from 'chai';
2+
import type { Connection, Signer } from '@solana/web3.js';
3+
import { PublicKey } from '@solana/web3.js';
4+
import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
5+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
6+
7+
import {
8+
ExtensionType,
9+
createInitializeMintInstruction,
10+
createInitializeScaledUiAmountConfigInstruction,
11+
getMint,
12+
getMintLen,
13+
getScaledUiAmountConfig,
14+
updateMultiplier,
15+
setAuthority,
16+
AuthorityType,
17+
} from '../../src';
18+
19+
const TEST_TOKEN_DECIMALS = 2;
20+
const MINT_EXTENSIONS = [ExtensionType.ScaledUiAmountConfig];
21+
22+
describe('scaledUiAmount', () => {
23+
let connection: Connection;
24+
let payer: Signer;
25+
let owner: Keypair;
26+
let mint: PublicKey;
27+
let mintAuthority: Keypair;
28+
let multiplier: number;
29+
before(async () => {
30+
connection = await getConnection();
31+
payer = await newAccountWithLamports(connection, 1000000000);
32+
owner = Keypair.generate();
33+
multiplier = 5.0;
34+
});
35+
36+
beforeEach(async () => {
37+
const mintKeypair = Keypair.generate();
38+
mint = mintKeypair.publicKey;
39+
mintAuthority = Keypair.generate();
40+
const mintLen = getMintLen(MINT_EXTENSIONS);
41+
const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen);
42+
const mintTransaction = new Transaction().add(
43+
SystemProgram.createAccount({
44+
fromPubkey: payer.publicKey,
45+
newAccountPubkey: mint,
46+
space: mintLen,
47+
lamports: mintLamports,
48+
programId: TEST_PROGRAM_ID,
49+
}),
50+
createInitializeScaledUiAmountConfigInstruction(mint, owner.publicKey, multiplier, TEST_PROGRAM_ID),
51+
createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID),
52+
);
53+
await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined);
54+
});
55+
56+
it('initialize mint', async () => {
57+
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
58+
const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo);
59+
expect(scaledUiAmountConfig).to.not.equal(null);
60+
if (scaledUiAmountConfig !== null) {
61+
expect(scaledUiAmountConfig.authority).to.eql(owner.publicKey);
62+
expect(scaledUiAmountConfig.multiplier).to.eql(multiplier);
63+
}
64+
});
65+
66+
it('update authority', async () => {
67+
await setAuthority(
68+
connection,
69+
payer,
70+
mint,
71+
owner,
72+
AuthorityType.ScaledUiAmountConfig,
73+
null,
74+
[],
75+
undefined,
76+
TEST_PROGRAM_ID,
77+
);
78+
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
79+
const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo);
80+
expect(scaledUiAmountConfig).to.not.equal(null);
81+
if (scaledUiAmountConfig !== null) {
82+
expect(scaledUiAmountConfig.authority).to.eql(PublicKey.default);
83+
}
84+
});
85+
86+
it('update multiplier', async () => {
87+
const newMultiplier = 10.0;
88+
const effectiveTimestamp = BigInt(1000);
89+
90+
await updateMultiplier(
91+
connection,
92+
payer,
93+
mint,
94+
owner,
95+
newMultiplier,
96+
effectiveTimestamp,
97+
[],
98+
undefined,
99+
TEST_PROGRAM_ID,
100+
);
101+
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
102+
const scaledUiAmountConfig = getScaledUiAmountConfig(mintInfo);
103+
expect(scaledUiAmountConfig).to.not.equal(null);
104+
if (scaledUiAmountConfig !== null) {
105+
expect(scaledUiAmountConfig.multiplier).to.eql(newMultiplier);
106+
expect(scaledUiAmountConfig.newMultiplierEffectiveTimestamp).to.eql(effectiveTimestamp);
107+
expect(scaledUiAmountConfig.newMultiplier).to.eql(newMultiplier);
108+
}
109+
});
110+
});

0 commit comments

Comments
 (0)