Thanks for the interest. The SDK is a small library with strict guarantees, so the contribution rules lean conservative.
zkCoins follows the Bitcoin full-node model: your wallet trusts your node, exactly as a Bitcoin wallet trusts your own bitcoind. "Trusted node" means your node — never a third party. Running your own node is the trustless, private path, and it is the model the whole system is designed around. The node↔wallet split is packaging (a heavy validator process vs. a thin key-holder), not a trust boundary. The only line the node never crosses is the wallet's private key — that stays in the wallet.
This is a hard project rule. It shapes every design and implementation decision:
- Self-hosting gives you trustlessness and privacy at once. Your own node verifies your transactions and sees your plaintext — and you are the operator, so nothing leaks. The wallet must always be able to switch to a different node by changing a single configuration value.
- Using someone else's node is a trade-off you choose, not a flaw. A public operator can never steal, forge, or double-spend your coins — that is enforced cryptographically (recursive proofs + Bitcoin-anchored nullifiers). What a foreign operator can see is your privacy, and it can affect liveness — the same spectrum as using an Electrum/SPV server instead of your own Bitcoin node.
- The thin wallet and SDK are not a compromise. No anti-node logic: no client-side proof verification, no scan loops, no view-key / spend-key splits, no consistency checks against a second node, no "node integrity" indicators in the UI. Trustlessness comes from running your own node, not from bolting verification onto a thin client. Anything that exists to reduce trust in the node belongs node-side — or the answer is self-hosting.
- The node is built so that self-hosting is easy. Single container, documented configuration, deterministic state, no operator-specific dependencies.
- The SDK and wallet stay thin. They expose seed + address + the small set of operations every familiar wallet SDK exposes. Integrators (Cake Wallet, LayerZ, BlueWallet, …) should be able to wire zkCoins up with the same effort as adding a second Bitcoin-family chain.
When in doubt about whether a feature belongs in the wallet, SDK, or node: if it exists to reduce trust in the node, build it node-side, or document self-hosting as the answer. This rule is mirrored verbatim in zk-coins/node, zk-coins/sdk, zk-coins/app, and zk-coins/docs.
@zkcoins/sdk is pure TypeScript — no WASM, no native modules, no environment-specific code paths. It must run identically in Node 22+, modern browsers, and React Native (Expo + bare workflow). Any change that breaks this is rejected on principle, even if it would simplify the SDK internally.
Cryptographic primitives come from audited upstream libraries:
@scure/bip39— BIP-39 mnemonic.@scure/bip32— BIP-32 HD derivation.@noble/curves/secp256k1— secp256k1 + Schnorr (BIP-340).@noble/hashes— SHA-256.
Do not roll your own crypto. If a primitive is missing, add it via these libraries (or escalate the discussion in an issue).
Documented exception — Poseidon-over-Goldilocks / E(·) / Hc. The SDK is the pure-TypeScript reference implementation of the client-side Poseidon surface (spec §§1.7 / V.4) under src/poseidon/. That module is deliberately in-tree (not an audited upstream crate), because no suitable pure-JS library matches the protocol's Goldilocks Poseidon + encoding. It is pinned against independent Rust-generated vectors (test/poseidon.test.ts) and is the sole rolled-crypto exception this repo accepts. Everything else — BIP-39/32, secp256k1/Schnorr, SHA-256 — still comes only from the upstream list above.
feature/* → develop → main
- Always open feature PRs against
develop. There is nostagingbranch in this repo — feature branches merge directly intodevelopafter review and the slim CI (lint, typecheck, build, test, cross-tests). mainis auto-PR'd fromdevelopbyauto-release-pr.yaml. The Release PR is what the maintainer merges to cut a release.- After
mainmerges, tag the version (git tag v0.1.0 && git push origin v0.1.0) — that triggerspublish.yaml, which runs the full gate one more time and publishes to npm with provenance. developandmainreject direct pushes. Hotfixes also go through a feature PR todevelop.- Never force-push, never amend a published commit, never squash on the agent side — the maintainer squashes at merge time if needed.
Every PR must pass:
- Lint + typecheck + build —
npm run lint && npm run typecheck && npm run build.dist/must be produced (ESM + CJS +.d.ts). - Unit tests with 100 % coverage gate —
npm run test:coverage. Vitest enforceslines: 100, functions: 100, statements: 100, branches: 100onsrc/*(excludingsrc/index.tsandsrc/types.tswhich are pure re-exports). A structurally unreachable sub-expression may carry a justified/* v8 ignore next -- <reason> */immediately before that sub-expression only. ~2^-256 hash/scalar edges without a production injection seam belong in this category (already documented in source attweakScalarOrRejectandhalfAgg.ts); do not introduce a hasher seam. Do not use a file-level ignore for reachable logic. - Cross-tests —
npm run test:cross. Spawns the Rust shim intest/cross-rust/shim/(built withcargo build --lockedagainst the checked-inCargo.lock) and compares JS vs Rust output for 100 randomized inputs per cryptographic primitive, including 100 independent half-aggregation batches. A green cross-test is the single load-bearing guarantee that the pure-JS reimpl matches the protocol spec encoded inzk-coins/app/rust/client/.
CI runs all three on every PR. Drafts skip CI by convention (re-enable by marking ready-for-review).
The shim at test/cross-rust/shim/ is a JSON-stdin → JSON-stdout CLI mirroring every cryptographic function from the SDK. The Rust dependencies (bitcoin, secp256k1, sha2, hex, serde_json) match those used by zk-coins/app/rust/client/, so the shim's behaviour is byte-equivalent to the app's WASM source.
When you add a new primitive to src/derivation.ts or src/signing.ts:
- Add the matching dispatch arm in
test/cross-rust/shim/src/main.rs. - Add a generator + assertion block in
test/cross-rust/cross.test.ts. - Run
npm run test:crosslocally before pushing — CI will re-run, but feedback loop is faster locally.
If a future primitive cannot be reasonably cross-tested (e.g. it's a JS-only convenience over already-tested primitives), document the omission in test/cross-rust/README.md.
- Prettier 3 with
singleQuote: true, trailingComma: 'all', printWidth: 100. Runnpm run lint:fixto apply. - ESLint with
@typescript-eslint/recommended-requiring-type-checking. Floating promises are errors. verbatimModuleSyntax: true— type-only imports must useimport type.- No
any. No// @ts-ignore. If you need to escape the type system, write a justifying comment and useas unknown as T. - No inline
console.login committed code. Errors viaconsole.errorare fine; everything else is for tests.
Follow Conventional Commits:
type(scope): summary
Longer body explaining WHY, not WHAT — the diff already shows what.
Types: feat, fix, docs, style, refactor, test, chore, ci, build, perf.
Releases are gated by npm version plus the auto-release-pr workflow. The version field in package.json is the single source of truth — src/version.ts is regenerated from it by scripts/sync-version.cjs (wired in as the version lifecycle hook in package.json#scripts). Hand-editing the version field anywhere is unnecessary and surfaced loudly by test/version.test.ts if attempted.
End-to-end flow for a release:
# On develop, with all feature work merged and committed.
npm version patch # or minor / major
# ↑ This bumps package.json, runs sync-version.cjs (which updates
# src/version.ts and stages it), creates a single "v0.1.2" commit
# with both files, and creates a local tag v0.1.2.
git push origin develop
# ↑ Pushes the bump commit. The auto-release-pr workflow opens
# (or updates) "Release: develop -> main" with the bump diff
# included. Review and merge as a maintainer.
git push origin v0.1.2
# ↑ Pushes the tag. publish.yaml triggers, runs the full
# pre-publish gate again, and `npm publish --provenance` ships
# @zkcoins/sdk@0.1.2 to npm with OIDC provenance attestation.The tag push is the "send it" step — gated by the maintainer's explicit action after the release PR has merged to main. Never push the tag before the merge: publish would ship from a develop-only commit before the main branch has caught up.
If the auto-release-pr workflow fails or you need to amend mid-release: don't force-push the tag (npm publish would reject the same version anyway). Bump again (npm version patch produces v0.1.3) and start over — versions are cheap, history is forever.
For bugs: include the SDK version (@zkcoins/sdk@X.Y.Z), the runtime (Node version / browser / RN), a minimal reproduction, and the actual vs expected output. For features: link the consumer use case (a wallet integration, an example, an issue in zk-coins/node etc.) — the SDK does not add features speculatively.
MIT — by submitting a PR you agree your contribution is licensed under the same terms.