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

@locuschain/lib

v0.2.1

Published

> 한국어: [README.ko.md](./README.ko.md)

Readme

@locuschain/lib

한국어: README.ko.md

Viem-style TypeScript client for Locus Chain. Provides typed RPC, dual-key account handling, contract calls, and post-quantum keystore helpers backed by the project's own WASM crypto.

Install

pnpm add @locuschain/lib
# or
npm i @locuschain/lib

Node ~22.19, ESM and CJS dual builds. Browser-compatible (no Node built-ins beyond fetch).

Quick start

import {
  createLocusPublicClient,
  createLocusWalletClient,
  keysToAccount,
  createKeystoreBundle,
  unlockKeystoreBundle,
} from '@locuschain/lib';
import { loadLocusWasm } from '@locuschain/lib/utils';

await loadLocusWasm();

// 1. Read-only client — `publicActions` + `debugPublicActions` already attached.
const reader = createLocusPublicClient({
  rpcUrl: 'https://node.locuschain.example/rpc',
});
const detail = await reader.getAccountDetail({ account: 'LC1xxx...' });

// 2. Build a dual-key account (in-memory keys; see "Account models" below).
const bundle = createKeystoreBundle({
  passwordMaster: 'pw-master',
  passwordNormal: 'pw-normal',
  algoMaster: 'FALCON+ED25519',
  algoNormal: 'ED25519',
});
const { address, msk, mpk, nsk, npk } = unlockKeystoreBundle({
  ksJsonBundle: bundle,
  passwordMaster: 'pw-master',
  passwordNormal: 'pw-normal',
});
const account = keysToAccount({ address, msk, mpk, nsk, npk });

// 3. Wallet client — `walletActions` + `debugWalletActions` already attached.
//    Every write RPC runs the prepare → sign → submit pipeline internally.
const wallet = createLocusWalletClient({
  rpcUrl: 'https://node.locuschain.example/rpc',
  account,
});

await wallet.transferCoin({
  from: address,
  to: 'LC1yyy...',
  amount: '1000',
  sign: '',
  signedHeight: 0,
  feeType: 0,
});

Bare createPublicClient / createWalletClient + manual .extend(...) is still available when you want to customize the action set; see "Clients" below.

API surface

| Subpath | Exports | |---|---| | @locuschain/lib | Root barrel — re-exports clients, transports, accounts, errors, contracts, keystore helpers, primitive types | | @locuschain/lib/clients | createClient, createPublicClient, createWalletClient, createLocusPublicClient, createLocusWalletClient, Client, PublicClient, WalletClient, LocusPublicClient, LocusWalletClient | | @locuschain/lib/transports | http(url, config?), Transport, TransportInstance | | @locuschain/lib/accounts | keysToAccount, toAccount, mnemonicToAccount, localKeySource, toKeySource, KeySource, DualKeyAccount | | @locuschain/lib/errors | BaseError, RpcRequestError, RpcResponseError, HttpRequestError, TimeoutError, AbortedError, AccountRequiredError, InvalidTxResultError, SignFailedError, SignKeyBindUnsupportedError, DegenerateKeyError | | @locuschain/lib/contracts | getContract({ address, abi, client }), erc20Abi | | @locuschain/lib/autogen | publicActions, walletActions, debugPublicActions, debugWalletActions, every generated per-method action (e.g. transferCoin, getAccountDetail, ...), and every RPC schema type | | @locuschain/lib/utils | WASM bindings (sign, signByMasterKey, signKeyBind, loadLocusWasm, ...), keystore helpers (createKeystoreBundle, unlockKeystoreBundle, ...), address utilities | | @locuschain/lib/types | Hex, Hash, Address, and every autogen domain type | | @locuschain/lib/constant | TxType, TxTypes, Currency, AddressClass, FeeType |

Clients

There are two layers of client constructors:

| Constructor | Use when | |---|---| | createLocusPublicClient({ rpcUrl, transport? }) | You want a read-only client with the standard Locus action set (publicActions + debugPublicActions) pre-attached. Returns a LocusPublicClient. | | createLocusWalletClient({ rpcUrl, account, transport? }) | You want a write client with the standard write action set (walletActions + debugWalletActions) pre-attached and a DualKeyAccount bound. Returns a LocusWalletClient. | | createPublicClient({ transport }) / createWalletClient({ transport, account }) | You want to attach a custom action set yourself via .extend(...). |

