Asentum

Reference

SDK API Reference

@asentum/sdk 0.3.1 · Estimated read time: 14 minutes

Updated 2026-11-07: SDK 0.3.1 and node 0.9.0. Adds names (reads and wallet writes) and signed messages.

TL;DR

@asentum/sdk is the TypeScript SDK for Asentum. It reads the chain, signs with Dilithium3 (ML-DSA-65) locally, sends transactions and follows them from "in a block" to "final". It works in Node.js 18+ and modern browsers. Three classes:

  • AsentumClient: every read the node serves, submitting signed transactions, and waitForReceipt.
  • AsentumWallet: a keypair that signs, hands out nonces and sends (single or batch).
  • AsentumContract: view, send and getEvents for one contract.

Plus native @names (lookups and wallet writes) and signed messages.

Working with names? Registering, transferring, primary names, profile records and building a names marketplace are covered step by step in Names for Developers.

Addresses are ase1 everywhere: the SDK accepts ase1 or 0x hex as input and returns ase1, also where the node itself answers with hex.

Install

npm install @asentum/sdk
# or
pnpm add @asentum/sdk

ESM only. Dependencies: @noble/hashes, @noble/post-quantum and @chainsafe/ssz. No polyfills are needed in the browser.

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). The node's /dev/faucet route is off on every public node, so the SDK's old faucet helpers are deprecated.

Things to know

  • 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. getBlockNumber() is the finalized height.
  • Accepted is not included. A send resolves once the node has put the transaction in its mempool. It has not run yet. 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 and views read the tip. A receipt with finalized: false is in a block. Rarely that block loses to a sibling and the transaction lands again later. Only finalized: true is permanent.
  • Addresses. Contracts work with lowercase hex, so the SDK converts ase1 addresses inside contract arguments to hex before it signs or calls /view. Hashes stay 0x hex (they have no ase1 form), and so do addresses inside contract return values and event data: use toAse1 before showing those.
  • Amounts are wei (1 ASE = 1018 wei), as decimal strings in responses.
  • Rate limits. The public gateway allows about 25 requests a second per IP. The SDK retries 429 and 5xx with backoff before it throws RateLimitError. See Rate limits and caching.

AsentumClient

Each method wraps one route of the Node API (or one JSON-RPC method). Not found is null, never an error object; an HTTP 200 error body throws RpcError.

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

const client = new AsentumClient('https://testnet.asentum.com', {
  retries: 3,              // retries for 429, 5xx and network errors
  requestTimeoutMs: 30000, // per request
  headers: {},             // for example Authorization for a private RPC
  // fetch,                // a custom fetch, optional
});

// Every method takes an optional last argument { signal, retries }
const heads = await client.getHeads({ signal: AbortSignal.timeout(5000) });

Chain and health

MethodRouteReturns
getHeads()GET /v3/heads{ finalized, tip } as numbers
getTipNumber() / getFinalizedNumber()GET /v3/headsnumber
getBlockNumber()GET /chainThe finalized height, number
getChainInfo()GET /chainchainId, finalized height, mempoolSize, latestHeader
getChainId()GET /chainbigint (1423 on testnet), cached
getMetadata()GET /metadataEIP-3085 chain details
getApiInfo()GET /apiNode identity
getGenesis()GET /genesisGenesis spec
isHealthy()GET /healthboolean
isReady()GET /readyboolean, false on 503. Node 0.8.1 reports ready even when stalled, so watch getHeads() for that.

Accounts

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

Blocks

MethodRouteReturns
getBlock(n, { tip? })GET /block/<n>[?tip=1]Block or null. finalized, plus confirmations and auraHash on a tip block.
getBlockReceipts(n, { tip? })GET /block/<n>/receipts{ receipts, finalized?, available }. available is false when the node does not have the height.
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 or null: success, error?, finalized, confirmations, blockHash, auraHash?, events, returnValue
waitForReceipt(hash, opts)polls GET /receiptsReceipt. See Fast confirmations below.
getTransaction(hash)eth_getTransactionByHashThe transaction body or null
submitRawTx(rawTx)POST /tx{ accepted, reason?, txHash? }. Does not throw on rejection.
submitRawTxBatch(rawTxs)POST /tx/batch{ accepted, rejected, total, results }

Contracts and events

