@asentum/sdk

中文文档
0.3.1发布于 2026/11/07Apache-2.0TypeScriptES Module近 30 天下载 1972

TypeScript SDK for Asentum: chain reads, ase1 addresses, Dilithium3 signing, transactions with in-block and final confirmations, contracts and events. Works in Node.js and browsers.

原始使用文档
文档目录23 节
  1. Quick start
  2. Things to know about the chain
  3. AsentumClient
  4. Chain and health
  5. Accounts
  6. Blocks
  7. Transactions and receipts
  8. Contracts
  9. Validators, consensus, governance
  10. AsentumWallet
  11. Sending
  12. Names (@usernames)
  13. Registering, transferring and records (0.3.1)
  14. Building a names marketplace
  15. Signed messages
  16. AsentumContract
  17. Fast confirmations
  18. Errors
  19. Helpers
  20. Upgrading from 0.1.x
  21. How it works
  22. Development
  23. License

@asentum/sdk

TypeScript SDK for Asentum. Read the chain, sign with Dilithium3 (ML-DSA-65), send transactions, follow them from "in a block" to "final", and work with contracts and their events. Works in Node.js 18+ and modern browsers.

Current with node 0.8.1.

npm install @asentum/sdk

Quick start

import { AsentumClient, AsentumWallet, formatAse } from '@asentum/sdk';

const client = new AsentumClient('https://testnet.asentum.com');

// Reads need no keys
const { finalized, tip } = await client.getHeads();
const bal = await client.getBalance('ase1v0myp2de3dhj3crvkhxqlnjaw2mvp64a9efkpl');
console.log(formatAse(bal.balance), 'ASE at block', tip);

// A wallet signs locally; keys never leave your process
const wallet = AsentumWallet.fromSecretKey(client, SECRET_KEY_HEX, PUBLIC_KEY_HEX);
console.log(wallet.address); // ase1...

const tx = await wallet.sendTransfer({
  to: 'ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue',
  amountWei: 10n ** 18n, // 1 ASE
});
const receipt = await tx.wait();          // in a block, about 2.5 s
if (!receipt.success) throw new Error(receipt.error);
await tx.wait({ until: 'final' });        // final, a few blocks later

Test ASE comes from the airdrop dashboard (see "Get Test ASE" in the docs). The node's /dev/faucet route is off on every public node.

Things to know about the chain

  • Two heads. The tip is the newest block on the best chain. The finalized height trails it by about 3 blocks and never changes. getHeads() returns both.
  • Accepted is not included. POST /tx answering accepted: true only means the node put the transaction in its mempool. Wait for the receipt.
  • Receipts use success, a boolean. There is no status on the REST receipt. A failed transaction has success: false and an error reason.
  • Receipts read the tip by default. A receipt with finalized: false is in a block; rarely that block loses to a sibling and the transaction lands again in a later block. Only finalized: true is permanent.
  • Addresses. The SDK accepts ase1... or 0x... everywhere and returns ase1 for every address it surfaces, also where the node answers with hex (receipts, participation, committee, governance). Contracts work with lowercase hex internally, so the SDK converts ase1 addresses inside contract arguments to hex before signing or viewing. Hashes and raw bytes stay 0x hex (they have no ase1 form), and so do addresses inside contract return values and event data: convert those with toAse1 before showing them.
  • Amounts are wei (1 ASE = 10^18 wei) as decimal strings in responses.
  • Rate limits. The public gateway allows about 25 requests a second per IP and answers 429 with an HTML page; nodes limit block and receipt reads to a burst of 240, then 4 a second. The SDK retries 429 and 5xx with backoff (honouring Retry-After when sent) before it throws RateLimitError.

AsentumClient

const client = new AsentumClient(url?, {
  retries: 3,              // retries for 429 / 5xx / network errors
  requestTimeoutMs: 30000,
  fetch,                   // custom fetch, optional
  headers: {},             // e.g. Authorization for a private RPC
});

