Imported from mmlado/qrkit (
AGENTS.md). Install upstream withnpx skills add mmlado/qrkit. Copyright stays with the author.
qrkit — Agent Guide
Project Overview
qrkit is a TypeScript monorepo providing a generic QR connector library for airgapped wallet flows. It is a frontend library — all packages are designed to run in the browser. It implements the ERC-4527 / UR / CBOR protocol stack used by Shell, Keystone, and similar hardware wallets — allowing web dApps to connect and sign transactions without any online wallet bridge.
Package Layout
apps/
react/ @qrkit/app-react — Vite demo app for exercising React flows
packages/
core/ @qrkit/core — framework-agnostic protocol logic
react/ @qrkit/react — React provider, hooks, drop-in components
wagmi/ @qrkit/wagmi — wagmi connector adapter for EVM dApps
Dependencies flow in one direction only:
@qrkit/core ← @qrkit/react ← @qrkit/wagmi
@qrkit/core has no framework dependencies. @qrkit/react and @qrkit/wagmi declare React as a peer dependency. wagmi and viem are peer dependencies of @qrkit/wagmi only.
Tooling
- Package manager: pnpm with workspaces (
pnpm-workspace.yaml) - Build orchestration: Turborepo (
turbo.json) — respects inter-package build order automatically - Per-package build: tsup (wraps esbuild) — outputs ESM, CJS, and
.d.ts - Testing: Vitest
- Versioning / changelogs: Changesets
Common commands
pnpm install # install all workspace deps
pnpm build # build all packages in dependency order
pnpm test # run all tests
pnpm typecheck # typecheck all packages
pnpm changeset # open changeset wizard for a new release entry
pnpm version-packages # apply pending changesets and bump versions
pnpm release # build + publish all public packages
Working on a single package
pnpm --filter @qrkit/core build
pnpm --filter @qrkit/core test
pnpm --filter @qrkit/react dev # watch mode
pnpm --filter @qrkit/app-react dev # run the Vite demo app
Source Structure
Each package follows the same layout:
packages/<name>/
src/
index.ts # public API — only export from here
dist/ # generated, do not edit
package.json
tsconfig.json
tsup.config.ts
README.md
CHANGELOG.md
All public exports must go through src/index.ts. Do not import across packages using relative paths — use the package name (@qrkit/core), which resolves via workspace:*.
packages/react/examples/ contains example integrations that are linted by @qrkit/react (eslint src examples). Keep those examples current with the public API; they are not covered by the root formatter glob unless explicitly formatted.
Code Style
Import ordering
Group and order imports in every file:
- Node built-ins (if any)
- External packages
- Internal packages (
@qrkit/*) - Relative imports — deepest first, then siblings, then parent
Leave a blank line between each group.
// external
import { decode } from "cborg";
import { UrFountainDecoder } from "@qrkit/bc-ur-web";
// internal package
import type { ScannedUR } from "@qrkit/core";
// relative
import { deriveEvmAccount } from "../eth/deriveAccount.js";
import type { QRKitConfig } from "./types.js";
TypeScript Config
The root tsconfig.base.json defines shared compiler options. Each package extends it and adds its own outDir, rootDir, and (for React packages) jsx. Key settings:
strict: trueverbatimModuleSyntax: true— useimport typefor type-only importserasableSyntaxOnly: true— no enums, no experimental decoratorsnoUnusedLocalsandnoUnusedParameters: true
Protocol Background
The core package implements:
- UR (Uniform Resources) — self-describing, CBOR-encoded format for QR data, optionally split across animated frames. Uses
@qrkit/bc-ur-web(pure JS, noBufferdependency). - CBOR — binary encoding used inside UR payloads. Uses
cborg. - crypto-hdkey / crypto-account — UR types for exporting xpubs from a hardware wallet.
- eth-sign-request / eth-signature — UR types for EVM signing flows (ERC-4527).
- HD key derivation — derive EVM addresses from exported xpubs. Uses
@scure/bip32.
The prototype implementation at ../shell_dapp_prototype/src/lib/ is the reference for all protocol logic. When in doubt about encoding details, check those files first.
Key Invariants
@qrkit/coremust never import fromreact,react-dom, DOM APIs, or any camera/canvas library.@qrkit/reactmust never import fromwagmiorviem.- Session state is the responsibility of
@qrkit/core. React and wagmi layers wrap it, they do not reimplement it. - All QR parts (animated or single-frame) are represented as
string[]in the core layer. Rendering is the responsibility of the React layer. - BTC address derivation lives in
src/btc/address.tsandsrc/btc/deriveAccount.ts. It derivesderiveChild(0).deriveChild(addressIndex)and produces P2WPKH (purpose 84, bech32), P2SH-P2WPKH (purpose 49, base58check), or P2PKH (purpose 44, base58check) addresses. Uses@scure/basefor encoding and@noble/hashesfor sha256/ripemd160. All further BTC-specific logic (sign requests, signature parsing, PSBT handling) must also live insrc/btc/and must not bleed into EVM files, tests, or types. BTC and EVM code paths must remain clearly separated at all times. crypto-multi-accounts(BCR-2020-015) is supported inparseXpub. Key 2 is the accounts array (same ascrypto-account). Key 3 is the outer device name — used asfallbackNamefor hdkey entries that don't carry key 9 themselves.crypto-accountandcrypto-multi-accountsboth returnParsedXpub[].deriveEvmAccountreturnsEvmAccount[](all matching EVM entries), not just the first.parseConnectioniterates and pushes all. Downstream consumers must handle multiple EVM accounts.- Both
EvmAccountandBtcAccountcarry the account-level xpub and account-levelderivationPathreconstructed from the QR origin keypath (m/purpose'/coinType'/accountIndex'). CallderiveAddress(addressIndex)to derive the external childm/purpose'/coinType'/accountIndex'/0/addressIndex; the last two path segments are always derived locally. - secp256k1 operations use
@noble/curves/secp256k1.js(v2 API:secp256k1.Point.fromHex(hexString).toBytes(false)).@noble/secp256k1is not a dep — do not add it back. QRKitProviderrenders no wrapper DOM elements — modals portal intodocument.body, theme variables inject a<style>tag into<head>.- Scanning and rendering are split into two layers: batteries-included (
useQRScanner,useQRDisplay) and primitive (useURDecoder,useQRParts). The primitives accept/emit raw strings and are scanner/renderer agnostic. - UI dependencies:
jsqrfor camera scanning (pure JS, no WASM — works in browser extensions and service workers),qrcodefor canvas rendering,focus-trapfor modal accessibility. Do not pull in full UI frameworks. - Default styles follow Material Design 3 tokens and support automatic light/dark via
prefers-color-scheme. Theme overrides use CSS custom properties via a<style>tag injected byQRKitProvider. - UR codec is
@qrkit/bc-ur-web— a browser-native fork of@ngraveio/bc-ur2.0-beta with Buffer and React Native polyfill deps removed. API: encode viaUR.fromCbor({ type, payload: Uint8Array })+UrFountainEncoder, decode viaUrFountainDecoderwithreceivePartUr(string),isComplete(),resultUr.getPayloadCbor(). The old@ngraveio/bc-ur1.x API (new UR(Buffer, type),UREncoder,URDecoder,resultUR(),ur.cbor) is gone.@qrkit/bc-uris a deprecated shell that re-exports@qrkit/bc-ur-web. useURDecodermust latch completion untilreset(); repeated final animated frames are common in camera scanning and should not triggeronScantwice after a successful decode.focus-trapis configured withescapeDeactivates: false— Escape is handled by a separatekeydownlistener. Do not setonDeactivate: onClose; the camera permission dialog steals focus and would immediately close the modal.Accountcarries adevice?: stringfield sourced from crypto-hdkey key 9 (the optionalnamefield in the UR spec). It isundefinedwhen the wallet does not set it. Any new chain-specific account type (BTC etc.) must include this field too.
Changelogs
Each package has its own CHANGELOG.md following Keep a Changelog format. Do not edit changelogs manually — use pnpm changeset to add entries, then pnpm version-packages to apply them. Prefer one changeset per affected package when the package-facing changes differ, including private workspace apps such as @qrkit/app-react.
Adding a New Package
- Create
packages/<name>/withsrc/index.ts,package.json,tsconfig.json,tsup.config.ts,README.md,CHANGELOG.md. - Extend
tsconfig.base.jsonin the package tsconfig. - The package will be picked up automatically by
pnpm-workspace.yamland Turborepo.
Testing Approach
- Unit tests live alongside source in
src/or in asrc/__tests__/subdirectory. - Tests use Vitest. No test framework globals — import from
vitestexplicitly. @qrkit/coretests should cover all protocol encode/decode paths with known-good UR fixtures.- Do not mock the UR codec or CBOR parser in core tests — use real encoded payloads.
@qrkit/reacttests use@testing-library/reactwith jsdom. Avitest.config.tsper package setsenvironment: "jsdom"andglobals: true(required by@testing-library/jest-dom).- jsdom does not expose
crypto.getRandomValues. Polyfill it insrc/__tests__/setup.tsusing Node'swebcrypto. - Do not test
SignModalrendering directly in unit tests — it callsbuildEthSignRequestURPartswhich usesCborTaginstanceof checks that fail across module boundaries in Vitest workspace resolution. Test the context state shape instead; sign request encoding is covered by@qrkit/coretests.
Finishing a Task
When the user says "finish", do the following in order:
- Add missing tests — cover any new or changed behaviour not yet tested.
- Lint — run
pnpm lintand fix all errors. - Format — run
pnpm formatand apply changes. - Run all tests — run
pnpm testand confirm everything passes. 4a. Run the example — runpnpm --filter @qrkit/core exampleand confirm it executes without errors. - Add changesets — run
pnpm changesetand follow the prompts: select the affected packages, choose a bump type (patch/minor/major), and write a short summary. Use separate changeset files when package-facing changes need different descriptions. Do not editCHANGELOG.mdmanually — it is generated bypnpm version-packages. - Update AGENTS.md — if anything was added, changed, or decided that is non-obvious and useful for future sessions (new invariants, new conventions, architectural decisions), add it here.
References
- ERC-4527 spec: https://eips.ethereum.org/EIPS/eip-4527
- UR spec (bc-ur): https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
- Keystone QR protocol write-up: https://github.com/KeystoneHQ/Keystone-developer-hub/blob/main/research/ethereum-qr-data-protocol.md
- wagmi connector API: https://wagmi.sh/dev/creating-connectors