🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@zkcoins/sdk

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@zkcoins/sdk

Pure-TypeScript wallet SDK for zkCoins — BIP-39/32 derivation, Schnorr signing, typed REST client, high-level account adapter.

latest
Source
npmnpm
Version
0.4.0
Version published
Weekly downloads
5
-54.55%
Maintainers
1
Weekly downloads
 
Created
Source

@zkcoins/sdk

Pure-TypeScript wallet SDK for zkCoins. One package covers BIP-39 / BIP-32 derivation, BIP-340 Schnorr signing, the typed REST client for the Jobs API (/api/jobs/*), and a high-level account adapter that wallet integrators (Cake Wallet, Layerz Wallet, the in-tree web app) consume as a drop-in InterfaceAccountBasedWallet-style API.

Status: v0.3.0 — complete node-endpoint coverage (service/health probes, address list, inscription lookup, a receive polling helper). See CONTRIBUTING.md for the workflow and CHANGELOG.md for the change log.

Why pure TypeScript

Earlier iterations of the wallet primitives were compiled to WASM from zk-coins/app/rust/client/. That works in the browser but creates friction for every other consumer — React Native (Layerz Wallet) struggles to bundle WASM cleanly, and Cake Wallet (Dart) cannot consume a WASM blob at all. The functions involved are all standard BIP-39 / BIP-32 / secp256k1 Schnorr / SHA-256 — every audited pure-JS library can do them. @zkcoins/sdk is the pure-JS replacement, so the same library runs identically in Node 22+, the browser, and React Native.

Install

npm install @zkcoins/sdk

Quick start

import { ZkCoinsAccount, generateMnemonic } from '@zkcoins/sdk';

// 1. Create or restore an account. With no `apiUrl` passed the SDK
//    talks to `https://api.zkcoins.app`; pass `{ apiUrl: '...' }`
//    to point at any other node — see "Choosing a node" below.
const mnemonic = await generateMnemonic();
const account = await ZkCoinsAccount.fromMnemonic(mnemonic, /* accountIndex */ 0);

// 2. Read authoritative state from the server.
const { balance, username, num_sends } = await account.getBalance();
console.warn('balance:', balance, 'sats; username:', username, '; sends:', num_sends);

// 3. Mint (DEV faucet / authorised issuance). Mint is fully
//    server-mediated — the SDK admits the job and polls it to
//    completion for you. Throws `JobFailedError` if the job fails.
const mint = await account.mint(/* amountSats */ 10_000);
console.warn('mint proof id:', mint.proofId);

// 4. Send. `pay()` runs the whole send → commit lifecycle:
//    refresh balance → sign → admit job → poll to awaiting_signature
//    → sign the commitment → commit → poll to completed.
const result = await account.pay(/* recipient */ recipientHex, /* amountSats */ 5_000);
console.warn('send proof id:', result.proofId);

// 5. History.
const { items, total } = await account.getTransactions({ limit: 50 });
console.warn(`${items.length} of ${total} transactions`);

The Jobs API

