在 Solana 上支持 Scaled UI Amount 扩展
背景
Scaled UI Amount 扩展允许代币发行方指定一个乘数,用于计算用户代币余额的 UI 显示数量。这使得发行方可以创建弹性代币(rebasing tokens),并实现如股票拆分等功能。该扩展与利息型代币扩展类似,提供的是纯粹用于界面显示的数量,这意味着团队需要做一些额外的工作来优化用户体验。底层的计算和转账操作都仍然使用程序中的原始数量进行。
资源:
TL;DR
- 最终用户在查看代币价格、余额和数量时,应尽量与 UIAmount(原始数量 * 乘数)交互
- dApp 和服务提供商在所有计算中应使用原始数量和未缩放价格,并在用户端进行转换
- 历史价格数据需要同时提供缩放和未缩放的数值,便于集成
- 历史乘数数值需要可访问,以保证历史数据的准确性
术语定义
- 乘数:用于 UI Amount 计算的可更新静态乘数
- UIAmount:乘数 * 原始数量(又称:缩放数量)
- 原始数量:amount(又称:未缩放数量)
当前余额
用于显示的当前数量
- 当你向最终用户展示使用 Scaled UI Amount 扩展的代币数量时,建议使用:
- UIAmount/UIAmountString(推荐)
- 手动计算原始数量 * 乘数
- 建议根据代币的小数位数对该值进行截断。
- 例如:如果 yUSD 有 6 位小数,用户的 UIAmount 为 1.123456789,则应显示为 “1.123456”
如何获取这些数据:
- 若要获取用户的实时余额,可以通过调用 getTokenAccountBalance 或 getAccountInfo 获取上述金额的最新信息。
- 如果你需要知道任意金额的 UI Amount,可以通过调用
amountToUiAmountForMintWithoutSimulation(web3.js v1)函数,或通过 amountToUiAmount 模拟交易来获得该计算结果。- 注意:amountToUiAmount 需要模拟交易,这意味着还需要一个有余额的有效手续费支付者。因此,这不应作为获取余额的默认方式。
RPC 调用
getTokenAccountBalance- 返回 token account balance 和 mint 信息
$ curl http://localhost:8899 -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "getTokenAccountBalance", "params": ["2uuvxpnEKw52aTqNerHiQ3WeSqZriCMNVt8LhWfrkbPk"]}' | jq .{"jsonrpc": "2.0","result": {"context": {"apiVersion": "2.2.14","slot": 381130751},"value": {"amount": "10000000","decimals": 6,"uiAmount": 20.0,"uiAmountString": "20"}},"id": 1}
getAccountInfo- 返回 account info 和 mint 信息
$ curl http://localhost:8899 -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": ["2uuvxpnEKw52aTqNerHiQ3WeSqZriCMNVt8LhWfrkbPk", {"encoding": "jsonParsed"}]}' | jq .{"jsonrpc": "2.0","result": {"context": {"apiVersion": "2.2.14","slot": 381131001},"value": {"data": {"parsed": {"info": {"extensions": [{"extension": "immutableOwner"},{"extension": "pausableAccount"}],"isNative": false,"mint": "BZCd6HfTbb5ZYJ8hQsm8282tG4QzLyeqFR6tdgQA9EAG","owner": "G7ARQSUCwNrfvTDUCZvM5xdiRdBJiN3qVw2PryD8Wnib","state": "initialized","tokenAmount": {"amount": "10000000","decimals": 6,"uiAmount": 20.0,"uiAmountString": "20"}},"type": "account"},"program": "spl-token-2022","space": 174},"executable": false,"lamports": 2101920,"owner": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb","rentEpoch": 18446744073709551615,"space": 174}},"id": 1}
更新当前金额
由于发行方可以随时更新 multiplier,你可以考虑定期轮询以保持账户余额的最新。发行方通常一天不会多次更新 multiplier。如果 multiplier 设置了未来的生效时间,可以在该时间点自动轮询更新。
交易中的 Token Amount(金⾦额)(转账 / 兑换等)
- 用户应输入要被解释为缩放后的 UIAmount 的金额。需要处理该金额的应用应将其转换为原始 token
amount 以用于交易。
- 如果存在舍入问题,建议向下取整,宁可留下极少量 dust,也不要让交易失败。
- 进行此转换时,可以使用
uiAmountToAmountForMintWithoutSimulation(web3.js v1)函数,或通过 amountToUiAmount 模拟交易。
web3js-uiAmountToAmountForMintWithoutSimulation.ts
import { uiAmountToAmountForMintWithoutSimulation } from "@solana/web3.js";import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";const connection = new Connection(clusterApiUrl("devnet"), "confirmed");const mint = new PublicKey("BZCd6HfTbb5ZYJ8hQsm8282tG4QzLyeqFR6tdgQA9EAG");const uiAmount = "20.2";const rawAmount = await uiAmountToAmountForMintWithoutSimulation(connection as unknown as Connection,mint,uiAmount);console.log("Raw Amount:", rawAmount);/* Raw Amount: 20200000 */
- 当用户请求使用其余额的 “max” 或 “all” 进行操作时,应用应使用总原始金额。这可以确保不会留下任何零碎余额。
- 可选:当使用 “max” 退还用户存储押金时,你可以考虑自动关闭账户。
代币价格
- 代币价格应尽可能始终以缩放后的价格显示。
- 如果你是价格数据服务提供商(如 oracle),应同时提供缩放和未缩放的价格。
- 尽可能提供可抽象缩放 UI 金额扩展复杂性的 SDK/API。
当前乘数
- 可随时通过调用
getAccountInfo从代币铸造合约读取当前乘数。此外,如果设置了未来的乘数,也可以从代币铸造合约获取该信息。我们建议不要显示该乘数,以免影响用户体验。
import { address, createSolanaRpc } from "@solana/kit";const rpc_url = "https://api.devnet.solana.com";const rpc = createSolanaRpc(rpc_url);const publicKey = address("BZCd6HfTbb5ZYJ8hQsm8282tG4QzLyeqFR6tdgQA9EAG");const accountInfo = await rpc.getAccountInfo(publicKey, { encoding: "jsonParsed" }).send();const mintData = accountInfo.value?.data as Readonly<{parsed: {info?: {extensions: {extension: string;state: object;}[];};type: string;};program: string;space: bigint;}>;const scaledUiAmountConfig = mintData.parsed.info?.extensions?.find((extension) => extension.extension === "scaledUiAmountConfig") as Readonly<{state: {newMultiplierEffectiveTimestamp: number;newMultiplier: number;multiplier: number;};}>;const currentMultiplier =scaledUiAmountConfig?.state &&Date.now() / 1000 >=scaledUiAmountConfig.state.newMultiplierEffectiveTimestamp? scaledUiAmountConfig.state.newMultiplier: scaledUiAmountConfig.state.multiplier;console.log("Scaled UI Amount Config:", scaledUiAmountConfig);console.log("Current Multiplier:", currentMultiplier);/*Scaled UI Amount Config: {extension: 'scaledUiAmountConfig',state: {authority: 'G7ARQSUCwNrfvTDUCZvM5xdiRdBJiN3qVw2PryD8Wnib',multiplier: '2',newMultiplier: '2',newMultiplierEffectiveTimestamp: 1743000000n}}Current Multiplier: 2*/
历史数据
价格数据源的历史数据
- 提供历史数据的服务应存储并展示缩放和未缩放的价格(用于缩放 UI 金额扩展)。
- 我们预计缩放金额会被更频繁地使用,因为这与传统金融领域处理带有拆股的代币相关图表的方式一致。
- 乘数更新可能包含一个过去的时间戳。我们建议历史数据使用区块时间戳。
- 请注意,当前生效的乘数可能是“multiplier”或“newMultiplier”,具体取决于当前时间戳以及新乘数的生效时间。
金额的历史数据
- 如果你想显示过去转账的余额,需要获取当时 slot 的乘数。你也可以在处理交易时保存转账的 UiAmount,以便将来无需再次计算。
向后兼容性
- 默认情况下,不支持缩放 UI 金额扩展的钱包和应用会通过非缩放价格 * 原始金额来显示活动的正确总价。
- 但它们会显示未缩放的价格,可能会让用户感到困惑。
- 我们希望这能鼓励团队升级他们的 dapp,以兼容使用缩放 UI 金额扩展的代币,并且我们很乐意在此过程中提供支持。
各平台推荐集成优先级
通用要求
| 要求 | 描述 | 优先级 |
|---|---|---|
| 支持使用 UiAmount 的用户操作 | 当应用启用 UiAmount 时,所有用户操作都应以 UiAmount 输入。如果应用未显示 UiAmount,则应继续使用原始金额,直到应用升级。 | P0 |
钱包
| 要求 | 描述 | 优先级 |
|---|---|---|
| 显示缩放余额 | 将缩放金额(uiAmount)作为主要余额显示。 | P0 |
| 支持代币转账 | 终端用户应以其缩放余额(原始金额 * 余额)输入转账金额。 | P0 |
| 显示现货价格 | 为用户显示缩放后的现货价格。 | P0 |
| 交易历史元数据 | 在可能的情况下,为每笔转账显示缩放金额(UIAmount)。 | P1 |
| 在交易历史中显示乘数更新 | 当发生乘数更新时,在用户的交易历史中显示该事件,包括获得的金额。 | P2 |
| 显示价格历史图表 | 在价格图表中反映缩放后的价格。 | P1 |
| 新手引导/工具提示 | 提供工具提示或新手引导,帮助用户了解使用缩放 ui amount 扩展的代币。 | P2 |
区块浏览器
| 要求 | 描述 | 优先级 |
|---|---|---|
| 代币详情页增强 | 显示元数据,如总缩放市值和当前乘数 | P0 |
| 显示余额的缩放数值 | 对当前余额显示缩放余额(UiAmount) | P0 |
| 显示交易的缩放数值 | 对历史交易的转账金额显示缩放余额(UiAmount) | P0 |
| 显示交易的缩放价格 | 显示历史交易的缩放价格 | P1 |
| 正确解析并显示乘数更新交易 | 正确显示乘数更新的详细信息 | P2 |
市场数据聚合平台(如 CoinGecko、Birdeye)
| 要求 | 描述 | 优先级 |
|---|---|---|
| 缩放数据的 API 更新 | 扩展 API 功能,包含乘数随时间变化以及缩放价格数据。 | P0 |
| 总供应量按缩放调整 | 显示总供应量和总市值时,需考虑缩放余额。 | P0 |
| 历史价格追踪 | 提供基于缩放价格的历史价格走势图。 | P1 |
| 历史乘数追踪 | 为生息代币提供乘数更新的历史标记。注意,乘数更新可能包含过去的时间戳。建议使用区块时间戳而非乘数更新中指示的时间戳作为历史数据。 | P2 |
| 教育内容或说明 | 提供简要描述或工具提示,解释缩放代币的工作原理。 | P2 |
价格源提供方
| 需求 | 描述 | 优先级 |
|---|---|---|
| 缩放与非缩放价格源 | 提供缩放和非缩放价格的价格源。 | P0 |
| 历史乘数数据 | 提供包含历史乘数变更的 API。注意,乘数更新可能包含过去的时间戳。我们建议在历史数据中使用区块时间戳,而不是乘数更新中标注的时间戳。 | P0 |
| 历史价格数据 | 提供基于缩放和非缩放金额的历史价格 API。 | P0 |
去中心化交易所(DEXes)
| 需求 | 描述 | 优先级 |
|---|---|---|
| 显示重定基代币余额 | 在界面上显示用于交易或流动性提供的缩放余额(后端仍可使用原始金额)。 | P0 |
| 支持代币操作 | 终端用户应以其 UiAmount 余额(乘数 * 原始金额)输入操作金额。 | P0 |
| 价格源适配 | 任何使用价格源显示当前价格的地方,都应向终端用户提供缩放价格。 | P1 |
| 显示价格历史图表 | 在价格图表中反映缩放价格。 | P1 |
中心化交易所(CEXes)
| 需求 | 描述 | 优先级 |
|---|---|---|
| 跟踪乘数更新 | 跟踪使用缩放 UI 金额扩展的代币的乘数更新。 | P0 |
| 显示重定基代币余额 | 在界面上显示用于交易或流动性提供的缩放余额(后端仍可使用原始金额)。 | P0 |
| 支持代币操作 | 终端用户应以其 UiAmount 余额(乘数 * 原始金额)输入操作金额。 | P0 |
| 历史操作不应重新缩放 | 历史操作(如交易)应以操作当时的准确缩放金额和价格显示。 | P1 |
| 内部跟踪原始余额 | 对链上交易应跟踪原始余额而非缩放余额,这样长期来看更准确且更易管理。 | P1 |
| 价格源适配 | 任何使用价格源显示当前价格的地方,都应向终端用户提供缩放价格。 | P1 |
| 显示价格历史图表 | 在价格图表中反映缩放价格,包括将历史价格按当前乘数重新缩放。 | P1 |
| 缩放成本基准 | 每股成本应按当前乘数进行缩放。 | P1 |
Is this page helpful?