import { createLocusPublicClient, createLocusWalletClient } from '@locuschain/lib';

const reader = createLocusPublicClient({
  rpcUrl: 'https://node.example/rpc',
  transport: { headers: { 'X-Trace-Id': 'abc' } },   // forwarded to http()
});
await reader.getNodeStatus();
await reader.getAccountDetail({ account });

const wallet = createLocusWalletClient({ rpcUrl: 'https://node.example/rpc', account });
await wallet.transferCoin({ from, to, amount: '1000', sign: '', signedHeight: 0, feeType: 0 });

The bare constructors are still useful when you want to pick a subset of actions or stack debug / custom helpers explicitly:

import { createPublicClient, http } from '@locuschain/lib';
import { publicActions, debugPublicActions } from '@locuschain/lib/autogen';

const client = createPublicClient({ transport: http(url) });
const c2 = client.extend(publicActions).extend(debugPublicActions);
await c2.getNodeStatus();
await c2.allHeights();

request is a thin JSON-RPC call. Per-method actions wrap it with typed args; you rarely need request directly.

// Equivalent of c2.getNodeStatus()
const status = await client.request<RpcGetNodeStatusResult>({
  method: 'locus_getNodeStatus',
  params: [],
});

Transports

import { http } from '@locuschain/lib';

const transport = http('https://node.example/rpc', {
  timeout: 30_000,                            // default 30s
  headers: { 'X-Trace-Id': '...' },           // merged into every request
  fetchOptions: { keepalive: true },          // forwarded to fetch
});

Errors thrown by the http transport are subclasses of BaseError: HttpRequestError, TimeoutError, AbortedError, RpcResponseError.

Account models

keysToAccount — in-memory plain keys

Suitable for dev, CLI tools, or service-side daemons that hold key material in process memory.

const account = keysToAccount({ address, msk, mpk, nsk, npk });

mnemonicToAccount — BIP-39 derivation

One-call shortcut that turns a mnemonic into a sign-ready DualKeyAccount. The derived secret keys live in the returned localKeySource closures only — the library never re-exposes them.

const account = mnemonicToAccount({
  mnemonic: 'word1 word2 ...',
  algoMaster: 'FALCON+ED25519',
  algoNormal: 'ED25519',
  index: 0,
});
// or:
const account = mnemonicToAccount({
  mnemonic: '...',
  path: "m/4403'/190301'/0'/0'/0'",
});

Use this when you need to sign a one-off TX from a mnemonic and discard the keys afterwards — CLI scripts, end-to-end tests, a dApp's "paste mnemonic, send one TX" flow.

Do NOT use this when you need the raw { msk, mpk, nsk, npk } — for example, when a wallet wants to persist the keypair in its own storage, derive a keystore JSON, or pre-compute a keyBind.keySign for later open-account flows. mnemonicToAccount returns a DualKeyAccount whose master / normal are KeySource objects with no secret-key accessor by design (see KeySource / localKeySource — the key argument is captured in a closure and never re-exposed).

For that case, call the lower-level WASM helper directly and reshape the JSON yourself:

import { deriveKeysFromMnemonic } from '@locuschain/lib/utils';
import { keysToAccount } from '@locuschain/lib';

const k = JSON.parse(deriveKeysFromMnemonic({
  mnemonic, algoMaster, algoNormal, index: 0,
}));
// k = { address, masterSecretKey, masterPublicKey, secretKey, publicKey, ... }

// Persist `k.*` in your own wallet storage, derive keystores, etc.
// Only when you need a sign-ready account again:
const account = keysToAccount({
  address: k.address,
  msk: k.masterSecretKey, mpk: k.masterPublicKey,
  nsk: k.secretKey,       npk: k.publicKey,
});

This separation is deliberate — mnemonicToAccount is the viem-style "derive and forget" API, deriveKeysFromMnemonic is the raw-material API for wallet builders who must own the key bytes.

toAccount({ master, normal }) — callback-driven (keystore / hardware / external wallet)

