本文介绍如何在Solana链上使用Token-2022程序的Scaled UI Amount扩展。如何扩展Token-2022代币,并通过mint,转移及更新UI 乘数等操作,解释了raw amount与UI amount之间的关系,以及multiplier的实际应用场景,比如股票分割和分红等。
本指南提供 Solana Web3.js (Legacy v 1.x) 和 Solana Kit (v 2.x) 版本。选择适当的选项卡以查看你首选库的代码片段和说明:
Solana Token-2022 程序 引入了强大的扩展,增强了代币功能,使其超越了原始的 SPL Token 程序。其中一个扩展是 Scaled UI Amount(缩放 UI 金额),它允许代币发行者定义一个乘数,该乘数会影响向用户显示的代币余额,而不会更改链上存储的底层原始金额。此扩展启用了强大的用例,包括:
为了使用 Scaled UI Amount 扩展进行构建,开发人员需要了解它的工作原理以及如何在他们的应用程序中实现它。本指南将引导你完成创建具有 Scaled UI Amount 扩展的代币、铸造代币、转移代币以及更新 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'      │
└─────────┴───────────────────────────┴──────────────┴────────────┴─────────────┴────────────┘
让我们开始吧!
在开始本教程之前,请确保你已具备:
agave-install init 2.2.14 或任何最新版本来更新它)在深入实施之前,让我们了解什么是 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 扩展引入了两个新指令:
Scaled UI Amount 扩展引入了几个重要的关键概念:
我们的演示将展示这些概念如何在实践中发挥作用。
以下是 Scaled UI Amount 扩展的一些实际应用:
| 用例 | 描述 | 实施方法 | 
|---|---|---|
| 股票拆分和反向股票拆分 | 将现有股份分成多个或部分股份 | 调整乘数以反映新的股份数量 | 
| 计息代币 | 随着时间的推移在视觉上累积利息的代币 | 根据收益率逐步增加乘数 | 
| 股息分配 | 向代币持有者分配股息 | 调整乘数以反映股息分配 | 
| 面额变更 | 在同一资产的不同单位之间转换 | 更改乘数以表示新的面额 | 
| 重新定价代币 | 根据外部因素(如算法稳定币)增加/减少总供应量的代币 | 定期调整乘数以反映供应变化 | 
实施 Scaled UI Amount 扩展需要仔细考虑应用程序的架构和功能。虽然你的影响可能是独一无二的,但以下是一些常见的考虑因素:
amountToUiAmount,uiAmountToAmount))。 这需要仔细的 UX 设计,以防止用户混淆,尤其是在乘数更改期间。 考虑在代币使用此扩展时添加工具提示或指示器。更喜欢直接跳到代码?查看我们在 GitHub 上的示例存储库,获取本指南的完整代码!
在我们开始之前,让我们回顾一下我们将要构建的内容。我们将创建一个简单的演示脚本,该脚本:
让我们从创建我们的项目结构开始:
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 目录中创建四个密钥对文件。
在我们开始之前,让我们回顾一下我们将要构建的内容。我们将创建一个简单的演示脚本,该脚本:
让我们从创建我们的项目结构开始:
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 目录中创建四个密钥对文件。
让我们创建我们的 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')
};
这设置了我们的基本配置,包括:
接下来,让我们添加一个状态日志记录系统来跟踪我们整个演示中的更改:
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 extension
    const extensions = [ExtensionType.ScaledUiAmountConfig];
    const mintLen = getMintLen(extensions);
    // Calculate lamports needed for rent-exemption
    const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen);
    // Create a new token with Token-2022 program & Scaled UI Amount extension
    const transaction = new Transaction().add(
      // Create account for the mint
      SystemProgram.createAccount({
        fromPubkey: payer.publicKey,
        newAccountPubkey: mint.publicKey,
        space: mintLen,
        lamports: mintLamports,
        programId: TOKEN_2022_PROGRAM_ID,
      }),
      // Initialize Scaled UI Amount extension
      createInitializeScaledUiAmountConfigInstruction(
        mint.publicKey,
        mintAuthority.publicKey,
        CONFIG.INITIAL_UI_AMOUNT_MULTIPLIER,
        TOKEN_2022_PROGRAM_ID
      ),
      // Initialize the mint
      createInitializeMintInstruction(
        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有关这些步骤的更多信息,请参见Solana 程序文档
现在,让我们添加一个 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 setup
    console.log(`\n=== Step 1: Creating Token Mint ===`);
    // TODO Create Token Mint with UI Amount Scaled extension
    console.log(`\n=== Step 2: Creating Holder's Token Account ===`);
    // TODO Create Holder's Token Account
    console.log(`\n=== Step 3: Minting Initial Tokens ===`);
    // TODO Mint Initial Tokens to Holder
    console.log(`\n=== Step 4: Transferring Tokens ===`);
    // TODO Transfer Tokens to another account
    console.log(`\n=== Step 5: Updating Scale Multiplier ===`);
    // TODO Update Scale Multiplier
    console.log(`\n=== Step 6: Minting Additional Tokens ===`);
    // TODO Mint Additional Tokens to Holder
    console.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);
在此步骤中,我们:
setup 函数来空投资金给付款人帐户将 // 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"
    );