Mint / send / commit are asynchronous jobs on the node (/api/jobs/*); the old synchronous /api/{mint,send,commit} routes were removed node-side. A job moves through:

queued → proving → [awaiting_signature → broadcasting] → completed | failed | cancelled

ZkCoinsAccount.mint() / .pay() drive the whole lifecycle and poll for you. If you compose the lower-level ZkCoinsClient directly, the building blocks are:

const idem = newIdempotencyKey(); // reuse across retries of one logical op

// Admit (Idempotency-Key is mandatory on mint/send).
const { job_id } = await client.mintJob({ account_address, amount }, idem);

// Poll (respects the node's Retry-After backoff).
const { status, retryAfterMs } = await client.getJobWithRetry(job_id);

// Or stream transitions over SSE (falls back to polling where SSE
// is unavailable). The generator ends after the terminal frame.
for await (const frame of client.streamJob(job_id)) {
  console.warn(frame.status, frame.phase);
}

// Send-only: attach the wallet-signed commitment once the job parks
// in awaiting_signature, then poll to completed.
await client.commitJob(job_id, { proof_id, public_key, signature, message });

// Cancel a still-queued job.
await client.cancelJob(job_id);

asset_id (multi-asset)

mint(amountSats, assetId?) and pay(recipient, amountSats, assetId?) take an optional 32-byte-hex asset_id. Omit it for the native asset. A present-but-malformed value is rejected by the node (422) — there is no silent fallback to native. Gate the UI on info.capabilities.multi_asset.

bitcoin_network

info() returns a typed bitcoin_network ('mainnet' | 'mutinynet') for behaviour switches — prefer it over the free-text, operator-overridable network label. It is optional for forward/backward compatibility: a node that predates it simply omits the field, and clients fall back to matching network.

Choosing a node

@zkcoins/sdk is a protocol SDK, not a service SDK. The constructor parameter is how you tell the SDK which node to talk to:

new ZkCoinsClient({ apiUrl: 'https://...' });
ZkCoinsAccount.fromMnemonic(mnemonic, 0, { apiUrl: 'https://...' });

When apiUrl is omitted, the SDK falls back to https://api.zkcoins.app. The SDK does not read environment variables, config files, or any other ambient state — where you source the URL from (env, config-file, hardcoded, CLI arg) is your app's concern.

zkcoins.app is one such operator (one of hopefully many). It runs two public stages today:

URLBitcoin networkNotes
https://api.zkcoins.appMainnetProduction. No faucet — mint requires real on-chain funding.
https://dev-api.zkcoins.appMutinynetDEV. Open mint faucet, signet-grade reorgs, no real value.

Self-hosters point apiUrl at their own node (zk-coins/node docker image). Wallet integrators typically expose a chooser in their own config; the SDK stays opinion-free on which node is "the right one".

API surface

The ZkCoinsAccount class is the recommended entry point:

class ZkCoinsAccount {
  static fromMnemonic(
    mnemonic: string,
    accountIndex: number,
    opts?: { apiUrl?: string; passphrase?: string },
  ): Promise<ZkCoinsAccount>;

  readonly address: string;
  readonly client: ZkCoinsClient;

  getBalance(): Promise<{ balance: number; username?: string; num_sends: number }>;
  mint(amountSats: number, assetId?: string): Promise<MintResult>;
  pay(recipient: string, amountSats: number, assetId?: string): Promise<PayResult>;
  getTransactions(opts?: HistoryOpts): Promise<HistoryResponse>;
  // Receiving: share `address`, then poll for the credit (see "Receiving").
  waitForIncoming(opts?: WaitForIncomingOpts): Promise<BalanceResponse>;
  waitForJob(
    jobId: string,
    stopAt: ReadonlySet<JobStatus['status']>,
    opts?: WaitForJobOpts,
  ): Promise<JobStatus>;
  claimUsername(username: string): Promise<UsernameResponse>;
  resolveUsername(username: string): Promise<UsernameResponse>;

  getNumPubkeys(): number;
  setNumPubkeys(value: number): void; // e.g. setNumPubkeys(balance.num_sends)
}

ZkCoinsClient — full endpoint reference

The lower-level ZkCoinsClient (also account.client) mirrors every node endpoint a wallet legitimately reads or drives. Each method validates the response against a Zod schema and maps a non-2xx onto ApiError (no silent fallback).

MethodNode routeReturns
root()GET /RootResponse
health()GET /healthstring ("ok")
ready()GET /health/readyReadyResponse¹
publisherHealth()GET /health/publisherPublisherHealthResponse
info()GET /api/infoInfoResponse
balance(address)GET /api/balanceBalanceResponse
history(address, opts?)GET /api/historyHistoryResponse
addresses()GET /api/address²AddressesResponse
inscription(txid)GET /api/inscriptions/:txidInscriptionSummary
resolveUsername(name)GET /api/username/resolve/:uResolveUsernameResponse
claimUsername(signedReq)POST /api/username/claim²ClaimUsernameResponse
mintJob(req, idem)POST /api/jobs/mintJobAccepted
sendJob(signedReq, idem)POST /api/jobs/sendJobAccepted
getJob(id) / getJobWithRetry(id)GET /api/jobs/:idJobStatus (+ retry)
commitJob(id, req)POST /api/jobs/:id/commitvoid
cancelJob(id)POST /api/jobs/:id/cancelvoid
streamJob(id)GET /api/jobs/:id/stream (SSE)AsyncGenerator<JobStatus>

¹ ready() returns the body for both the 200 (ready) and 503 (not-ready) branches — a not-ready node is a valid readiness answer, not a transport error. Read result.ready / result.failures. Any other non-2xx still throws ApiError. ² Feature-gated node-side (address-list / username-claim Cargo features). Gate the call on info().capabilities.address_list / .username_claim; a build without the feature 404s the route.

Deliberately not exposed:

  • GET /api/proof/:id returns a binary bincode CoinProof blob. A pure-TS SDK does not decode bincode, and the wallet does not need to: it reads account_state_hash / output_coins_root from the awaiting_signature job result to build the commitment, never from the proof blob. Mirroring this as a method would require either a bincode decoder or a fake — neither is acceptable, so it is omitted.
  • POST /api/receive is a legacy octet-stream route, not a wallet operation — see Receiving below.
  • GET /api/admin/r2-probe/history is an operator telemetry endpoint, out of wallet-SDK scope.

Other building blocks are exported too (custom signing flows, key-derivation helpers, every Zod schema + typed error, and newIdempotencyKey).

Receiving

There is no client-initiated "receive" call, by design. A zkCoins account receives by sharing its address — the sender's pay() credits it server-side. (The node's POST /api/receive is internal plumbing, not a wallet method, and is intentionally not mirrored here.)

To observe an incoming credit, poll the authoritative balance / history:

// Share this with the sender:
console.warn('my address:', account.address);

// Option A — your own loop over getBalance() / getTransactions().
// Option B — the built-in convenience helper (pure getBalance() on a
// timer; no signing, no extra trust assumption):
const updated = await account.waitForIncoming({ timeoutMs: 120_000 });
console.warn('received! new balance:', updated.balance);

waitForIncoming reads the current balance as a baseline (or takes an explicit fromBalance), then polls until the balance rises above it, resolving with the updated BalanceResponse. It throws on timeout rather than returning a stale balance.

Fees

zkCoins has no client-side fee estimation, and the SDK fabricates none. The node exposes no fee-estimation endpoint:

  • Mint amounts are server-controlled (DEV faucet / authorised issuance).
  • Send / inscription fees are paid server-side from the operator-funded publisher wallet — the wallet never constructs or signs a Bitcoin fee.

The only fee-relevant figure the node surfaces is the publisher wallet's UTXO state via client.publisherHealth() ({ address, utxo_count, total_sats }); a depleting total_sats is the observable fee-spend signal. There is deliberately no fees() method — adding one would mean inventing a number the node does not provide.

Compatibility

  • Node 22+ (no native dependencies; runs in Bun, Deno, AWS Lambda, etc.).
  • All evergreen browsers (Web Crypto + standard fetch + ReadableStream for SSE).
  • React Native (Expo + bare workflow). newIdempotencyKey() uses @noble/hashes randomBytes, not crypto.randomUUID, so it works where the latter is absent.

Wallet integrator notes

  • Thin-client invariant. Before every signed request the server is the source of truth. ZkCoinsAccount.pay() enforces this by calling getBalance() first internally — replicate that if you build a flow on the lower-level client.
  • No local state. The SDK persists nothing. Mnemonic storage, address book, transaction cache — all handled by the integrating wallet. Re-hydrate numPubkeys from balance().num_sends.
  • No silent fallback. A malformed response hard-fails at the schema boundary (ZodError); a failed/cancelled job throws JobFailedError.

License

MIT.

See also

Keywords

zkcoins

FAQs

Package last updated on 09 Jun 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts