nockchain-agent-sdk
Typed TypeScript client for reading Nockchain.
Built by Prism. Not affiliated with, endorsed by, or operated by Nockchain, Nock Community Co, Zorp, or National Compute.
npm install nockchain-agent-sdk
Node 20 or newer. The package is ESM only.
Client
import { NockchainClient } from "nockchain-agent-sdk";
const client = new NockchainClient({
// Every option is optional. This is the default pool, probed and ranked by freshness.
endpoints: [
{ host: "api.nockscan.net", port: 443, tls: true, label: "nockscan" },
{ host: "23.252.122.18", port: 5556, tls: false, label: "public-node" },
{ host: "rpc.nockbox.org", port: 443, tls: true, label: "nockbox" },
],
maxStalenessSeconds: 900,
maxHeightLag: 3,
});
The client probes the pool before it answers and sticks with the endpoint it settled on until that probe expires, failing over on error. When nothing in the pool is fresh it throws StaleChainError naming the ages it measured, so a stale reading never reaches your code unannounced. Set allowStale: true if hours-old data is acceptable for your case.
Call client.close() when you are done to release the gRPC channels.
Balances and notes
| method | returns |
|---|---|
getBalance(address) |
{ nicks, nock, notes, tipHeight, blockId } |
getNotes(address) |
the unspent notes behind that balance, with the snapshot height |
getNotesByFirstName(firstName, lockKind) |
notes under one selector |
firstNames(address) |
the two selectors an address resolves to |
The balance is derived from the note first-names rather than the address selector, which is the only path that reports v1 notes correctly. A payout can land under either the simple lock or the coinbase lock, so both are queried and the results are unioned. The two queries are independent reads and can land on different snapshots, so the lower one wins: tipHeight and blockId always name the same block, and confirmations are counted from it.
Amounts are bigint nicks. One NOCK is 65536 nicks, and nicksToNock and nockToNicks convert exactly. An amount finer than one nick raises InvalidAmountError rather than rounding.
Payments
const result = await client.verifyPayment({
address: invoiceAddress,
minNock: "1.5",
sinceHeight: 133400,
confirmations: 3,
});
verifyPayment returns { paid, requiredNicks, receivedNicks, confirmedNicks, confirmedNock, notes, confirmedNotes, confirmations, tipHeight, sinceHeight }. waitForPayment takes the same arguments plus timeoutMs, pollMs, an AbortSignal, and an onPoll callback, and polls until the answer settles or the deadline passes. A read that fails mid-wait is retried on the next tick, so an endpoint dropping out does not end a wait measured in minutes; the result reports those retries as failedPolls.
Pass sinceTime instead of sinceHeight and it is resolved to a block height for you. The window is inclusive of that block: a payment mined in the first block at or after the given moment counts, which is what an invoice issued at that moment needs. The explicit sinceHeight option keeps its exclusive meaning of "above this height".
Nothing the wallet sends carries a memo or tag on the wire, so correlate payments by issuing one address per invoice. An empty balance is not proof that an address is real: the chain indexes notes by name and answers a name it has never seen with the same empty set as an address nobody has paid yet.
Chain data
| method | returns |
|---|---|
getTip() |
height, block ID, and timestamp at the selected endpoint |
getBlocks({ limit, pageToken }) |
a page of blocks, newest first |
getBlockDetails(heightOrId) |
one block with its transactions |
getTransactionBlock(txId) |
where a transaction landed |
getTransactionDetails(txId) |
transaction contents, or a typed fallback |
transactionAccepted(txId) |
whether the node has accepted a submission |
getMetrics() |
explorer metrics |
getPeerStats() |
peer counts and connection state |
health(force?) |
the endpoint table: height, refresh age, latency, verdict |
pickEndpoint() |
the endpoint the pool would use right now |
getTransactionDetails never throws the upstream decoder failure that post-Logos transactions trigger. It returns { ok: false, reason: "upstream_decoder_bug", txId, fallback } instead, where fallback carries the inclusion facts still available from the block. fallback.included is true in a block, false still in the mempool, and null when the inclusion lookup itself failed and fallback.lookupError says why. Treat null as an answer to retry, never as proof the transaction is missing.
Sending
sendTransaction({ txId, rawTransaction }) submits a payload that the official nockchain-wallet CLI has already produced. No signing library for Nockchain exists, so this package cannot build or sign a transaction, and a helper that would imply otherwise raises UnsupportedOperationError.
Errors
Every failure is a NockchainError subclass carrying a stable code. Branch on the code rather than on the message text.
| class | code |
raised when |
|---|---|---|
StaleChainError |
stale_chain |
endpoints answered, none was fresh enough to read |
EndpointUnavailableError |
endpoint_unavailable |
nothing in the pool answered |
RpcError |
rpc_failed |
the call failed at the transport or gRPC status level |
ChainStatusError |
chain_error |
the node answered with an error status of its own |
InvalidResponseError |
invalid_response |
the response did not match the vendored protobuf shape |
InvalidAddressError |
invalid_address |
an address could not be turned into note first-names |
InvalidAmountError |
invalid_amount |
an amount is finer than one nick |
PaginationLimitError |
pagination_limit |
a listing ran past maxPages |
PaginationStalledError |
pagination_stalled |
a node repeated a page cursor, so the listing never ends |
UnsupportedOperationError |
unsupported |
the operation needs transaction signing |
InvalidAddressError and PaginationLimitError both reach getBalance, getNotes and verifyPayment: the first for a malformed address, the second for a mining address holding more notes than the page budget covers. A balance read that cannot finish raises rather than returning the notes it managed to collect.
Limits
A balance is the unspent-note set at the current tip. A spent note leaves it, so the total answers what an address can spend now and says nothing about its history. The public API is alpha and unauthenticated, and its endpoints move without notice.
Full background and measurements are in the repository README.
License
Apache-2.0.