npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cellula-sdk

v0.2.1

Published

TypeScript client for the Cells naming protocol on Nervos CKB

Readme

cellula-sdk

TypeScript client for Cells, the permissionless .cell naming protocol on Nervos CKB. It resolves names and records, resolves records back to names, and drives every write action (register, edit, transfer, renew, recycle, delegate a manager, set a primary name), signed by the caller's own wallet, with no operator in the loop.

Built on @ckb-ccc/core. The wire codec (src/codec.ts) is a 1:1 mirror of the on-chain Rust cells-core, drift-checked against real on-chain bytes in test/codec.test.ts, so the SDK and the contract can never disagree. It handles both the v1 layout and the v2 manager layout (decision 0006) transparently.

Getting it

npm i cellula-sdk @ckb-ccc/core @ckb-ccc/spore

CCC is a peer dependency, not a dependency, and that is deliberate. It carries shared state, so two copies in one tree do not recognise each other's objects; pinning it here would have handed every application a second copy of whatever it already had. Measured: with CCC as an ordinary dependency, an application on 1.22.0 ended up with ours at 1.19.1 nested underneath. As a peer, it keeps one.

@ckb-ccc/spore is a peer for the same reason; it carries deeds. Each Spore release pins one CCC exactly (1.6.9 pins 1.19.1, 1.6.12 pins 1.21.0, 1.6.15 pins 1.23.0), so take the one that pins the CCC you use, or npm nests a second CCC under it.

Inside this repository the same specifier resolves to the source: the web app writes cellula-sdk and its bundler maps it to sdk/src/index.ts (see ui/vite.config.ts), while the scripts and examples import the path directly. The runnable version of every snippet on this page is in examples/, and node examples/resolve.ts alice.cell works with no key and no configuration file.

You may not need this at all. Reading a name is one HTTP call to the resolver, and cellula-id wraps exactly that with no keys and no dependencies. Take this package when you need to write: register, renew, transfer, sell, protect. See ../docs/RESOLVER.md for the plain HTTP path.

Read

import { ccc } from '@ckb-ccc/core'
import { CellsClient, deploymentFor } from 'cellula-sdk'

const cells = new CellsClient(new ccc.ClientPublicMainnet(), deploymentFor('mainnet'))

await cells.resolve('alice.cell', '60')    // ETH address (0x..) or null
await cells.addresses('alice.cell', '60')  // all address.60 records
await cells.records('alice.cell', 'profile.')
await cells.reverse('0xabc...', '60')      // names whose records point at this address
await cells.primaryName('ckt1...')         // a wallet's verified primary name, or null
await cells.list()                         // every live name, with its records

list() reads each name's records, one round trip per name. For a list people page through, read the names without their records and fetch the records of the rows on screen:

import { payMethodsOf } from 'cellula-sdk'

const names = await cells.liteList()       // label, id, owner, expiry: one scan per 100 names
const shown = await cells.hydrateMany(names.slice(0, 10))
payMethodsOf(shown[0].records)             // ['ckb', 'lightning', ...]

deploymentFor carries the code cells this release was built against. They move when a contract is upgraded in place (the code hashes do not), so after an upgrade take the next release; the resolver's /verify lists the live ones. Reading needs only the type script, which does not move.

primaryName is trustless without any extra contract: it reads the wallet's reverse-record cell, then forward-resolves the claimed name and returns it only if that name is actually owned by the same wallet, so a forged record resolves to nothing.

Meals (decision 0037): a payment into the lock of a name's deed that says which name it was for, with an optional note. Only these feed a name's being, and only a name held as a deed has one; a transfer into the address the name publishes is a payment to its owner, not a meal. What the being eats stays under that lock, the belly, until the deed's holder empties it.

import { addMeal, bellyOf, deedLock, scanMeals } from 'cellula-sdk'