此部分:
将 // 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。
将 // 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`
    );
在此步骤中,我们:
将 // 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`
    );
此部分:
将 // 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 金额乘数将 // 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>;
}
这设置了我们的基本配置,包括:
接下来,我们添加一个状态日志记录系统,以跟踪整个演示过程中的更改:
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 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 - 发送并确认一组指令,返回交易签名接下来,让我们添加用于演示的核心函数。将 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有关这些步骤的更多信息,请参见 Solana 程序文档
接下来,让我们添加一些有助于基本 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,这意味着它将立即生效),并等到交易确认后再继续。
现在,让我们逐步构建主要的 demonstrateScaledToken 函数。首先,添加一个占位符函数,其中包含每个步骤的 TODO:
async function demonstrateScaledToken(): Promise<void> {
  try {
    console.log(`=== SCALED TOKEN DEMONSTRATION ===`);
    console.log(`\n=== Setup ===`);
    // TODO Add setup
    console.log(`\n=== Step 1: Creating Token Mint ===`);
    // TODO Create Token Mint with UI Amount Scaled extension
    console.log(`\n=== Step 2: Creating Holder's Token Account ===`);
    // TODO Create Holder's Token Account
    console.log(`\n=== Step 3: Minting Initial Tokens ===`);
    // TODO Mint Initial Tokens to Holder
    console.log(`\n=== Step 4: Transferring Tokens ===`);
    // TODO Transfer Tokens to another account
    console.log(`\n=== Step 5: Updating Scale Multiplier ===`);
    // TODO Update Scale Multiplier
    console.log(`\n=== Step 6: Minting Additional Tokens ===`);
    // TODO Mint Additional Tokens to Holder
    console.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 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);
在此步骤中,我们:
setup 函数将资金空投到 payer account将 // 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"
        );
本节:
将 // 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。
将 // 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。我们还在之后记录状态。
将 // 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"
        );
此步骤:
将 // 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将 // 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"
        );
本节:
将 // 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();
最后,我们:
在文件末尾添加:
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));
}
要运行演示:
solana-test-validator -r
npm start
这是你应该在输出中看到的:
=== 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 ===
让我们仔细看看演示中的关键点:
恭喜!你已成功在 Solana Token-2022 程序中实现了 Scaled UI Amount 扩展。你现在有一个工作演示,展示了如何创建 token、mint 和 transfer token,以及更新 UI amount multiplier。
Scaled UI Amount 扩展为 token 发行方提供了一种强大的机制来控制余额在用户面前的显示方式,而无需修改底层 raw amount。这为在 Solana 上创建创新 token 经济打开了新的可能性。
主要收获:
如有问题和支持,请加入 QuickNode Discord 或在 Twitter 上关注我们。
如果你有任何反馈或新主题的要求,请告诉我们。我们很乐意听到你的来信。
>- 原文链接: [quicknode.com/guides/sol...](https://www.quicknode.com/guides/solana-development/spl-tokens/token-2022/scaled-ui-amount)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~ 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!