This is the path for production wallets that don't want to give the library raw secret keys. Provide two KeySource callback objects via toKeySource(...); the library calls them and never sees the secret.

import { toAccount, toKeySource } from '@locuschain/lib';

const master = toKeySource({
  type: 'ledger',
  getPublicKey: () => ledger.getPublicKey(),
  signMessage: ({ message }) => ledger.sign(message),
  signKeyBind: ({ newNpk, currentNpk }) => ledger.signKeyBind(newNpk, currentNpk),
});

const normal = toKeySource({
  type: 'keystore',
  getPublicKey: () => Promise.resolve(npk),
  signMessage: async ({ message }) => {
    const password = await promptUser();
    const sk = await loadNormalKeystore({ passStr: password, ksJson });
    return sign({ sk, message });
  },
});

const account = toAccount({ address, master, normal });

Mix freely — master from hardware wallet + normal from keystore is fine. The library doesn't care.

Keystore helpers

For the bundled keystore format that the project's WASM emits:

import {
  createKeystoreBundle,
  splitKeystoreBundle,
  unlockMasterKeystore,
  unlockNormalKeystore,
  unlockKeystoreBundle,
} from '@locuschain/lib';

const bundle = createKeystoreBundle({
  passwordMaster, passwordNormal,
  algoMaster: 'FALCON+ED25519',
  algoNormal: 'ED25519',
});
// bundle.raw           — persistable JSON
// bundle.masterKsJson  — master half only
// bundle.normalKsJson  — normal half only

const keys = unlockKeystoreBundle({
  ksJsonBundle: bundle,           // or the raw JSON string
  passwordMaster, passwordNormal,
});
// keys = { address, msk, mpk, nsk, npk }

// Individually if needed:
const master = unlockMasterKeystore({ ksJson: bundle.masterKsJson, password: passwordMaster });
const normal = unlockNormalKeystore({ ksJson: bundle.normalKsJson, password: passwordNormal });

Recommended algorithms

| Slot | Recommended | Notes | |---|---|---| | master | FALCON+ED25519 | Default when algoMaster is left empty. PQ-safe combined signer. | | normal | ED25519 | Default when algoNormal is left empty. Fast, deterministic, sufficient for per-tx signing. |

mnemonicToAccount and unlockKeystoreBundle defensively detect the case where the WASM returns identical master and normal public keys and throw DegenerateKeyError. This guard is a regression net — an earlier JS-bridge bug (now fixed) used to drop the deterministic seed when forwarding it to WASM, causing keys to repeat across slots for AIMER / HAETAE pairs. The bridge fix is in place, so the guard should never fire under normal operation; if it does, it indicates a new regression on the JS or WASM side rather than an algorithm-level limitation.

Locus dual-key transaction model

Every TX is signed by one of the two keys depending on tx.type. The library handles the routing inside dualKeyAccount.signTransaction:

| TX type | tx main signature | extra master attestation | |---|---|---| | TX_CLOSE_ACCOUNT | master | — | | TX_OPEN_ACCOUNT | normal | keySignmaster.signKeyBind(newNpk = pk, currentNpk = "") | | TX_CHANGE_KEYPAIR | new normal | signByMasterKeymaster.signKeyBind(newNpk = newNormalPkey, currentNpk = old normal pubkey) | | TX_CHANGE_VKEY | normal | — (newValidationPkey is part of the prepare param) | | everything else | normal | — |

For TX_OPEN_ACCOUNT the master attestation must be present on the prepare RPC too — the node validates it before returning the tx hash. dualKeyAccount.prepareParams fills keySign in automatically when the caller passes an empty string. On the submit RPC the library reuses the prepare-stage signature read back from prepared.tx.keyBind.sign — this matters because FALCON+ED25519 is non-deterministic, so a re-signed value would diverge from what the node already accepted.

Changing the normal key

TX_CHANGE_KEYPAIR is not auto-filled. The node verifies the main tx signature against the new normal pubkey (proof-of-possession), and signByMasterKey needs the old normal pubkey as currentNpk — those are two different keys, so the auto-pipeline can't infer both from a single bound account. The caller drives the flow explicitly:

import { keysToAccount, createWalletClient, http } from '@locuschain/lib';
import { walletActions } from '@locuschain/lib/autogen';
import { createNormalKey } from '@locuschain/lib/utils';

