原文作者:PaperMoon 团队
你写了一个 Polkadot 上的应用,用户连接钱包后,你需要显示余额。看上去很简单,在以太坊里调用 `eth_getBalance` 就完事了。到了 Polkadot,账户结构多了好几层:`free`、`reserved`、`frozen`、nonce、consumers……这些字段分别是什么,从哪个 SDK 查,怎么转换成人类可读的数字?
本文把五种 SDK 的代码都列出来,你选一个自己熟悉的语言跑一遍就清楚了。
说明:本文示例连接的是 Paseo 测试网(Asset Hub)。如果是主网,把 WebSocket 地址换掉即可,代码逻辑完全相同。
一、账户数据字段详解
在写代码之前,先把要查的字段弄清楚。Polkadot 账户信息存在链上的 `System.Account` 这个 storage 里,查出来的数据结构长这样:
```
nonce
consumers
providers
sufficients
data:
free
reserved
frozen
```
nonce:账户发出的交易计数器,和以太坊的 nonce 含义相同。每发一笔交易加 1,用于防止同一笔交易被重放。构建交易时需要用到它。
free:可自由使用的余额,转账、支付手续费都从这里扣。查到的数值是最小单位(无小数),除以 `10_000_000_000`(10 的 10 次方)才是显示给用户看的 PAS/DOT 数量。
reserved:被锁定用于特定目的的余额,比如参与链上治理需要存款、创建资产需要押金。这部分余额不能转账,但还是你的。
frozen:被冻结的余额,不能用于转账,但某些操作(如质押投票)仍可使用。通常由特定 pallet 设置。
consumers / providers / sufficients:这三个是账户的"引用计数",用于管理账户的生命周期。`providers` 表示维持账户存在的来源数(比如余额不为零);`consumers` 表示有几个模块依赖了这个账户;`sufficients` 表示独立维持账户存在的来源数。一般开发者不需要直接操作这三个字段,知道它们的含义就够了。
二、选哪个 SDK?
本文涉及五个 SDK,先说一个重要信息:**Polkadot.js API 已经不再主动维护**,官方文档明确标注了这一点。如果你是新项目,直接选 PAPI 或 Dedot;如果你的现有代码用了 Polkadot.js,可以考虑逐步迁移。
|
SDK |
语言 |
维护状态 |
推荐使用场景 |
一句话理解 |
|---|---|---|---|---|
|
PAPI (Polkadot API) |
TypeScript |
主动维护 ✅ |
新的 TS/JS dApp、前端应用 |
新一代官方交互库(推荐默认选择) |
|
Polkadot.js API |
JavaScript |
停止维护 ⚠️ |
仅用于维护旧项目 |
历史主流库,但已进入维护模式 |
|
Dedot |
TypeScript |
主动维护 ✅ |
轻量级 TS 项目、性能敏感前端 |
更轻、更快的现代 TS 方案 |
|
Python Substrate Interface |
Python |
主动维护 ✅ |
自动化脚本、数据抓取、后端工具 |
写脚本最舒服的方案 |
|
Subxt |
Rust |
主动维护 ✅ |
高性能服务、Indexer、链工具 |
Rust 原生类型安全 SDK |
三、PAPI 实现(推荐)
3.1 环境准备
```bash
mkdir papi-query-account-example && cd papi-query-account-example
npm init -y && npm pkg set type=module
```
安装依赖:
```bash
npm install polkadot-api
npm install --save-dev @types/node tsx typescript
```
生成链类型(连接测试网生成类型描述文件):
```bash
npx papi add polkadotTestNet -w wss://asset-hub-paseo.dotters.network
```
这一步 PAPI 区别于 Polkadot.js 的地方:它会从链上拉取元数据并生成强类型的 TypeScript 描述,后续调用时有完整的代码补全,不容易写错字段名。
3.2 代码
新建 `query-account.ts`:
```typescript
import { createClient } from 'polkadot-api';
// 中文:PAPI 的核心入口,用于创建到链的连接客户端
import { getWsProvider } from 'polkadot-api/ws-provider';
// 中文:WebSocket provider,负责建立 ws 连接
import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat';
// 中文:兼容层,确保 PAPI 能正确处理 Polkadot SDK 链的响应格式
import { polkadotTestNet } from '@polkadot-api/descriptors';
// 中文:由 npx papi add 命令生成的链类型描述,提供强类型支持
const POLKADOT_HUB_RPC = 'INSERT_WS_ENDPOINT'; // 替换为你的 WebSocket 地址
const ACCOUNT_ADDRESS = 'INSERT_ACCOUNT_ADDRESS'; // 替换为要查询的 SS58 地址
const PAS_UNITS = 10_000_000_000;
// 中文:PAS/DOT 的最小单位换算系数(精度 10 位),链上数据除以此值得到人类可读余额
async function main() {
try {
const client = createClient(
withPolkadotSdkCompat(getWsProvider(POLKADOT_HUB_RPC))
);
const api = client.getTypedApi(polkadotTestNet);
console.log('Connected to Polkadot Hub');
const accountInfo = await api.query.System.Account.getValue(
ACCOUNT_ADDRESS
);
// 中文:查询 System pallet 下的 Account storage,传入 SS58 地址,返回账户完整信息
console.log(`Nonce: ${accountInfo.nonce}`);
console.log(`Consumers: ${accountInfo.consumers}`);
console.log(`Providers: ${accountInfo.providers}`);
console.log(`Sufficients: ${accountInfo.sufficients}`);
console.log(
`Free Balance: ${accountInfo.data.free} (${
Number(accountInfo.data.free) / PAS_UNITS
} PAS)`
);
console.log(
`Reserved Balance: ${accountInfo.data.reserved} (${
Number(accountInfo.data.reserved) / PAS_UNITS
} PAS)`
);
console.log(
`Frozen Balance: ${accountInfo.data.frozen} (${
Number(accountInfo.data.frozen) / PAS_UNITS
} PAS)`
);
const total =
Number(accountInfo.data.free) + Number(accountInfo.data.reserved);
console.log(`Total Balance: ${total} (${total / PAS_UNITS} PAS)`);
// 中文:总余额 = free + reserved,frozen 不单独计入(它是 free 的子集)
await client.destroy();
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
main();
```
3.3 运行
```bash
npx tsx query-account.ts
```
预期输出:
```
Connected to Polkadot Hub
Querying account: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
Account Information:
===================
Nonce: 15
Consumers: 0
Providers: 1
Sufficients: 0
Balance Details:
================
Free Balance: 59781317040 (5.978131704 PAS)
Reserved Balance: 0 (0 PAS)
Frozen Balance: 0 (0 PAS)
Total Balance: 59781317040 (5.978131704 PAS)
Disconnected
```
四、Polkadot.js API 实现(旧项目维护用)
官方已明确该库不再主动开发,新项目建议直接跳过这一节,用 PAPI 或 Dedot。
```bash
mkdir pjs-query-account-example && cd pjs-query-account-example
npm init -y && npm pkg set type=module
npm install @polkadot/api
```
新建 `query-account.js`:
```javascript
import { ApiPromise, WsProvider } from '@polkadot/api';
// 中文:ApiPromise 是 Polkadot.js 的主要 API 类;WsProvider 建立 WebSocket 连接
const POLKADOT_HUB_RPC = 'INSERT_WS_ENDPOINT';
const ACCOUNT_ADDRESS = 'INSERT_ACCOUNT_ADDRESS';
const PAS_UNITS = 10_000_000_000;
async function main() {
const wsProvider = new WsProvider(POLKADOT_HUB_RPC);
const api = await ApiPromise.create({ provider: wsProvider });
const accountInfo = await api.query.system.account(ACCOUNT_ADDRESS);
// 中文:注意大小写:PAPI 用 System.Account(大写),Polkadot.js 用 system.account(小写)
console.log(`Nonce: ${accountInfo.nonce.toString()}`);
// 中文:Polkadot.js 返回的字段是 Codec 类型,需要 .toString() 或 .toBigInt() 才能转成普通数值
console.log(`Free Balance: ${Number(accountInfo.data.free.toBigInt()) / PAS_UNITS} PAS`);
console.log(`Reserved Balance: ${Number(accountInfo.data.reserved.toBigInt()) / PAS_UNITS} PAS`);
console.log(`Frozen Balance: ${Number(accountInfo.data.frozen.toBigInt()) / PAS_UNITS} PAS`);
await api.disconnect();
}
main().catch(console.error);
```
```bash
node query-account.js
```
五、Dedot 实现
Dedot 是 Polkadot.js 的轻量级替代品,API 风格更接近 Polkadot.js,但体积更小,也在主动维护。从 Polkadot.js 迁移过来的成本比切换到 PAPI 更低。
```bash
mkdir dedot-query-account-example && cd dedot-query-account-example
npm init -y && npm pkg set type=module
npm install dedot
npm install --save-dev @dedot/chaintypes @types/node tsx typescript
```
新建 `query-account.ts`:
```typescript
import { DedotClient, WsProvider } from 'dedot';
// 中文:DedotClient 是 Dedot 的主客户端,用法和 Polkadot.js 的 ApiPromise 类似
import type { PolkadotAssetHubApi } from '@dedot/chaintypes';
// 中文:来自 @dedot/chaintypes 的链类型定义,提供代码补全
const POLKADOT_HUB_RPC = 'INSERT_WS_ENDPOINT';
const ACCOUNT_ADDRESS = 'INSERT_ACCOUNT_ADDRESS';
const PAS_UNITS = 10_000_000_000;
async function main() {
const provider = new WsProvider(POLKADOT_HUB_RPC);
const client = await DedotClient.new<PolkadotAssetHubApi>(provider);
const accountInfo = await client.query.system.account(ACCOUNT_ADDRESS);
// 中文:查询方式和 Polkadot.js 几乎一致(system.account 小写),
// 但返回值是原生 TypeScript 类型,不需要 .toString() 转换
console.log(`Nonce: ${accountInfo.nonce}`);
console.log(`Free Balance: ${Number(accountInfo.data.free) / PAS_UNITS} PAS`);
console.log(`Reserved Balance: ${Number(accountInfo.data.reserved) / PAS_UNITS} PAS`);
console.log(`Frozen Balance: ${Number(accountInfo.data.frozen) / PAS_UNITS} PAS`);
const total = Number(accountInfo.data.free) + Number(accountInfo.data.reserved);
console.log(`Total Balance: ${total / PAS_UNITS} PAS`);
await client.disconnect();
}
main().catch(console.error);
```
```bash
npx tsx query-account.ts
六、Python Substrate Interface 实现
```bash
mkdir psi-query-account-example && cd psi-query-account-example
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install substrate-interface
```
新建 `query_account.py`:
```python
from substrateinterface import SubstrateInterface
# 中文:SubstrateInterface 是 Python 连接 Substrate 链的主要接口类
POLKADOT_HUB_RPC = "INSERT_WS_ENDPOINT"
ACCOUNT_ADDRESS = "INSERT_ACCOUNT_ADDRESS"
PAS_UNITS = 10_000_000_000
def main():
substrate = SubstrateInterface(url=POLKADOT_HUB_RPC)
account_info = substrate.query(
module="System",
storage_function="Account",
params=[ACCOUNT_ADDRESS]
)
# 中文:Python 版查询方式是字符串形式的 module + storage_function,没有代码补全,
# 拼写必须和链上 pallet 名完全匹配(大小写敏感)
free_balance = account_info.value["data"]["free"]
reserved_balance = account_info.value["data"]["reserved"]
frozen_balance = account_info.value["data"]["frozen"]
print(f"Nonce: {account_info.value['nonce']}")
print(f"Free Balance: {free_balance / PAS_UNITS} PAS")
print(f"Reserved Balance: {reserved_balance / PAS_UNITS} PAS")
print(f"Frozen Balance: {frozen_balance / PAS_UNITS} PAS")
print(f"Total Balance: {(free_balance + reserved_balance) / PAS_UNITS} PAS")
substrate.close()
if __name__ == "__main__":
main()
```
```bash
python query_account.py
```
七、Subxt 实现(Rust)
Subxt 是 Rust 生态中查询和提交 Substrate 链交易的主要库。和之前账户创建用的 `sp-core` 不同,Subxt 面向的是链上交互,需要先拉取链的元数据。
```bash
cargo new subxt-query-account-example && cd subxt-query-account-example
cargo install subxt-cli@0.35.3
```
下载链元数据(只需执行一次):
```bash
subxt metadata --url INSERT_WS_ENDPOINT -o polkadot_testnet_metadata.scale
```
在 `Cargo.toml` 中添加依赖:
```toml
[package]
name = "subxt-query-account-example"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "query_account"
path = "src/bin/query_account.rs"
[dependencies]
subxt = { version = "0.44.0" }
tokio = { version = "1.36.0", features = ["macros", "rt"] }
```
新建 `src/bin/query_account.rs`:
```rust
use std::str::FromStr;
use subxt::utils::AccountId32;
use subxt::{OnlineClient, PolkadotConfig};
// 中文:OnlineClient 建立到链的连接;PolkadotConfig 包含 Polkadot 网络的配置(地址类型、哈希函数等)
#[subxt::subxt(runtime_metadata_path = "polkadot_testnet_metadata.scale")]
pub mod polkadot_testnet {}
// 中文:这个宏从本地的 .scale 元数据文件生成链的类型绑定,类似 PAPI 的 npx papi add 步骤
const POLKADOT_TESTNET_RPC: &str = "INSERT_WS_ENDPOINT";
const ACCOUNT_ADDRESS: &str = "INSERT_ACCOUNT_ADDRESS";
const PAS_UNITS: u128 = 10_000_000_000;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = OnlineClient::<PolkadotConfig>::from_url(POLKADOT_TESTNET_RPC).await?;
let account = AccountId32::from_str(ACCOUNT_ADDRESS)?;
// 中文:将 SS58 地址字符串解析为 AccountId32 类型,Subxt 的 storage 查询接受这个类型
let storage_query = polkadot_testnet::storage().system().account(account);
// 中文:构建 storage 查询:访问 system pallet 下的 account storage,传入账户 ID
let account_info = api
.storage()
.at_latest() // 中文:查询最新区块的状态
.await?
.fetch(&storage_query)
.await?;
if let Some(info) = account_info {
println!("Nonce: {}", info.nonce);
println!("Free Balance: {} PAS", info.data.free as f64 / PAS_UNITS as f64);
println!("Reserved Balance: {} PAS", info.data.reserved as f64 / PAS_UNITS as f64);
println!("Frozen Balance: {} PAS", info.data.frozen as f64 / PAS_UNITS as f64);
let total = info.data.free + info.data.reserved;
println!("Total Balance: {} PAS", total as f64 / PAS_UNITS as f64);
} else {
println!("Account not found");
// 中文:账户从未接收过任何 DOT 时,链上不会存储账户记录,fetch 返回 None
}
Ok(())
}
```
```bash
cargo run --bin query_account
```
八、五种 SDK 对比
|
对比项 |
PAPI |
Polkadot.js |
Dedot |
Python SI |
Subxt |
|---|---|---|---|---|---|
|
语言 |
TypeScript |
JavaScript |
TypeScript |
Python |
Rust |
|
维护状态 |
✅ 主动维护 |
⚠️ 已停止维护 |
✅ 主动维护 |
✅ 主动维护 |
✅ 主动维护 |
|
类型安全 |
强(自动生成类型) |
弱(动态 + Codec) |
中(需手动类型包) |
无 |
强(宏 + 编译期检查) |
|
元数据获取方式 |
npx papi add 生成类型 |
自动加载 |
引入 @dedot/chaintypes |
自动 |
subxt metadata 生成代码 |
|
余额字段类型 |
原生 JS 数值 / bigint |
Codec 类型(需 .toBigInt()) |
原生 JS 数值 |
Python dict |
Rust u128 |
|
开发体验 |
现代前端体验 |
历史 Web3 体验 |
轻量快速 |
简单直接 |
工程化最强 |
|
学习成本 |
低 |
中 |
低 |
很低 |
高 |
|
典型用途 |
dApp、钱包、课程教学 |
旧项目维护 |
轻量前端 |
脚本/运维 |
如果你用 TypeScript,从 PAPI 开始。如果你已经有了 Polkadot.js 的项目,切 Dedot 的迁移成本最低。如果你写 Python 脚本,`substrate-interface` 就够用。需要高性能或要深度集成 Substrate 的场景,用 Subxt。
查账户余额这件事本身不难,代码跑通了也就十几行。真正需要花时间搞清楚的是 free / reserved / frozen 这三种余额的区别——在用户实际使用中,如果你只显示 free balance,用户可能会困惑"为什么我明明有这么多余额,却转不出去那么多"。把这三个字段都显示出来,或者至少显示"可用余额(free)"和"总余额(free + reserved)",对用户体验会有明显改善。
阅读原文:https://docs.polkadot.com/chain-interactions/accounts/query-accounts/

&spm=1001.2101.3001.5002&articleId=158456387&d=1&t=3&u=873c364112c24fc684d1378fb4d04473)
390

被折叠的 条评论
为什么被折叠?