Every method takes an optional last argument { signal?: AbortSignal, retries?: number }.

Chain and health

MethodRouteReturns
getHeads()GET /v3/heads{ finalized: number, tip: number }
getTipNumber() / getFinalizedNumber()GET /v3/headsnumber
getBlockNumber()GET /chainfinalized height, number
getChainInfo()GET /chainchain id, finalized height, mempool size, header
getChainId()GET /chainbigint (cached)
getMetadata()GET /metadataEIP-3085 metadata
getApiInfo()GET /apinode identity
getGenesis()GET /genesisgenesis spec
isHealthy()GET /healthboolean
isReady()GET /readyboolean (false on 503; note that node 0.8.1 reports ready even when stalled, so watch getHeads())

Accounts

MethodRouteReturns
getBalance(addr)GET /balance/<addr>{ address, balance, nonce, pendingNonce }
getNonce(addr, { pending? })GET /balance/<addr>bigint, pendingNonce by default
getAccount(addr)GET /asentum/account/<addr>{ exists, balance, nonce, codeHash, storageRoot, height }
getPublicKeyStatus(addr)GET /public-key/<addr>{ recorded, canOmitKey, ... }

Blocks

MethodRouteReturns
getBlock(n, { tip? })GET /block/<n>[?tip=1]Block | null (finalized, and confirmations/auraHash on tip blocks)
getBlockReceipts(n, { tip? })GET /block/<n>/receipts{ receipts, finalized?, available }
getRawBlock(n)GET /block-raw/<n>SSZ hex or null
getFinalizedStatus(h)GET /v3/finalized?height={ finalized, auraHash? }

Transactions and receipts

MethodRouteReturns
getReceipt(hash, { finalizedOnly? })GET /receipts/<hash>[?tip=0]Receipt | null (success, error?, finalized, confirmations, blockHash, auraHash?)
waitForReceipt(hash, opts)polls GET /receiptsReceipt (see below)
getTransaction(hash)eth_getTransactionByHashEthTransaction | null
submitRawTx(rawTx)POST /tx{ accepted, reason?, txHash? }
submitRawTxBatch(rawTxs)POST /tx/batch{ accepted, rejected, total, results }

Contracts

MethodRouteReturns
viewCall(contract, method, args, { blockTag? })POST /view{ ok, returnValue, reason?, blockNumber, finalized, ... }
view(contract, method, args, { blockTag? })POST /viewthe return value; throws ViewError
getCode(addr) / getContractSource(addr)eth_getCodehex / source text
getStorageAt(addr, key)eth_getStorageAtstored text or null
getLogs({ address, event, fromBlock, toBlock })eth_getLogsdecoded Log[] (finalized blocks only; ranges over 10,000 blocks are split)

blockTag is 'tip' (default; 'latest' and 'pending' mean the same) or 'finalized' ('safe'). There are no historical state reads.

Validators, consensus, governance

MethodRoute
getValidators()GET /validators (about 260 KB)
getParticipation({ lookback })GET /participation
getCommittee({ epoch })GET /v3/committee
getLiveness()GET /consensus/liveness
getTombstones()GET /slashing/tombstones
getProposals() / getProposal(id)GET /governance/proposals, /governance/proposal/<id>
getGovernanceLocked(addr) / getGovernanceWithdrawable(addr)GET /governance/locked/<addr>, /governance/withdrawable/<addr>
getGovernanceLibraries() / getGovernanceLibrary(name)GET /governance/libraries, /governance/library/<name>
jsonRpc(method, params)POST / (any JSON-RPC method)

faucet() is deprecated: it calls /dev/faucet, which only works on a local devnet that turned it on.

AsentumWallet

const wallet = AsentumWallet.create(client);                    // random key
const wallet2 = AsentumWallet.fromSeed(client, seed32);         // deterministic
const wallet3 = AsentumWallet.fromSecretKey(client, skHex, pkHex);