const deed = await cells.deedOf('alice.cell') // null for a name owned by a plain key
const belly = await deedLock(client, deed.sporeTypeHash) // where a meal goes; 73 CKB is the smallest cell it holds
const tx = ccc.Transaction.from({ outputs: [{ lock: belly, capacity: 73_00000000n }], outputsData: ['0x'] })
await tx.completeInputsByCapacity(signer)
addMeal(tx, { to: 'alice.cell', note: 'congrats on the launch' }) // after the inputs, before the fee
await tx.completeFeeBy(signer, 2000)

const { meals } = await scanMeals(client, belly, 'alice.cell') // newest first: payer, amount, note
const { total } = await bellyOf(client, deed.sporeTypeHash) // eaten and not yet taken
await cells.emptyBelly(holderSigner, 'alice.cell') // the holder takes it, the Spore in the inputs

Accept .cell in a CCC app

@ckb-ccc/core 1.23.0 added addressResolver to the client config (ckb-devrel/ccc#575): a hook Address.fromString calls for a string it cannot parse. This package ships the .cell one, so any app that reads addresses through fromString takes names with one line:

import { ccc } from '@ckb-ccc/core'
import { cellResolver } from 'cellula-sdk'

const client = ccc.ClientPublicMainnet.open({ addressResolver: cellResolver() }).value
const to = await ccc.Address.fromString('alice.cell', client) // the CKB address alice.cell publishes

A real address still parses first, so nothing else changes. For a .cell name the resolver asks the cellula.id resolver where the name's cell is, then checks that answer against the client you gave it: the cell is live, its type is the namespace's, its data names the label, and the payout sits in records whose hash the cell commits to. Only then is the address in them returned. A wrong or stale hint falls through to reading the chain, so the hint can speed things up but never point elsewhere. It answers undefined, which fromString reports as not found, for a name that is unregistered, expired, withdrawn under the disputes policy, or without a CKB payout. cellResolver({ api: null }) reads the chain only. node examples/resolve-ccc.ts developer.cell runs it against testnet.

This package builds against @ckb-ccc/core 1.19.1, the version the wallets pin (decision 0031); the resolver is a plain object, so an app on 1.23.0 or later uses it as is.

Write

const signer = new ccc.SignerCkbPrivateKey(client, '0x...')   // any wallet signer

// Permissionless register (commit-reveal handled for you):
const { available } = await cells.availability('satoshi')
if (available) {
  await cells.registerWithCommit(signer, 'satoshi', {
    records: [{ key: 'address.60', label: '', value: '0x...20 bytes', ttl: 300 }],
  })
}

// Owner or manager:
await cells.editRecords(signer, 'satoshi.cell', [{ key: 'address.60', label: '', value: '0x...', ttl: 300 }])

// Owner only:
await cells.setManager(signer, 'satoshi.cell', managerAddress)  // delegate (migrates v1 -> v2)
await cells.transfer(signer, 'satoshi.cell', newOwnerAddress)    // resets the manager
await cells.renew(signer, 'satoshi.cell', newExpiredAt)
await cells.setPrimary(signer, 'satoshi.cell')                   // or '' to clear

// Permissionless:
await cells.recycle(signer, 'expired.cell')

// Buying a listed name: the listing is read again when you buy, so pass the one you showed,
// and a listing that changed in between is refused before the wallet is asked.
const offer = await cells.listing('satoshi.cell')
if (offer) await cells.buy(signer, 'satoshi.cell', { maxPriceShannons: offer.priceShannons, outPoint: offer.outPoint })

register finds the predecessor, builds the splice (predecessor preserved plus a new cell owned by the signer), funds capacity and fee from the signer, adds the commit-cell header dep for the L-2 future-expiry check, and broadcasts. No sequencer, no ConfigCell, no predecessor-owner signature.

Run

Node 24 (native TypeScript, no build step). npm install, then:

npm test                              # codec drift tests (offline), incl. v2 + reverse-record
node examples/resolve.ts alice.cell   # live resolve against testnet
node examples/manage.ts               # full write lifecycle (live)
node examples/delegate.ts [label]     # manager delegation + primary name (live)

examples/delegate.ts reads a delegated-manager key from CELLS_MANAGER_KEY or CELLS_MANAGER_KEY_FILE (default .testnet/manager.key).