// 1. Build the OLD account and compute signByMasterKey on it.
const oldAccount = keysToAccount({ address, msk, mpk, nsk: nsk1, npk: npk1 });
const newKey = JSON.parse(createNormalKey({ addrStr: address, keyAlgo: 'ED25519' }));
const { nsecretKey: nsk2, npublicKey: npk2 } = newKey;

const signByMasterKey = await oldAccount.master.signKeyBind({
  newNpk: npk2,
  currentNpk: npk1,
});

// 2. Bind the NEW normal key into a fresh account.
const newAccount = keysToAccount({ address, msk, mpk, nsk: nsk2, npk: npk2 });
const wallet = createWalletClient({ transport: http(url), account: newAccount })
  .extend(walletActions);

// 3. Submit. `signByMasterKey` is non-empty so prepareParams passes through;
//    the main tx is signed by newAccount.normal (= nsk2), which matches the
//    NewKey field the node will verify against.
await wallet.changeKey({
  account: address,
  masterPkey: mpk,
  newNormalPkey: npk2,
  signByMasterKey,
  sign: '',
  signedHeight: 0,
});

Calling wallet.changeKey({ ..., signByMasterKey: '' }) raises a BaseError pointing at this section. The library will not silently compute a signature that the node would reject on submit.

Contracts

import { getContract, erc20Abi } from '@locuschain/lib';

const token = getContract({
  address: 'LC1xxxTOKEN',
  abi: erc20Abi,
  client: reader,
});

const symbol  = await token.read.symbol();
const dec     = await token.read.decimals();
const balance = await token.read.balanceOf([userAddress]);

Only view / pure methods are exposed via read.*. To call a state-changing contract method use the autogen callContract action on a wallet client:

import { callContract } from '@locuschain/lib/autogen';

await callContract(wallet, {
  callerAccount: walletAddr,
  contractAccount: tokenAddr,
  /* fuelLimit / amount / tokenAmounts / func / argData / feeType */
  ...
});

Wallet integration

This section captures the pattern used by locus-wallet-extension — the canonical reference consumer of @locuschain/lib. Same shape applies to any wallet that wants to keep secret material out of the library while still using the typed action surface.

The wallet wraps two pieces from the lib:

  1. toAccount({ address, master, normal }) — builds a DualKeyAccount from two KeySource callback objects. The lib never sees the raw secret key; it just calls getPublicKey / signMessage / (signKeyBind on master) when it needs them.
  2. createLocusWalletClient({ rpcUrl, account }) — binds that account to a write client. Every TX method then runs prepare → sign → submit, routing to the right slot by txKeyPolicy (see "Locus dual-key transaction model" above).

Lazy KeySource per slot

Build a KeySource for each slot, in priority order. The lib only calls the callbacks at sign time, so a slot can stay unfilled (or filled with a stub that throws on use) until a TX actually requires it.

import { toAccount, toKeySource, localKeySource } from '@locuschain/lib';
import { loadMasterKeystore, sign as wasmSign, signByMasterKey, signKeyBind } from '@locuschain/lib/utils';

function buildAccount(opts: {
  address: string;
  mpub: string;
  npub: string;
  npriv?: string;                                  // memory-resident normal sk
  masterKeystore?: { json: string; password: string };  // unlocked on demand
  hardware?: { signMessage(args): Promise<string>; ... };
}) {
  // Master slot — memory secret? keystore? hardware? otherwise a stub
  // that throws a marker error so the wallet UI can prompt for input.
  const master = opts.hardware
    ? toKeySource({ type: 'hw-master', ...opts.hardware })
    : opts.masterKeystore
      ? toKeySource({
          type: 'keystore-master',
          getPublicKey: () => Promise.resolve(opts.mpub),
          signMessage: ({ message }) => unlock(opts.masterKeystore!).then(({ msk }) =>
            signByMasterKey({ msk, message })),
          signKeyBind: ({ newNpk, currentNpk }) => unlock(opts.masterKeystore!).then(({ msk }) =>
            signKeyBind({ msk, newNpk, currentNpk })),
        })
      : toKeySource({
          type: 'require-master',
          getPublicKey: () => Promise.resolve(opts.mpub),
          signMessage: () => { throw markerError('requireMasterKey'); },
          signKeyBind:  () => { throw markerError('requireMasterKey'); },
        });

  // Normal slot — usually memory-resident, falling back the same way.
  const normal = opts.npriv
    ? localKeySource({ key: opts.npriv, publicKey: opts.npub, role: 'normal' })
    : toKeySource({
        type: 'require-normal',
        getPublicKey: () => Promise.resolve(opts.npub),
        signMessage: () => { throw markerError('requireNormalKey'); },
      });

  return toAccount({ address: opts.address, master, normal });
}