wallet.address;      // ase1...
wallet.addressHex;   // 0x... lowercase, what contracts see as msg.sender
wallet.secretKeyHex; // back this up like a password
wallet.publicKeyHex;

await wallet.getBalance(); // bigint wei
await wallet.getNonce();   // bigint, the node's pendingNonce

Sending

await wallet.sendTransfer({ to, amountWei: 10n ** 18n });
await wallet.sendCall({ to: contract, method: 'swap', args: [...], valueWei: 0n });
const c = await wallet.deploy(source); // c.address is known right away
await c.waitForDeploy();

Every send returns { hash, nonce, from, contractAddress?, wait() } once the node has accepted it, and throws TxRejectedError (with reason) when it did not. A rejected transaction never reaches the chain.

Amounts. amountWei / valueWei are exact wei. The older amount / value fields go through parseAse: a number or decimal string is ASE ('1.5' is 1.5 ASE), a bigint is wei, and an integer string longer than 18 digits is read as wei. sendTransferWei and sendCallWei take wei only.

Nonces. The wallet asks the node for pendingNonce and hands out consecutive nonces itself, so back-to-back and concurrent sends from one wallet get n, n+1, n+2. After a rejection it asks the node again (wallet.resetNonce() does the same by hand). Pass nonce to override. Submit and poll through the same RPC: pendingNonce is per node.

Batches. Sign first, then submit in one request:

const a = await wallet.signCall({ to, method: 'a' });
const b = await wallet.signCall({ to, method: 'b' });
const res = await wallet.sendBatch([a, b]);   // POST /tx/batch
res.results[0].accepted; res.results[0].wait();

Key-less transactions. Once the chain has an account's public key on record, its transactions can leave the key out and are about a third smaller. AsentumWallet.create(client, { keyless: 'auto' }) switches to that as soon as getPublicKeyStatus().canOmitKey is true. The default is 'off'.

Other overrides on every send: gasLimit (defaults: 21,000 for transfers, 2,000,000 for calls, at least 2,000,000 for deploys; contract calls need at least 50,000), maxFeePerGas (default 1), signal.

Names (@usernames)

From node 0.9.0 and the names flag day (block 1,200,000 on chain 1423) an account can own names like @milkie. A name points at an address; resolve it when you build a transaction and show both on the confirm screen.

if (await client.namesActive()) {
  const { address, name } = await client.resolveRecipient('@milkie'); // throws NameNotFoundError for a free name
  const primary = await client.lookupAddress(wallet.address);          // "milkie" or null
}

Before the flag day, and on nodes older than 0.9.0, every lookup throws NamesNotActiveError.

Registering, transferring and records (0.3.1)

Every write is a signed call to the NameRegistry system contract (NAME_REGISTRY_CONTRACT, 0x...0b). The wallet has a method for each:

await (await wallet.registerName('milkie')).wait();      // sends exactly nameFeeWei('milkie')
await wallet.transferName('milkie', 'ase1...');          // free; ase1 or 0x recipient
await wallet.setPrimaryName('milkie');                   // the name apps show for this address
await wallet.clearPrimaryName();
await wallet.setNameRecord('milkie', 'x', '@asentum');   // keys: avatar, url, x, description; '' clears
MethodContract callNotes
registerName(name)register(name) + fee500 / 100 / 50 ASE for 3 / 4 / 5 characters, 10 ASE for 6 or more. Half is burned, half goes to the treasury. The first name a wallet gets becomes its primary. At most 5 registrations per address per 43,200 blocks.
transferName(name, to)transfer(name, to)Owner only, no fee. The name stops being the sender's primary and its records are cleared for the new owner.
setPrimaryName(name)setPrimary(name)Owner only.
clearPrimaryName()clearPrimary()Keeps the name.
setNameRecord(name, key, value)setRecord(name, key, value)Owner only. avatar and url start with https://, http:// or ipfs://; x is an X handle.

Each method takes the usual send options (gasLimit, nonce, signal, ...) and returns a SentTx; call .wait() for the receipt. Invalid names throw InvalidNameError before anything is signed.