MethodRouteReturns
viewCall(contract, method, args, { blockTag? })POST /view{ ok, returnValue, reason?, gasUsed, events, blockNumber, finalized }
view(contract, method, args, { blockTag? })POST /viewThe return value. Throws ViewError on ok: false.
getCode(addr) / getContractSource(addr)eth_getCodeSource as hex / as text (null without code)
getStorageAt(addr, key)eth_getStorageAtThe stored text, or null
getLogs({ address, event, fromBlock, toBlock })eth_getLogsDecoded logs: address (ase1), data (parsed JSON), blockNumber, logIndex, ... Finalized blocks only; ranges over 10,000 blocks are split.

blockTag is 'tip' (the 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; cached 2 s on the gateway)
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> (null for an unknown id)
getGovernanceLocked(addr) / getGovernanceWithdrawable(addr)GET /governance/locked/<addr>, /governance/withdrawable/<addr>
getGovernanceLibraries() / getGovernanceLibrary(name)GET /governance/libraries, /governance/library/<name>
jsonRpc(method, params)Any JSON-RPC method on POST /. Throws JsonRpcError.
faucet(to) (deprecated)POST /dev/faucet. Only works on a local devnet that turned it on.

AsentumWallet

A Dilithium3 keypair bound to a client.

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

const w1 = AsentumWallet.create(client);                      // random keypair
const w2 = AsentumWallet.fromSeed(client, seed32);            // deterministic
const w3 = AsentumWallet.fromSecretKey(client, skHex, pkHex); // exported keys
const w4 = AsentumWallet.create(client, { keyless: 'auto' }); // smaller txs once the key is on record

w1.address;      // ase1...
w1.addressHex;   // 0x... lowercase: what contracts see as msg.sender
w1.secretKeyHex; // back it up like a password
MethodWhat it does
address / addressHexase1 address / the same address as lowercase 0x hex.
getBalance() / getNonce()Balance in wei (bigint) / the node's pendingNonce (bigint).
sendTransfer({ to, amountWei })Sign and submit a transfer. Returns SentTx.
sendCall({ to, method, args, valueWei? })Sign and submit a contract call. ase1 addresses in args become hex. Default gas 2,000,000 (calls need at least 50,000).
sendTransferWei / sendCallWeiThe same with the amount or value in wei only.
deploy(source)Deploy a contract. Returns AsentumContract at the derived address; await contract.waitForDeploy().
signTransfer / signCall / signDeploySign without sending. Returns { rawTx, hash, nonce, from, contractAddress? }.
sendBatch(signedTxs)Submit signed transactions in one POST /tx/batch, in order. Per-item accepted, reason and wait().
resetNonce()Forget locally handed-out nonces; the next send asks the node.
registerName / transferName / setPrimaryName / clearPrimaryName / setNameRecord0.3.1. One method per NameRegistry write. See Names below.
signMessage(message)0.3.0. Sign a text message for sign-in. See Signed messages below.
requestFaucet() (deprecated)POST /dev/faucet. Devnets only.
await wallet.sendTransfer({ to: 'ase1...', amountWei: 10n ** 18n });
await wallet.sendCall({ to: 'ase1v0myp2de3dhj3crvkhxqlnjaw2mvp64a9efkpl', method: 'swapExactIn', args, valueWei });
const counter = await wallet.deploy(source); // counter.address is known right away
await counter.waitForDeploy();

// Sign first, submit several in one request (POST /tx/batch)
const a = await wallet.signCall({ to, method: 'a' });
const b = await wallet.signCall({ to, method: 'b' }); // nonce a.nonce + 1
const res = await wallet.sendBatch([a, b]);
if (!res.results[1].accepted) console.log(res.results[1].reason);
  • Sends resolve at acceptance. They return { hash, nonce, from, contractAddress?, wait() } once the node has the transaction in its mempool, and throw TxRejectedError with the node's reason when it did not. A rejected transaction never reaches the chain.
  • Amounts. amountWei and valueWei are exact wei. The older amount and 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.
  • Nonces. The wallet starts from the node's pendingNonce and hands out consecutive nonces itself, so back-to-back and concurrent sends get n, n+1, n+2. After a rejection it asks the node again. Pass nonce to override. Submit and poll through the same RPC, since pendingNonce is per node.
  • 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. { keyless: 'auto' } switches to that when getPublicKeyStatus().canOmitKey is true. The default is 'off'.

AsentumContract

A handle to a deployed contract, from wallet.deploy() or from an address (ase1 or hex; address is always ase1, addressHex the hex form).

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

