在Solana上使用 Scaled UI Amount 扩展
本指南提供 Solana Web3.js (Legacy v 1.x) 和 Solana Kit (v 2.x) 版本。选择适当的选项卡以查看你首选库的代码片段和说明:
- Solana Web3.js (Legacy)
- Solana Kit
概述
Solana Token-2022 程序 引入了强大的扩展,增强了代币功能,使其超越了原始的 SPL Token 程序。其中一个扩展是 Scaled UI Amount(缩放 UI 金额),它允许代币发行者定义一个乘数,该乘数会影响向用户显示的代币余额,而不会更改链上存储的底层原始金额。此扩展启用了强大的用例,包括:
- 股票拆分和反向股票拆分
- 视觉上累积利息的计息代币
- 股息和分配
- 调整总供应量的重新定价代币
为了使用 Scaled UI Amount 扩展进行构建,开发人员需要了解它的工作原理以及如何在他们的应用程序中实现它。本指南将引导你完成创建具有 Scaled UI Amount 扩展的代币、铸造代币、转移代币以及更新 UI 金额乘数的过程,以了解它如何影响显示的余额。
在本指南中,我们将构建一个完整的演示脚本,该脚本:
- 创建一个具有 Scaled UI Amount 扩展的 Token-2022 代币
- 将代币铸造给持有者
- 在帐户之间转移代币
- 更新 UI 乘数
- 显示 UI 金额如何在原始余额保持一致的同时发生变化
- 铸造和转移额外的代币以观察更新后的乘数的影响
最终目标是生成一个摘要表,清楚地显示演示的每个步骤中原始金额和 UI 金额之间的关系:
=== DEMONSTRATION SUMMARY ===
┌─────────┬───────────────────────────┬──────────────┬────────────┬─────────────┬────────────┐
│ (index) │ Step │ Timestamp │ Multiplier │ Raw Balance │ UI Balance │
├─────────┼───────────────────────────┼──────────────┼────────────┼─────────────┼────────────┤
│ 0 │ 'Initial Setup' │ '3:02:16 PM' │ 1 │ 'n/a' │ 'n/a' │
│ 1 │ 'After Initial Mint' │ '3:02:17 PM' │ 1 │ '100000000' │ '100' │
│ 2 │ 'After Transfer #1' │ '3:02:18 PM' │ 1 │ '90000000' │ '90' │
│ 3 │ 'After Multiplier Update' │ '3:02:19 PM' │ 2 │ '90000000' │ '180' │
│ 4 │ 'After Second Mint' │ '3:02:20 PM' │ 2 │ '190000000' │ '380' │
│ 5 │ 'After Transfer #2' │ '3:02:21 PM' │ 2 │ '180000000' │ '360' │
└─────────┴───────────────────────────┴──────────────┴────────────┴─────────────┴────────────┘
让我们开始吧!
前提条件
在开始本教程之前,请确保你已具备:
- Node.js (v22 或更高版本)
- Solana CLI v 2.2.x 或更高版本(注意:如果你已经有较旧的版本,你可以使用
agave-install init 2.2.14
或任何最新版本来更新它) - 对 Solana 和 TypeScript 的基本了解
- 熟悉 Solana Token-2022 程序 和 SPL Token 概念
了解 Scaled UI Amount 扩展
在深入实施之前,让我们了解什么是 Scaled UI Amount 扩展以及它的工作原理。Scaled UI Amount 扩展定义了一个乘数,该乘数应用于代币的原始金额以确定向用户显示的 UI 金额。这允许灵活的代币经济学,而无需更改底层原始金额。
深入了解
该扩展还允许将来更新乘数,从而实现诸如逐渐增加或计划更改之类的功能。这是 Scaled UI Amount 配置的结构:
pub struct ScaledUiAmountConfig {pub authority: OptionalNonZeroPubkey,pub multiplier: PodF64,pub new_multiplier_effective_timestamp: UnixTimestamp,pub new_multiplier: PodF64,
}
(来源: Solana Token-2022 Program)
该配置包括:
authority
:可以设置缩放金额的授权公钥multiplier
:应用于原始金额的当前乘数new_multiplier_effective_timestamp
:新乘数生效的时间戳new_multiplier
:一旦达到生效时间戳,将应用的新乘数
Scaled UI Amount 扩展引入了两个新指令:
initialize
: 初始化 mint 的 Scaled UI Amount 扩展 ( src)update_multiplier
:更新 mint 的乘数 ( src)
关键概念
Scaled UI Amount 扩展引入了几个重要的关键概念:
- 原始金额 vs. UI 金额:
- 原始金额是链上存储的代币的实际数量
- UI 金额是用户看到的,计算公式为“原始金额 × 当前乘数”
- 乘数:
- 创建代币时设置
- 可以由代币的授权机构更新
- 可以安排以供将来更新
- UI 金额更改:
- 当乘数更改时,UI 金额会成比例地更改
- 原始金额保持不变
我们的演示将展示这些概念如何在实践中发挥作用。
实际应用
以下是 Scaled UI Amount 扩展的一些实际应用:
用例 | 描述 | 实施方法 |
---|---|---|
股票拆分和反向股票拆分 | 将现有股份分成多个或部分股份 | 调整乘数以反映新的股份数量 |
计息代币 | 随着时间的推移在视觉上累积利息的代币 | 根据收益率逐步增加乘数 |
股息分配 | 向代币持有者分配股息 | 调整乘数以反映股息分配 |
面额变更 | 在同一资产的不同单位之间转换 | 更改乘数以表示新的面额 |
重新定价代币 | 根据外部因素(如算法稳定币)增加/减少总供应量的代币 | 定期调整乘数以反映供应变化 |
对现有项目的影响
实施 Scaled UI Amount 扩展需要仔细考虑应用程序的架构和功能。虽然你的影响可能是独一无二的,但以下是一些常见的考虑因素:
- UI/UX 设计考虑因素:必须更新应用程序以向用户显示缩放的 UI 金额,同时在后端处理原始金额(SPL 代币程序库中添加了一些有用的方法(例如,
amountToUiAmount
,uiAmountToAmount
))。 这需要仔细的 UX 设计,以防止用户混淆,尤其是在乘数更改期间。 考虑在代币使用此扩展时添加工具提示或指示器。 - 历史数据管理:服务应考虑索引原始金额和 UI 金额,以及历史乘数值,以正确显示交易历史和价格图表。 这对于乘数频繁更改的代币尤其重要。
- 交易处理:在处理转移或交换的用户输入时,应用程序需要在提交交易之前将 UI 金额(用户输入的金额)转换为原始金额(链上处理器使用的金额)。 舍入问题应通过首选向下舍入来处理,以避免交易失败。
- 价格馈送集成:价格服务应提供缩放和非缩放的价格馈送,以适应不同的客户端需求。 对于市场数据提供商,总供应量和市值计算必须考虑当前乘数才能显示准确的信息。
项目设置
更喜欢直接跳到代码?查看我们在 GitHub 上的示例存储库,获取本指南的完整代码!
- Solana Web3.js (Legacy)
- Solana Kit
在我们开始之前,让我们回顾一下我们将要构建的内容。我们将创建一个简单的演示脚本,该脚本:
- 创建一个具有 Scaled UI Amount 扩展的 Token-2022 代币
- 将代币铸造给持有者
- 在帐户之间转移代币
- 更新 UI 乘数
- 铸造和转移额外的代币以观察更新后的乘数的影响
- 记录每个步骤的状态并打印一个摘要表,演示乘数、原始金额和 UI 金额之间的关系
让我们从创建我们的项目结构开始:
mkdir solana-scaled-token-demo && cd solana-scaled-token-demo
初始化一个新的 Node.js 项目:
npm init -y
安装所需的依赖项:
npm install @solana/web3.js@1 @solana/spl-token
以及其他开发依赖项:
npm install --save-dev typescript ts-node @types/node
创建一个 tsconfig.json
文件:
{"compilerOptions": {"target": "es2020","module": "commonjs","lib": ["es2020"],"declaration": true,"outDir": "./dist","rootDir": "./","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"resolveJsonModule": true},"include": ["*.ts"],"exclude": ["node_modules", "dist"]
}
更新你的 package.json
脚本:
"scripts": {"start": "ts-node token-creator.ts","build": "tsc"
}
创建一个用于存储密钥对的目录:
mkdir -p keys
为付款人、mint 授权机构、代币持有者和 mint 创建新的 Solana 密钥对:
solana-keygen new -s --no-bip39-passphrase -o keys/payer.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/mint-authority.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/holder.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/mint.json
这应该在 keys
目录中创建四个密钥对文件。
在我们开始之前,让我们回顾一下我们将要构建的内容。我们将创建一个简单的演示脚本,该脚本:
- 创建一个具有 Scaled UI Amount 扩展的 Token-2022 代币
- 将代币铸造给持有者
- 在帐户之间转移代币
- 更新 UI 乘数
- 铸造和转移额外的代币以观察更新后的乘数的影响
- 记录每个步骤的状态并打印一个摘要表,演示乘数、原始金额和 UI 金额之间的关系
让我们从创建我们的项目结构开始:
mkdir solana-scaled-token-demo && cd solana-scaled-token-demo
初始化一个新的 Node.js 项目:
npm init -y
安装所需的依赖项:
npm i @solana/kit @solana-program/token-2022 @solana-program/system
以及其他开发依赖项:
npm install --save-dev typescript ts-node @types/node
创建一个 tsconfig.json
文件:
{"compilerOptions": {"target": "es2020","module": "commonjs","lib": ["es2020"],"declaration": true,"outDir": "./dist","rootDir": "./","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"resolveJsonModule": true},"include": ["*.ts"],"exclude": ["node_modules", "dist"]
}
更新你的 package.json
脚本:
"scripts": {"start": "ts-node token-creator.ts","build": "tsc"
}
创建一个用于存储密钥对的目录:
mkdir -p keys
为付款人、mint 授权机构、代币持有者和 mint 创建新的 Solana 密钥对:
solana-keygen new -s --no-bip39-passphrase -o keys/payer.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/mint-authority.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/holder.json && \
solana-keygen new -s --no-bip39-passphrase -o keys/mint.json
这应该在 keys
目录中创建四个密钥对文件。
实施
- Solana Web3.js (Legacy)
- Solana Kit
让我们创建我们的 token-creator.ts
文件并逐步构建它:
导入和配置
从必要的导入和配置开始:
import {Connection,Keypair,LAMPORTS_PER_SOL,PublicKey,SystemProgram,Transaction,sendAndConfirmTransaction
} from '@solana/web3.js';import {ExtensionType,TOKEN_2022_PROGRAM_ID,createInitializeMintInstruction,createInitializeScaledUiAmountConfigInstruction,getMintLen,getOrCreateAssociatedTokenAccount,mintTo,updateMultiplier,getScaledUiAmountConfig,unpackMint,createTransferInstruction
} from '@solana/spl-token';import * as fs from 'fs';
import * as path from 'path';const CONFIG = {DECIMAL_PLACES: 6,INITIAL_UI_AMOUNT_MULTIPLIER: 1.0,MODIFIED_UI_AMOUNT_MULTIPLIER: 2.0,TOKEN_NAME: "Scaled Demo Token",TOKEN_SYMBOL: "SDT",MINT_AMOUNT: 100,TRANSFER_AMOUNT: 10,CONNECTION_URL: 'http://127.0.0.1:8899',KEYPAIR_DIR: path.join(__dirname, 'keys')
};
这设置了我们的基本配置,包括:
- 代币参数(小数位数、乘数、名称、符号)
- 要 mint 和转移的金额
- 连接详细信息(我们将为此演示使用我们的 solana 本地测试验证器)
- 用于存储密钥对的目录
状态日志记录功能
接下来,让我们添加一个状态日志记录系统来跟踪我们整个演示中的更改:
interface StatusLog {step: string;timestamp: string;multiplier: number;rawBalance: string;uiBalance: string;description: string;
}const demoLogs: StatusLog[] = [];async function getTokenMultiplier(connection: Connection,mintPublicKey: PublicKey
): Promise<number> {const mintInfo = await connection.getAccountInfo(mintPublicKey);if (!mintInfo) {throw new Error(`Mint account not found: ${mintPublicKey.toString()}`);}const unpackedMint = unpackMint(mintPublicKey, mintInfo, TOKEN_2022_PROGRAM_ID);const extensionData = getScaledUiAmountConfig(unpackedMint);if (!extensionData) {return 1.0; // Default if no extension data} else {const currentTime = new Date().getTime();if (Number(extensionData.newMultiplierEffectiveTimestamp) < currentTime) {return extensionData.newMultiplier;} else {return extensionData.multiplier;}}
}async function getTokenBalance(connection: Connection,tokenAccount: PublicKey,
): Promise<{ rawAmount: string, uiAmount: string }> {try {const balanceDetail = await connection.getTokenAccountBalance(tokenAccount);return {rawAmount: balanceDetail.value.amount,uiAmount: balanceDetail.value.uiAmountString || '0'};} catch (error) {return {rawAmount: 'n/a',uiAmount: 'n/a'};}
}async function logStatus(connection: Connection,step: string,mintPublicKey: PublicKey,tokenAccount: PublicKey | null,description: string
): Promise<void> {const now = new Date();const timestamp = now.toLocaleTimeString();const multiplier = await getTokenMultiplier(connection, mintPublicKey);let rawBalance = 'n/a';let uiBalance = 'n/a';if (tokenAccount) {const balance = await getTokenBalance(connection, tokenAccount);rawBalance = balance.rawAmount;uiBalance = balance.uiAmount;}demoLogs.push({step,timestamp,multiplier,rawBalance,uiBalance,description});
}function printSummaryTable(): void {console.log("\n=== DEMONSTRATION SUMMARY ===");console.table(demoLogs.map(log => ({Step: log.step,Timestamp: log.timestamp,Multiplier: log.multiplier,"Raw Balance": log.rawBalance,"UI Balance": log.uiBalance})));
}
让我们分解一下这里的关键功能:
getTokenMultiplier
:获取给定 mint 的当前乘数。它利用 @solana/spl-token 中的unpackMint
和getScaledUiAmountConfig
辅助函数来获取 mint 的扩展数据(乘数和生效时间戳)。getTokenBalance
:获取给定代币帐户的原始余额和 UI 余额。它使用 Solana web3.js 库中的getTokenAccountBalance
方法。logStatus
:记录每个步骤的状态,包括当前乘数、原始余额和 UI 余额。它还将此信息存储在demoLogs
数组中以供稍后显示。printSummaryTable
:打印所有记录步骤的摘要表,显示乘数、原始余额和 UI 余额之间的关系。
实用功能
现在,让我们添加一些实用功能来处理事务确认和密钥对管理:
async function waitForTransaction(connection: Connection,signature: string,timeout = 30000,transactionNote: string
): Promise<string> {const startTime = Date.now();return new Promise((resolve, reject) => {(async () => {try {let done = false;while (!done && Date.now() - startTime < timeout) {const status = await connection.getSignatureStatus(signature);if (status?.value?.confirmationStatus === 'confirmed' ||status?.value?.confirmationStatus === 'finalized') {done = true;console.log(` ✅ Transaction ${transactionNote} confirmed: ${signature}`);resolve(signature);} else {await new Promise(resolve => setTimeout(resolve, 1000));}}if (!done) {reject(new Error(` ❌ Transaction confirmation timeout after ${timeout}ms`));}} catch (error) {reject(error);}})();});
}async function getOrCreateKeypair(keyPath: string, label: string): Promise<Keypair> {try {if (fs.existsSync(keyPath)) {const keyData = JSON.parse(fs.readFileSync(keyPath, 'utf-8'));const keypair = Keypair.fromSecretKey(new Uint8Array(keyData));return keypair;} else {const keypair = Keypair.generate();fs.writeFileSync(keyPath, JSON.stringify(Array.from(keypair.secretKey)));return keypair;}} catch (error) {const keypair = Keypair.generate();console.log(`Generated new ${label} keypair as fallback: ${keypair.publicKey.toString()}`);return keypair;}
}
这里我们创建了两个实用函数:
waitForTransaction
- 等待事务确认 (具有超时处理)getOrCreateKeypair
- 获取或创建密钥对,将其存储在文件中以便重复使用
核心功能
接下来,让我们为我们的演示添加核心功能。将 setup
函数添加到你的文件中以处理将 SOL 空投到付款人帐户:
async function setup(connection: Connection, payer: Keypair) {try {const airdropSignature = await connection.requestAirdrop(payer.publicKey,2 * LAMPORTS_PER_SOL);await waitForTransaction(connection, airdropSignature, 30000, "airdrop");} catch (error) {console.error('Error funding payer account:', error);console.log('If you are not using a local validator, you need to fund the payer account manually.');}
}
接下来,让我们创建 createScaledToken
函数以创建一个具有 Scaled UI Amount 扩展的新代币:
async function createScaledToken(connection: Connection, payer: Keypair, mint: Keypair, mintAuthority: Keypair) {try {// Calculate space needed for the mint account with Scaled UI Amount extensionconst extensions = [ExtensionType.ScaledUiAmountConfig];const mintLen = getMintLen(extensions);// Calculate lamports needed for rent-exemptionconst mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen);// Create a new token with Token-2022 program & Scaled UI Amount extensionconst transaction = new Transaction().add(// Create account for the mintSystemProgram.createAccount({fromPubkey: payer.publicKey,newAccountPubkey: mint.publicKey,space: mintLen,lamports: mintLamports,programId: TOKEN_2022_PROGRAM_ID,}),// Initialize Scaled UI Amount extensioncreateInitializeScaledUiAmountConfigInstruction(mint.publicKey,mintAuthority.publicKey,CONFIG.INITIAL_UI_AMOUNT_MULTIPLIER,TOKEN_2022_PROGRAM_ID),// Initialize the mintcreateInitializeMintInstruction(mint.publicKey,CONFIG.DECIMAL_PLACES,mintAuthority.publicKey,mintAuthority.publicKey,TOKEN_2022_PROGRAM_ID));const createMintSignature = await sendAndConfirmTransaction(connection,transaction,[payer, mint],{ commitment: 'confirmed' });console.log(` ✅ Token created! Transaction signature: ${createMintSignature}`);console.log(` Mint address: ${mint.publicKey.toString()}`);return;} catch (error) {console.error('Error creating token:', error);throw error;}
}
此函数创建并发送一个包含三个关键指令的事务:
createAccount
:为 mint 创建一个具有所需空间和 lamports 的新帐户,具体取决于扩展(在本例中,仅为 Scaled UI Amount 扩展)createInitializeScaledUiAmountConfigInstruction
:初始化 mint 的 Scaled UI Amount 扩展createInitializeMintInstruction
:使用指定的小数位数和授权机构初始化 mint
现在,让我们添加一个 updateScaledUiAmountMultiplier
函数来更新 UI 金额乘数:
async function updateScaledUiAmountMultiplier(connection: Connection,mint: Keypair,mintAuthority: Keypair,payer: Keypair,newMultiplier: number,startTimestamp: number = 0 // default, 0, is effective immediately
): Promise<string> {try {const signature = await updateMultiplier(connection,payer,mint.publicKey,mintAuthority,newMultiplier,BigInt(startTimestamp),[payer, mintAuthority],undefined,TOKEN_2022_PROGRAM_ID);await waitForTransaction(connection, signature, 30000, "multiplier update");return signature;} catch (error) {console.error(' Error updating UI amount multiplier:', error);throw error;}
}
在这里,我们只是使用 @solana/spl-token
库中的 updateMultiplier
函数来更新 mint 的乘数(请注意,我们将 0
作为新乘数的开始时间戳传递,这意味着它将立即生效),然后等待事务确认后再继续。
接下来,让我们添加一个可重用的 transferTokens
函数来处理帐户之间的代币转账。我们将使用它来演示在更新乘数之前和之后转移代币:
async function transferTokens(connection: Connection,payer: Keypair,source: PublicKey,sourceOwner: Keypair,mint: PublicKey
): Promise<string> {try {const amount = CONFIG.TRANSFER_AMOUNT * (10 ** CONFIG.DECIMAL_PLACES);const destinationOwner = Keypair.generate();const destinationAccount = await getOrCreateAssociatedTokenAccount(connection,payer,mint,destinationOwner.publicKey,false,'confirmed',{},TOKEN_2022_PROGRAM_ID);const tx = new Transaction().add(createTransferInstruction(source,destinationAccount.address,sourceOwner.publicKey,amount,[sourceOwner],TOKEN_2022_PROGRAM_ID));const transferSignature = await sendAndConfirmTransaction(connection,tx,[payer, sourceOwner],{ commitment: 'confirmed' });console.log(` ✅ Tokens transferred! Transaction signature: ${transferSignature}`);return transferSignature;} catch (error) {console.error(' ❌ Error transferring tokens');throw error;}
}
此函数处理:
- 为转移设置新的目标钱包和代币帐户(这只是一个用于演示的新抛弃帐户)
- 使用
createTransferInstruction
函数创建转移事务 - 发送事务并等待确认
主要演示功能
现在,让我们逐步构建主要的 demonstrateScaledToken
函数。首先,添加一个占位符函数,其中包含每个步骤的 TODO:
async function demonstrateScaledToken(): Promise<void> {try {console.log(`=== SCALED TOKEN DEMONSTRATION ===`);console.log(`\n=== Setup ===`);// TODO Add setupconsole.log(`\n=== Step 1: Creating Token Mint ===`);// TODO Create Token Mint with UI Amount Scaled extensionconsole.log(`\n=== Step 2: Creating Holder's Token Account ===`);// TODO Create Holder's Token Accountconsole.log(`\n=== Step 3: Minting Initial Tokens ===`);// TODO Mint Initial Tokens to Holderconsole.log(`\n=== Step 4: Transferring Tokens ===`);// TODO Transfer Tokens to another accountconsole.log(`\n=== Step 5: Updating Scale Multiplier ===`);// TODO Update Scale Multiplierconsole.log(`\n=== Step 6: Minting Additional Tokens ===`);// TODO Mint Additional Tokens to Holderconsole.log(`\n=== Step 7: Transferring Additional Tokens ===`);// TODO Transfer Tokens to another account} catch (error) {console.error('Error in scaled token demonstration:', error);}
}
让我们填写每个部分:
设置
将 // TODO Add setup
替换为:
const connection = new Connection(CONFIG.CONNECTION_URL, 'confirmed');const payer = await getOrCreateKeypair(path.join(CONFIG.KEYPAIR_DIR, 'payer.json'), 'payer');const mintAuthority = await getOrCreateKeypair(path.join(CONFIG.KEYPAIR_DIR, 'mint-authority.json'), 'mint authority');const mint = await getOrCreateKeypair(path.join(CONFIG.KEYPAIR_DIR, 'mint.json'), 'mint');const holder = await getOrCreateKeypair(path.join(CONFIG.KEYPAIR_DIR, 'holder.json'), 'token holder');await setup(connection, payer);
在此步骤中,我们:
- 创建与 Solana 网络的连接
- 加载或创建我们将需要的所有帐户的密钥对
- 运行我们的
setup
函数来空投资金给付款人帐户
步骤 1:创建代币 Mint
将 // TODO Create Token Mint with UI Amount Scaled extension
替换为:
await createScaledToken(connection, payer, mint, mintAuthority);await logStatus(connection,"1. After Token Initialized",mint.publicKey,null,"Token created with Scaled UI Amount extension");
此部分:
- 创建一个新的具有 Scaled UI Amount 扩展的 Token-2022 代币
- 记录初始状态(请注意,尚无代币帐户)
步骤 2:创建持有者的代币帐户
将 // TODO Create Holder's Token Account
替换为:
const holderTokenAccount = await getOrCreateAssociatedTokenAccount(connection,payer,mint.publicKey,holder.publicKey,false,'confirmed',{},TOKEN_2022_PROGRAM_ID);console.log(` ✅ Holder's token account created: ${holderTokenAccount.address.toString()}`);await logStatus(connection,"2. After ATA Created",mint.publicKey,holderTokenAccount.address,"Holder's token account created");
在这里,我们使用 getOrCreateAssociatedTokenAccount
函数为持有者创建一个关联代币帐户。 这将允许我们将代币直接铸造到持有者的帐户。 请注意,我们正在使用 Token-2022 程序 ID。
步骤 3:Mint 初始代币
将 // TODO Mint Initial Tokens to Holder
替换为:
const initialMintAmount = CONFIG.MINT_AMOUNT * (10 ** CONFIG.DECIMAL_PLACES);const mintToSignature = await mintTo(connection,payer,mint.publicKey,holderTokenAccount.address,mintAuthority,initialMintAmount,[],{},TOKEN_2022_PROGRAM_ID);await waitForTransaction(connection, mintToSignature, 30000, "initial mint");await logStatus(connection,"3. After Mint #1",mint.publicKey,holderTokenAccount.address,`Minted ${CONFIG.MINT_AMOUNT} tokens with initial multiplier`);
在此步骤中,我们:
- 计算要铸造的原始金额(包括小数位数)
- 将代币铸造到持有者的帐户
- 等待事务确认
- 之后记录状态
步骤 4:转移代币
将 // TODO Transfer Tokens to another account
替换为:
await transferTokens(connection,payer,holderTokenAccount.address,holder,mint.publicKey);await logStatus(connection,"4. After Transfer #1",mint.publicKey,holderTokenAccount.address,`Transferred ${CONFIG.TRANSFER_AMOUNT} tokens to another account`);
此部分:
- 将代币从持有者转移到新帐户(回想一下,我们的转移指令使用 CONFIG.TRANSFER_AMOUNT 常量来确定要转移多少代币 - 我们将对两次转移使用相同的原始金额,并比较 UI 金额如何变化)
- 转移后记录状态
步骤 5:更新小数位数乘数
将 // TODO Update Scale Multiplier
替换为:
await updateScaledUiAmountMultiplier(connection,mint,mintAuthority,payer,CONFIG.MODIFIED_UI_AMOUNT_MULTIPLIER);await logStatus(connection,"5. After Multiplier Update",mint.publicKey,holderTokenAccount.address,`Updated multiplier to ${CONFIG.MODIFIED_UI_AMOUNT_MULTIPLIER}x`);
在这里,我们:
- 使用
updateScaledUiAmountMultiplier
函数更新 UI 金额乘数 - 记录状态以查看 UI 金额如何变化
步骤 6:Mint 额外的代币
将 // TODO Mint Additional Tokens to Holder
替换为:
const additionalMintSignature = await mintTo(connection,payer,mint.publicKey,holderTokenAccount.address,mintAuthority,initialMintAmount, // Same raw amount as before[],{},TOKEN_2022_PROGRAM_ID```markdown
import * as fs from 'fs';
import * as path from 'path';const CONFIG = {DECIMAL_PLACES: 6,INITIAL_UI_AMOUNT_MULTIPLIER: 1.0,MODIFIED_UI_AMOUNT_MULTIPLIER: 2.0,TOKEN_NAME: "Scaled Demo Token",TOKEN_SYMBOL: "SDT",MINT_AMOUNT: 100,TRANSFER_AMOUNT: 10,HTTP_CONNECTION_URL: 'http://127.0.0.1:8899',WSS_CONNECTION_URL: 'ws://127.0.0.1:8900',KEYPAIR_DIR: path.join(__dirname, 'keys')
};
const LAMPORTS_PER_SOL = BigInt(1_000_000_000);interface Client {rpc: Rpc<SolanaRpcApi>;rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
}
这设置了我们的基本配置,包括:
- Token 参数 (decimals, multiplier, name, symbol)
- 要 mint 和 transfer 的数量
- 连接详细信息(我们将在此演示中使用我们的 solana 本地测试验证器)
- 用于存储密钥对的目录
- 用于处理 RPC 和订阅的 Client 接口
Status Logging Functions
接下来,我们添加一个状态日志记录系统,以跟踪整个演示过程中的更改:
interface StatusLog {step: string;timestamp: string;multiplier: number;rawBalance: string;uiBalance: string;description: string;
}const demoLogs: StatusLog[] = [];async function getTokenMultiplier(client: Client,mintAddress: Address
): Promise<number> {try {const mint = await fetchMint(client.rpc, mintAddress);if (!mint.data.extensions || mint.data.extensions.__option === 'None') {return 1.0; // Default if no extensions}const extensionArray = mint.data.extensions.__option === 'Some' ? mint.data.extensions.value : [];const extensionData = extensionArray.find((ext: Extension) => ext.__kind === 'ScaledUiAmountConfig');if (!extensionData) {return 1.0; // Default if no extension data} else {const currentTime = new Date().getTime();if (Number(extensionData.newMultiplierEffectiveTimestamp) < currentTime) {return extensionData.newMultiplier;} else {return extensionData.multiplier;}}} catch (error) {console.error('Error getting token multiplier:', error);return 1.0; // Default on error}
}async function logStatus(client: Client,step: string,mintAddress: Address,tokenAccount: Address | null,description: string
): Promise<void> {const now = new Date();const timestamp = now.toLocaleTimeString();const multiplier = await getTokenMultiplier(client, mintAddress);let rawBalance = 'n/a';let uiBalance = 'n/a';if (tokenAccount) {const balance = await client.rpc.getTokenAccountBalance(tokenAccount).send();rawBalance = balance.value.amount;uiBalance = balance.value.uiAmountString;}demoLogs.push({step,timestamp,multiplier,rawBalance,uiBalance,description});
}function printSummaryTable(): void {console.log("\n=== DEMONSTRATION SUMMARY ===");console.table(demoLogs.map(log => ({Step: log.step,Timestamp: log.timestamp,Multiplier: log.multiplier,"Raw Balance": log.rawBalance,"UI Balance": log.uiBalance})));
}
让我们分解一下这里的关键函数:
getTokenMultiplier
: 获取给定 mint 的当前 multiplier。它利用来自 @solana-program/token-2022 的fetchMint
来获取和解析 mint 的扩展数据(multiplier 和 effective timestamp)。- 我们使用
getTokenAccountBalance
方法来获取给定 token account 的 raw 和 UI 余额。 logStatus
: 记录每个步骤的状态,包括当前 multiplier、raw 余额和 UI 余额。它还将此信息存储在demoLogs
数组中,以供稍后显示。printSummaryTable
: 打印所有记录步骤的摘要表,显示 multiplier、raw 余额和 UI 余额之间的关系。
Utility Functions
现在,让我们添加一些 utility function 来处理交易确认和密钥对管理:
async function getOrCreateKeypairSigner(keyPath: string, label: string): Promise<KeyPairSigner<string>> {try {if (!fs.existsSync(keyPath)) {throw new Error(`Keypair file not found: ${keyPath}`);}const keyData = JSON.parse(fs.readFileSync(keyPath, 'utf-8'));const keypair = await createKeyPairSignerFromBytes(new Uint8Array(keyData));return keypair;} catch (error) {const keypair = await generateKeyPairSigner();console.log(`Generated new ${label} keypair as fallback: ${keypair.address}`);return keypair;}
}export const createDefaultTransaction = async (client: Client,feePayer: TransactionSigner
) => {const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();return pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx));
};
export const signAndSendTransaction = async (client: Client,transactionMessage: CompilableTransactionMessage &TransactionMessageWithBlockhashLifetime,commitment: Commitment = 'confirmed'
) => {const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);const signature = getSignatureFromTransaction(signedTransaction);await sendAndConfirmTransactionFactory(client)(signedTransaction, {commitment,});return signature;
};
export const sendAndConfirmInstructions = async (client: Client,payer: TransactionSigner,instructions: IInstruction[]
) => {const signature = await pipe(await createDefaultTransaction(client, payer),(tx) => appendTransactionMessageInstructions(instructions, tx),(tx) => signAndSendTransaction(client, tx));return signature;
};
这里我们创建了一些 utility function:
getOrCreateKeypairSigner
- 获取或创建密钥对,将其存储在文件中以便重复使用createDefaultTransaction
- 创建具有最新 blockhash 和 fee payer 的默认交易signAndSendTransaction
- 签名并发送交易,等待确认sendAndConfirmInstructions
- 发送并确认一组指令,返回交易签名
Core Functionality
接下来,让我们添加用于演示的核心函数。将 setup
函数添加到你的文件中,以处理将 SOL 空投到 payer account:
async function setup(client: Client, payer: KeyPairSigner<string>) {try {const airdrop = airdropFactory({ rpc: client.rpc, rpcSubscriptions: client.rpcSubscriptions });const airdropTx: Signature = await airdrop({commitment: 'processed',lamports: lamports(LAMPORTS_PER_SOL),recipientAddress: payer.address});console.log(` ✅ Transaction airdrop confirmed: ${airdropTx}`);} catch (error) {console.error(' ❌ Error funding payer account');}
}
接下来,让我们创建 createScaledToken
函数来创建一个新的 token,其中包含 Scaled UI Amount 扩展:
const getCreateMintInstructions = async (input: {authority: Address;client: Client;decimals?: number;extensions?: ExtensionArgs[];freezeAuthority?: Address;mint: TransactionSigner;payer: TransactionSigner;programAddress?: Address;
}) => {const space = getMintSize(input.extensions);const postInitializeExtensions: Extension['__kind'][] = [\'TokenMetadata',\'TokenGroup',\'TokenGroupMember',\];const spaceWithoutPostInitializeExtensions = input.extensions? getMintSize(input.extensions.filter((e) => !postInitializeExtensions.includes(e.__kind))): space;const rent = await input.client.rpc.getMinimumBalanceForRentExemption(BigInt(space)).send();return [\getCreateAccountInstruction({\payer: input.payer,\newAccount: input.mint,\lamports: rent,\space: spaceWithoutPostInitializeExtensions,\programAddress: input.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS,\}),\getInitializeMintInstruction({\mint: input.mint.address,\decimals: input.decimals ?? 0,\freezeAuthority: input.freezeAuthority,\mintAuthority: input.authority,\}),\];
};const createScaledToken = async (input: Omit<Parameters<typeof getCreateMintInstructions>[0],'authority' | 'mint'> & {authority: TransactionSigner;mint?: TransactionSigner;}
): Promise<Address> => {const mint = input.mint ?? (await generateKeyPairSigner());const [createAccount, initMint] = await getCreateMintInstructions({...input,authority: input.authority.address,mint,});const createMintSignature = await sendAndConfirmInstructions(input.client, input.payer, [\createAccount,\...getPreInitializeInstructionsForMintExtensions(\mint.address,\input.extensions ?? []\),\initMint,\...getPostInitializeInstructionsForMintExtensions(\mint.address,\input.authority,\input.extensions ?? []\),\]);console.log(` ✅ Token created! Transaction signature: ${createMintSignature}`);console.log(` Mint address: ${mint.address}`);return mint.address;
};
此函数使用三个关键指令创建并发送交易:
createAccount
: 基于扩展(在本例中,只是 Scaled UI Amount 扩展)使用所需的 space 和 lamports 为 mint 创建一个新 accountgetPreInitializeInstructionsForMintExtensions
: 为 mint 初始化 Scaled UI Amount 扩展getPostInitializeInstructionsForMintExtensions
: 使用指定的 decimals 和 authority 初始化 mint
接下来,让我们添加一些有助于基本 SPL token 操作的函数。我们将使用它们来 mint token 并在 account 之间 transfer token。将以下函数添加到你的文件中:
async function createAta(client: Client, payer: TransactionSigner, mint: TransactionSigner, owner: TransactionSigner): Promise<Address> {const createAta = await getCreateAssociatedTokenIdempotentInstructionAsync({payer,mint: mint.address,owner: owner.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});await sendAndConfirmInstructions(client, payer, [createAta]);const [ata] = await findAssociatedTokenPda({mint: mint.address,owner: owner.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,});console.log(` ✅ Associated token account created: ${ata}`);return ata;
}async function transferTokens(client: Client, payer: TransactionSigner, source: Address, sourceOwner: TransactionSigner, mint: TransactionSigner, amount: bigint) {try {const destination = await generateKeyPairSigner();const destinationTokenAccount = await createAta(client, payer, mint, destination);const transferInstruction = getTransferInstruction({source: source,destination: destinationTokenAccount,authority: sourceOwner,amount: amount,}, {programAddress: TOKEN_2022_PROGRAM_ADDRESS});const txid = await sendAndConfirmInstructions(client, payer, [transferInstruction]);console.log(` ✅ Transfer transaction confirmed: ${txid}`);return txid;} catch (error) {console.error(' ❌ Error transferring tokens');throw error;}
}async function mintTokens(client: Client, payer: TransactionSigner, mintAuthority: TransactionSigner,mint: TransactionSigner, tokenAccount: Address, amount: bigint) {try {const mintToInstruction = getMintToInstruction({mint: mint.address,token: tokenAccount,amount,mintAuthority}, {programAddress: TOKEN_2022_PROGRAM_ADDRESS});const txid = await sendAndConfirmInstructions(client, payer, [mintToInstruction]);console.log(` ✅ Mint transaction confirmed: ${txid}`);return txid;} catch (error) {console.error(' ❌ Error minting tokens');throw error;}
}
这些函数处理:
createAta
: 为给定的 mint 和 owner 创建一个关联的 Token Account(我们需要这个来 mint 和 transfer token)transferTokens
: 将 token 从一个 account transfer 到另一个 accountmintTokens
: 将 token mint 到给定的 token account
现在,让我们添加一个 updateMultiplier
函数来更新 UI amount multiplier:
async function updateMultiplier(client: Client, payer: TransactionSigner, mint: TransactionSigner, mintAuthority: TransactionSigner, newMultiplier: number) {try {const updateMultiplierInstruction = getUpdateMultiplierScaledUiMintInstruction({mint: mint.address,authority: mintAuthority,effectiveTimestamp: BigInt(0),multiplier: newMultiplier,}, {programAddress: TOKEN_2022_PROGRAM_ADDRESS});const txid = await sendAndConfirmInstructions(client, payer, [updateMultiplierInstruction]);console.log(` ✅ Update multiplier transaction confirmed: ${txid}`);return txid;} catch (error) {console.error(' ❌ Error updating multiplier');throw error;}
}
在这里,我们只是使用来自 @solana-program/token-2022 库的 getUpdateMultiplierScaledUiMintInstruction
函数来更新 mint 的 multiplier(请注意,我们正在传递 0
作为新 multiplier 的开始 timestamp,这意味着它将立即生效),并等到交易确认后再继续。
Main Demonstration Function
现在,让我们逐步构建主要的 demonstrateScaledToken
函数。首先,添加一个占位符函数,其中包含每个步骤的 TODO:
async function demonstrateScaledToken(): Promise<void> {try {console.log(`=== SCALED TOKEN DEMONSTRATION ===`);console.log(`\n=== Setup ===`);// TODO Add setupconsole.log(`\n=== Step 1: Creating Token Mint ===`);// TODO Create Token Mint with UI Amount Scaled extensionconsole.log(`\n=== Step 2: Creating Holder's Token Account ===`);// TODO Create Holder's Token Accountconsole.log(`\n=== Step 3: Minting Initial Tokens ===`);// TODO Mint Initial Tokens to Holderconsole.log(`\n=== Step 4: Transferring Tokens ===`);// TODO Transfer Tokens to another accountconsole.log(`\n=== Step 5: Updating Scale Multiplier ===`);// TODO Update Scale Multiplierconsole.log(`\n=== Step 6: Minting Additional Tokens ===`);// TODO Mint Additional Tokens to Holderconsole.log(`\n=== Step 7: Transferring Additional Tokens ===`);// TODO Transfer Tokens to another account} catch (error) {console.error('Error in scaled token demonstration:', error);}
}
让我们填写每个部分:
Setup
将 // TODO Add setup
替换为:
const client: Client = {rpc: createSolanaRpc(CONFIG.HTTP_CONNECTION_URL),rpcSubscriptions: createSolanaRpcSubscriptions(CONFIG.WSS_CONNECTION_URL)};const payer = await getOrCreateKeypairSigner(path.join(CONFIG.KEYPAIR_DIR, 'payer.json'), 'payer');const mintAuthority = await getOrCreateKeypairSigner(path.join(CONFIG.KEYPAIR_DIR, 'mint-authority.json'), 'mint authority');const mint = await getOrCreateKeypairSigner(path.join(CONFIG.KEYPAIR_DIR, 'mint.json'), 'mint');const holder = await getOrCreateKeypairSigner(path.join(CONFIG.KEYPAIR_DIR, 'holder.json'), 'token holder');await setup(client, payer);
在此步骤中,我们:
- 设置我们的 client 以连接到 Solana 网络
- 加载或创建我们将需要的所有 account 的密钥对
- 运行我们的
setup
函数将资金空投到 payer account
Step 1: Creating Token Mint
将 // TODO Create Token Mint with UI Amount Scaled extension
替换为:
const mintAddress = await createScaledToken({authority: mintAuthority,client,extensions: [\extension('ScaledUiAmountConfig', {\authority: mintAuthority.address,\multiplier: CONFIG.INITIAL_UI_AMOUNT_MULTIPLIER,\newMultiplierEffectiveTimestamp: BigInt(0),\newMultiplier: CONFIG.INITIAL_UI_AMOUNT_MULTIPLIER,\}),\],payer: payer,mint});await logStatus(client,"1. Token Created",mintAddress,null,"Token created with Scaled UI Amount extension");
本节:
- 使用 Scaled UI Amount 扩展创建新的 Token-2022 token
- 记录初始状态(请注意,还没有 token account)
Step 2: Creating Holder's Token Account
将 // TODO Create Holder's Token Account
替换为:
const holderTokenAccount = await createAta(client, payer, mint, holder);await logStatus(client,"2. Ata Created",mint.address,holderTokenAccount,"Holder's token account created");
在这里,我们使用我们的 createAta
函数为 holder 创建一个关联的 Token Account。这将允许我们将 token 直接 mint 到 holder 的 account。
Step 3: Minting Initial Tokens
将 // TODO Mint Initial Tokens to Holder
替换为:
await mintTokens(client, payer, mintAuthority, mint, holderTokenAccount, BigInt(CONFIG.MINT_AMOUNT));await logStatus(client,"3. After Mint #1",mint.address,holderTokenAccount,"Initial tokens minted");
在此步骤中,我们使用 mintTokens
函数将 token mint 到 holder 的 account。我们还在之后记录状态。
Step 4: Transferring Tokens
将 // TODO Transfer Tokens to another account
替换为:
await transferTokens(client, payer, holderTokenAccount, holder, mint, BigInt(CONFIG.TRANSFER_AMOUNT));await logStatus(client,"4. After Transfer",mint.address,holderTokenAccount,"Tokens transferred");
此步骤:
- 将 token 从 holder transfer 到新 account(我们正在使用 CONFIG.TRANSFER\AMOUNT 常量来确定要 transfer 多少 token - 我们将对两个 transfer 使用相同的 raw amount,并比较 UI amount 的差异)
- 在 transfer 后记录状态
Step 5: Updating Scale Multiplier
将 // TODO Update Scale Multiplier
替换为:
await updateMultiplier(client, payer, mint, mintAuthority, CONFIG.MODIFIED_UI_AMOUNT_MULTIPLIER);await logStatus(client,"5. After Update Multiplier",mint.address,holderTokenAccount,"Multiplier updated");
在这里,我们:
- 使用
updateMultiplier
函数更新 UI amount multiplier - 记录状态以查看 UI amount 如何变化
Step 6: Minting Additional Tokens
将 // TODO Mint Additional Tokens to Holder
替换为:
await mintTokens(client, payer, mintAuthority, mint, holderTokenAccount, BigInt(CONFIG.MINT_AMOUNT));await logStatus(client,"6. After Mint #2",mint.address,holderTokenAccount,"Additional tokens minted");
本节:
- Mint 与之前相同的 raw amount
- 记录状态以查看 UI amount 在新 multiplier 下有何不同
Step 7: Transferring Additional Tokens
将 // TODO Transfer Tokens to another account
替换为:
await transferTokens(client, payer, holderTokenAccount, holder, mint, BigInt(CONFIG.TRANSFER_AMOUNT));await logStatus(client,"7. After Transfer #2",mint.address,holderTokenAccount,"Additional tokens transferred");printSummaryTable();
最后,我们:
- 再次使用相同的 raw amount transfer token
- 记录状态,以便我们可以看到 UI amount 与之前的 transfer 有何不同
- 打印所有步骤的摘要表
Adding the Main Function Call
在文件末尾添加:
if (require.main === module) {console.log('Starting the Token-2022 Scaled UI Amount demonstration...');demonstrateScaledToken().then(() => console.log(`=== DEMONSTRATION COMPLETED ===`)).catch(error => console.error('Demonstration failed with error:', error));
}
Running the Demonstration
要运行演示:
- 打开一个新终端并启动一个本地测试验证器:
solana-test-validator -r
- 然后在你的主项目终端中,运行脚本:
npm start
Understanding the Output
这是你应该在输出中看到的:
=== DEMONSTRATION SUMMARY ===
┌─────────┬───────────────────────────┬──────────────┬────────────┬─────────────┬────────────┐
│ (index) │ Step │ Timestamp │ Multiplier │ Raw Balance │ UI Balance │
├─────────┼───────────────────────────┼──────────────┼────────────┼─────────────┼────────────┤
│ 0 │ 'Initial Setup' │ '3:02:16 PM' │ 1 │ 'n/a' │ 'n/a' │
│ 1 │ 'After Initial Mint' │ '3:02:17 PM' │ 1 │ '100000000' │ '100' │
│ 2 │ 'After Transfer #1' │ '3:02:18 PM' │ 1 │ '90000000' │ '90' │
│ 3 │ 'After Multiplier Update' │ '3:02:19 PM' │ 2 │ '90000000' │ '180' │
│ 4 │ 'After Second Mint' │ '3:02:20 PM' │ 2 │ '190000000' │ '380' │
│ 5 │ 'After Transfer #2' │ '3:02:21 PM' │ 2 │ '180000000' │ '360' │
└─────────┴───────────────────────────┴──────────────┴────────────┴─────────────┴────────────┘
=== DEMONSTRATION COMPLETED ===
让我们仔细看看演示中的关键点:
- After Initial Mint,UI amount = Raw amount / 10^decimals,因为我们的 multiplier 是 1.0
- After Transfer #1,raw 和 UI amount 成比例地减少
- After Multiplier Update - Raw amount 未更改,UI amount 翻倍(例如,类似于 2:1 的股票分割)
- After Second Mint - Raw amount 像以前一样增加,但 UI amount 乘以新 multiplier 增加(+ 100 * 2.0)
- After Transfer #2,raw amount 减少的量与以前相同,但 UI amount 现在减少的量乘以新 multiplier (-10 * 2.0)
Time to Scale!
恭喜!你已成功在 Solana Token-2022 程序中实现了 Scaled UI Amount 扩展。你现在有一个工作演示,展示了如何创建 token、mint 和 transfer token,以及更新 UI amount multiplier。
Scaled UI Amount 扩展为 token 发行方提供了一种强大的机制来控制余额在用户面前的显示方式,而无需修改底层 raw amount。这为在 Solana 上创建创新 token 经济打开了新的可能性。
主要收获:
- Token-2022 的 Scaled UI Amount 扩展支持 token 经济的高级应用,如股票分割、股息、收益和 rebasing
- 使用 multiplier 时,raw amount 保持不变,而 UI amount 随 multiplier 缩放
- 应用程序应适当处理 raw 和 UI amount 之间的转换
- 历史数据提供商应考虑索引 multiplier 更改以提供准确的历史数据
本文到此结束,更多相关文章,请, https://t.me/gtokentool