Reads: client.getNameLookup(name) (owner, records, primary flag), client.resolveName(name), client.lookupAddress(address), or a view call such as client.viewCall(NAME_REGISTRY_CONTRACT, 'isAvailable', ['milkie']).

Building a names marketplace

The registry has no approve or operator method: only the owner can move a name. A contract can own names, though, so a marketplace works as an escrow. Inside a contract the registry sees the marketplace as msg.sender, and the marketplace moves names with a cross-contract call, E(registry).transfer(name, buyer).

List in this order so nobody else can claim a name in between:

  1. The seller calls marketplace.prepare(name, price). The marketplace checks E(registry).get(name).owner === msg.sender and records the seller.
  2. The seller calls wallet.transferName(name, marketplaceAddress).
  3. marketplace.activate(name) checks the marketplace now owns the name.
  4. A buyer calls marketplace.buy(name) with the price. The marketplace pays the seller and calls E(registry).transfer(name, buyer).
  5. To cancel, the seller calls marketplace.cancel(name) and gets the name back through the same transfer.

Records are cleared on every transfer, so the buyer starts with a clean profile.

Signed messages

const proof = wallet.signMessage(challenge); // { address, publicKey, signature }
verifyMessage({ ...proof, message: challenge }); // true

The signed bytes are "Asentum Signed Message:\n" + byteLength(message) + "\n" + message (ML-DSA-65), the same as window.asentum.signMessage and the Telegram wallet, so a server checks every wallet the same way.

AsentumContract

import { AsentumContract } from '@asentum/sdk';

const pool = new AsentumContract(client, 'ase1v0myp2de3dhj3crvkhxqlnjaw2mvp64a9efkpl');
await pool.view('getPool', ['3']);                          // reads the tip
await pool.view('getPool', ['3'], { blockTag: 'finalized' });
await pool.viewFull('getPool', ['3']);                      // value, gas, events, blockNumber
const tx = await pool.connect(wallet).send('swapExactIn', [...], { valueWei });
await pool.getEvents('Swap', { fromBlock: 570000, toBlock: 571000 });
await pool.getSource();
await pool.getAccount(); // codeHash = BLAKE3 of the source

Fast confirmations

A transaction is in a block about one block after you send it (roughly 2.5 seconds) and final a few blocks later (roughly 7 to 10 seconds). Show success at the first point, act on it off chain only at the second.

import { TimeoutError } from '@asentum/sdk';

const tx = await contract.send('swapExactIn', args);
setStatus(tx.hash, 'pending');

try {
  await tx.wait({
    until: 'final',
    onIncluded: (r) => setStatus(tx.hash, r.success ? 'in-block' : 'failed'),
    onReorg: () => setStatus(tx.hash, 'pending'),   // its block lost; it will land again
    onFinal: (r) => setStatus(tx.hash, r.success ? 'final' : 'failed'),
    timeoutMs: 120_000,
    signal,                                          // AbortSignal, optional
  });
} catch (e) {
  if (e instanceof TimeoutError) setStatus(tx.hash, 'still-pending'); // not a failure
  else throw e;
}

client.waitForReceipt(hash, options) is the same for any hash. Options:

OptionDefaultMeaning
until'included''included': resolve at the first receipt. 'final': resolve once finalized: true.
timeoutMs60,000 / 120,000Rejects with TimeoutError (lastReceipt set if it was in a block).
pollIntervalMs1,000Blocks are about 2.5 s apart; faster buys nothing.
onIncluded(r)In a block. Fires again if a reorg moves it to another block.
onReorg(prev)The block it was in lost to a sibling.
onFinal(r)Final (with until: 'final').
throwOnFailurefalseReject with TxFailedError on success: false instead of resolving.
signalStop waiting (AbortError). The transaction is not affected.
pauseWhenHiddentrueIn a browser, no polling while the tab is hidden.

The poller backs off on 429 and 5xx (Retry-After when sent, otherwise doubling up to 10 s) and keeps going through network blips.