const amm = new AsentumContract(client, 'ase1v0myp2de3dhj3crvkhxqlnjaw2mvp64a9efkpl');
const pool = JSON.parse(await amm.view('getPool', ['3']));            // reads the tip
await amm.view('getPool', ['3'], { blockTag: 'finalized' });          // finalized state
const full = await amm.viewFull('getPool', ['3']);                    // value, gas, events, blockNumber, finalized

const tx = await amm.connect(wallet).send('swapExactIn', args, { valueWei });
await tx.wait();

const swaps = await amm.getEvents('Swap', { fromBlock: 570000, toBlock: 571000 });
swaps[0].data;     // the event payload, parsed
swaps[0].address;  // ase1 of the contract
MethodWhat it does
view(method, args?, { blockTag? })Read-only call, free. Returns the value; throws ViewError.
viewFull(method, args?, { blockTag? })The full /view result, including blockNumber and finalized.
send(method, args?, { valueWei?, gasLimit?, nonce?, wallet? })Write call with the connected wallet.
connect(wallet)A copy bound to a signer.
getEvents(name?, { fromBlock, toBlock })Decoded events from eth_getLogs (finalized blocks).
getSource() / getAccount() / getBalance()Source text / account record (codeHash is BLAKE3 of the source) / balance in wei.
waitForDeploy(opts?)Wait for the deploy transaction; takes the same options as tx.wait().

See Writing Contracts for the contract side of this interface.

Names (@usernames)

Native names are live since block 1,200,000 on chain 1423 (node 0.9.0). Reads arrived in 0.3.0 and a wallet method for every registry write in 0.3.1. Worked examples, including a names marketplace, are in Names for Developers.

import { NAME_REGISTRY_CONTRACT, normalizeName, nameFeeWei, formatName } from '@asentum/sdk';

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

await (await wallet.registerName('myname')).wait();          // sends exactly nameFeeWei('myname')
await wallet.transferName('myname', 'ase1...');              // owner only, free
await wallet.setPrimaryName('myname');
await wallet.clearPrimaryName();
await wallet.setNameRecord('myname', 'x', '@asentum');       // avatar, url, x, description; '' clears

Client (0.3.0)

MethodRouteReturns
namesActive()GET /name-of/<registry>boolean. False before the flag day and on nodes older than 0.9.0.
resolveName(name, { blockTag? })GET /name/<name>The ase1 owner, or null when the name is free
getNameLookup(name, { blockTag? })GET /name/<name>{ active, name, address, ase1, blockNumber }
lookupAddress(address, { blockTag? })GET /name-of/<address>The primary name without the @, or null
resolveRecipient(input, { allowBareName? })GET /name/<name> for "@name"{ address, name }. An address passes through with no call.

Wallet (0.3.1)

MethodRegistry callNotes
registerName(name, opts?)register(name) + feeSends exactly nameFeeWei(name): 500 / 100 / 50 ASE for 3 / 4 / 5 characters, 10 ASE for 6 or more. The first name becomes the primary. At most 5 per address per 43,200 blocks.
transferName(name, to, opts?)transfer(name, to)Owner only, no fee. ase1 or 0x recipient. Clears the sender's primary if it was this name, and clears the name's records.
setPrimaryName(name, opts?)setPrimary(name)Owner only.
clearPrimaryName(opts?)clearPrimary()Keeps the name.
setNameRecord(name, key, value, opts?)setRecord(name, key, value)Owner only. Keys avatar, url (https, http or ipfs), x, description. An empty value clears.

Each write takes the usual send options (gasLimit, nonce, signal) and returns a SentTx; call .wait() and check success. An invalid name throws InvalidNameError before signing. There is no approve or operator method on the registry.

Helpers and errors

ExportWhat it is
normalizeName(input)Canonical form ("@Alice" to "alice"), or null when it breaks a rule
isNameInput(input)true for valid "@name" input
formatName(name)"alice" to "@alice"
nameFeeWei(name) / NAME_FEES_ASEFee in wei for a name / the fee table in ASE
NAME_REGISTRY_CONTRACTThe registry address, ase1 (0x...0b)
NamesNotActiveErrorNAMES_NOT_ACTIVE: before the flag day or on a node older than 0.9.0
NameNotFoundErrorNAME_NOT_FOUND: a valid name that nobody owns
InvalidNameErrorThe input breaks a naming rule

Signed messages

For wallet sign-in (0.3.0). The signed bytes are "Asentum Signed Message:\n" + byteLength(message) + "\n" + message, signed with ML-DSA-65, the same as the browser extension and the Telegram wallet, so a server checks every wallet the same way.

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