The unlock(...) helper memoizes its inner WASM call (loadMasterKeystore{ msk, mpk }) inside the closure so a TX that triggers signMessage and signKeyBind only asks the user for the password once.

Signing pipeline at runtime

Once the account is bound, the wallet pipeline collapses to a single typed call. The library decides per-TX which slot to invoke:

import { createLocusWalletClient } from '@locuschain/lib';

const account = buildAccount({ ... });                     // lazy slots
const wallet  = createLocusWalletClient({ rpcUrl, account });

// TX_CLOSE_ACCOUNT — main sign is *master*. If the master slot is the
// require-stub above, the throw fires *here*, the wallet UI catches the
// marker, asks for the keystore + password, rebuilds the account with the
// keystore-backed master KeySource, and retries.
await wallet.closeAccount({ from, to, mpkey, sign: '', signedHeight: 0 });

// TX_OPEN_ACCOUNT — main sign is normal; master.signKeyBind is needed for
// `keySign`. The lib auto-fills `keySign` on the prepare RPC when the
// caller passes ''.
await wallet.openAccount({ account: addr, pk: npub, mpk: mpub, keySign: '', ... });

// transferCoin / callContract / ... — main sign is normal only.
await wallet.transferCoin({ from, to, amount: '1000', sign: '', signedHeight: 0, feeType: 0 });

For TX_CHANGE_KEYPAIR the lib does not auto-fill signByMasterKey — the caller has to drive the old/new key flow explicitly. See "Changing the normal key" above for the canonical pattern.

Surfacing "key required" to the UI

The recommended pattern when a slot can't sign yet:

  1. The KeySource.signMessage callback throws an error tagged with a marker (e.g. { requireMasterKey: true } / { requireNormalKey: true }).
  2. The wallet UI catches the marker, shows a "enter master keystore" / "enter normal keystore or secure key" modal.
  3. The user submits the keystore + password; the wallet rebuilds the account with a non-stub KeySource for that slot and retries the TX.

Because the lib never holds the secret, the only state to "rebuild" is the account object — the wallet client itself is cheap (no network state, just { transport, account, ...actions }).

Why not just pass plain keys?

Three reasons that surface during real wallet integration:

  • Multiple secret stores per account — master may be in a keystore while normal is memory-resident, or master could be on hardware. The callback boundary lets each slot evolve independently.
  • Non-deterministic signersFALCON+ED25519 produces different signatures for the same message. The lib reuses the prepare-stage signature on submit (see keyBind.sign reuse below). Going through callbacks instead of cached secrets makes that ordering explicit.
  • Just-in-time unlock — keystores cost a WASM round trip. Doing it lazily and caching the result inside the closure keeps password prompts to one per TX even if the lib signs twice.

Errors

Every error thrown by the lib is a subclass of BaseError:

import { BaseError, RpcResponseError, TimeoutError } from '@locuschain/lib';

try {
  await wallet.transferCoin({ ... });
} catch (err) {
  if (err instanceof TimeoutError) ...
  if (err instanceof RpcResponseError) console.error(err.code, err.shortMessage);
  if (err instanceof BaseError) console.error(err.metaMessages.join('\n'));
}

Project layout

  • src/clients, src/transports, src/accounts, src/errors, src/contracts, src/types — hand-written.
  • src/actions/wallet/ — internal 2-step pipeline primitives (_prepareTransaction, _signLocusTransaction, _sendPreparedTransaction).
  • src/utils/ — WASM bindings and helpers (mnemonic, address, keystore).
  • src/autogen/ — code-generator output. Do not edit manually. Regenerate with pnpm run generate-code (requires Docker; see generate-code.sh).