tx.wait() with no argument behaves as in 0.1.x: it resolves at the first receipt (in a block), including a failed one. tx.wait(ms) still takes a timeout.

Errors

All errors the SDK throws on purpose extend AsentumError and carry a stable code:

ClassWhen
TxRejectedErrorPOST /tx answered accepted: false (reason, isNonceError)
TxFailedErrorthe receipt has success: false (with throwOnFailure)
TimeoutErrorwaiting ran out (lastReceipt); the tx may still land
AbortErroryour AbortSignal fired
ViewErrorPOST /view answered ok: false
RpcErrorHTTP 200 with an error body (bad address, bad hash, failed governance read)
JsonRpcErrora JSON-RPC error object (rpcCode)
RateLimitErrorHTTP 429 after retries (retryAfterMs)
HttpErrorany other non-2xx (status, body)
NetworkErrorfetch failed or timed out

"Not found" is not an error: getReceipt, getBlock, getTransaction, getRawBlock and getProposal return null.

Helpers

import {
  toAse1, toHexAddress, hexToAse1, ase1ToHex, isAse1, isHexAddress, isAddress, shortAse1,
  ase1ArgsToHex, aseToWei, parseAse, formatAse, toWei, eventTopic, getContractAddress,
} from '@asentum/sdk';

toAse1('0x72f14d1509758feb87fdeb875d7255871d0329d3'); // 'ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue'
toHexAddress('ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue'); // '0x72f1...29d3'
ase1ArgsToHex(['ase1...', { owner: 'ase1...' }]); // every ase1 inside becomes 0x hex
aseToWei('1.5');        // 1500000000000000000n (always ASE)
formatAse(15n * 10n ** 17n); // '1.5'
eventTopic('Swap');     // topics[0] for eth_getLogs: BLAKE3 of the name
getContractAddress(wallet.address, 3n); // where a deploy with nonce 3 lands

Upgrading from 0.1.x

See CHANGELOG.md. The short list: wallet.address and contract.address are ase1 now (addressHex has the hex), receipt sender/recipient are ase1, getBlock returns null for a missing block, getReceipt throws on transport errors instead of returning null, and the faucet helpers are deprecated.

How it works

Transactions are SSZ encoded, hashed with BLAKE3, signed with ML-DSA-65 (Dilithium3) locally and submitted as hex to POST /tx. The transaction hash is BLAKE3 of the signed bytes, so the SDK knows it before the node answers. Reads use the node's REST routes, plus JSON-RPC for code, storage, transactions and logs. See the Node API Reference and Fast Confirmations.

Development

npm test                 # unit tests (mocked fetch)
npm run test:live        # read-only smoke test against https://testnet.asentum.com
npm run typecheck
npm run build

License

Apache-2.0

版本与发布记录

@asentum/sdk 共发布 4 个版本,首次发布于 2026/05/16;过去 12 个月发布了 4 个版本。

版本发布日期距今
0.3.1latest2026/11/07今天
0.3.02026/11/062 天前
0.2.02026/10/2118 天前
0.1.02026/05/165 个月前

关于 @asentum/sdk 的常见问题

@asentum/sdk 是什么?

TypeScript SDK for Asentum: chain reads, ase1 addresses, Dilithium3 signing, transactions with in-block and final confirmations, contracts and events. Works in Node.js and browsers.

如何安装 @asentum/sdk?

在项目目录运行 npm install @asentum/sdk,或使用 pnpm add / yarn add / bun add。

@asentum/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@asentum/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@asentum/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@asentum/sdk 可以商用吗?

@asentum/sdk 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@asentum/sdk 的下载量有多少?

根据 npm 官方下载统计,@asentum/sdk 在 2026/10/07 – 2026/11/05 这 30 天内被下载 1,972 次,日均约 66 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@asentum/sdk 还在维护吗?

最新版本 0.3.1 发布于 2026/11/07(今天);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/05/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。