const proof = wallet.signMessage(challenge);     // { address, publicKey, signature }
verifyMessage({ ...proof, message: challenge }); // true
ExportWhat it does
wallet.signMessage(message){ address, publicKey, signature }. No transaction, no fee.
verifyMessage({ address, message, publicKey, signature })true when the key matches the address and the signature is valid
signedMessageBytes(message)The exact bytes that are signed
signMessageProblem(message)Why a message cannot be signed, or null
signMessageWithKey(message, keys)Sign with a raw keypair
SIGNED_MESSAGE_PREFIX / SIGNED_MESSAGE_MAX_BYTESThe prefix and the size limit

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, and act on it off chain only at the second. The background is in Fast Confirmations.

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

const tx = await amm.connect(wallet).send('swapExactIn', args, { valueWei });
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 lands 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;
}

// Any hash, not only one you sent:
const receipt = await client.waitForReceipt(txHash, { until: 'final' });
OptionDefaultMeaning
until'included''included': resolve at the first receipt. 'final': resolve once finalized: true.
timeoutMs60,000 / 120,000Rejects with TimeoutError. lastReceipt is set if it was in a block. It may still land.
pollIntervalMs1,000Blocks are about 2.5 s apart; faster polling buys nothing.
onIncluded(receipt)In a block. Fires again if a reorg moves it to another block.
onReorg(previous)The block it was in lost to a sibling; it is pending again.
onFinal(receipt)Final (with until: 'final').
throwOnFailurefalseReject with TxFailedError on success: false instead of resolving.
signalAn AbortSignal to stop waiting (AbortError). The transaction is not affected.
pauseWhenHiddentrueIn a browser, no polling while the tab is hidden.

The poller backs off on HTTP 429 and 5xx (Retry-After when the server sends it, otherwise doubling up to 10 seconds) and keeps going through network errors.

tx.wait() with no argument works as in 0.1: it resolves at the first receipt (in a block), also for a failed transaction, so check receipt.success. tx.wait(ms) still takes a timeout.

Errors

Every error the SDK throws on purpose extends AsentumError and has a stable code.

ClassWhen
TxRejectedErrorPOST /tx answered accepted: false. reason is the node's text; isNonceError tells nonce problems apart.
TxFailedErrorThe receipt has success: false (with throwOnFailure). receipt is attached.
TimeoutErrorWaiting ran out. Not a failure: check the explorer before sending again.
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 (status, body).
NetworkErrorfetch failed or timed out.
import { TxRejectedError, RateLimitError } from '@asentum/sdk';

try {
  await wallet.sendCall({ to, method: 'claim' });
} catch (e) {
  if (e instanceof TxRejectedError) showError(e.reason); // never reached the chain
  else if (e instanceof RateLimitError) retryLater(e.retryAfterMs);
  else throw e;
}

Helpers

Address helpers use the same names as @asentum/connect where they overlap.

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

toAse1('0x72f14d1509758feb87fdeb875d7255871d0329d3'); // 'ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue'
toHexAddress('ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue'); // '0x72f14d15...0329d3'
shortAse1('ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue'); // 'ase1wtc569...dgf8ue'
ase1ArgsToHex(['ase1...', { owner: 'ase1...' }]);     // every ase1 inside becomes 0x hex

aseToWei('1.5');              // 1500000000000000000n (always ASE)
formatAse(15n * 10n ** 17n);  // '1.5'
toWei('42');                  // 42n (wei in, wei out)

eventTopic('Swap');           // topics[0] for eth_getLogs: BLAKE3 of the name
getContractAddress(wallet.address, 3n); // where a deploy with nonce 3 lands

All response and option types are exported for TypeScript: Receipt, Block, Heads, Balance, AccountInfo, ViewCallResult, Log, ValidatorsResponse, Committee, GovernanceProposal, WaitOptions, SentTx, SignedTx and more.

Upgrading from 0.1

  • wallet.address and contract.address are ase1. Use addressHex for the 0x form.
  • Receipt sender and recipient are ase1, as are the addresses from participation, committee, liveness and governance.
  • getBlock returns null for a missing block (0.1 returned the node's error object).
  • getReceipt throws on network and RPC errors instead of returning null; null still means not in a block yet.
  • Reads that get an HTTP 200 error body throw RpcError. isReady() now returns false on a 503.
  • The default timeout of tx.wait() is 60 seconds (was 15).
  • faucet() and requestFaucet() are deprecated.

The full list is in the SDK's CHANGELOG.md.

Read next