Pre-composed zustand wallet store (create/import/reveal/balance/send) that
replaces the npm-deprecated @luxwallet/core with an identical public API, so
consumers swap the import with no code change.
Decomplected into orthogonal, mostly-pure modules:
- hex: pure toHex/fromHex
- account: pure mnemonic/pk -> derived account + secrets
- evm: pure getBalance + EIP-1559 signAndSendEvm (self-check preserved)
- keychain: the secret-at-rest seam (memory + tauri bridge)
- store: thin zustand persist composition (devtools dropped)
Drops the @luxfi/crypto dependency entirely — the crypto engine is now a
minimal structural CryptoEngine interface the consumer's LuxCrypto loader
satisfies with zero changes. SIWx removed (belongs to @luxwallet/connect).
Behavioral invariants preserved: persist key 'lux-wallet', secrets never
persisted, keychain key format luxwallet:<id>:{mnemonic,privateKey}, BIP-44
m/44'/60'/0'/0/0 EVM derivation, EIP-55 addresses, sendEvm recid normalization
(27/28 or 0/1) + recovered-signer self-check.
Tests: 27 vitest specs over deterministic fakes (account/keychain/evm/store),
all green; full workspace build + test green (139 specs across 8 packages).
Co-authored-by: Hanzo Dev <[email protected]>
6.4 KiB
LLM.md — @luxwallet/sdk
Guidance for AI agents working in this repo. Per-package implementation
notes live in each packages/<name>/LLM.md; the hard parts are the crypto
and tx packages.
What this repo is
The unified Lux Wallet shared core, headless. An umbrella package
(@luxwallet/sdk) re-exports independent modules. The web wallet, the
browser extension, and the native wallets (via WASM/UniFFI) all consume
these modules so wallet logic exists once.
Architecture (decided — do not redesign)
- Umbrella + modules.
@luxwallet/sdkis a thin barrel. Each module is independently published (@luxwallet/chains,/rpc,/crypto,/keyring,/tx,/store) and independently usable. - connect/SIWx is separate. Lives in
luxwallet/connect(npm@luxwallet/connect, Gogithub.com/luxwallet/connect/go). DEPEND on it, never duplicate. It is documented in the umbrella and README as a sibling install; it is intentionally NOT a hard dependency here (and is currently unpublished, so adding it to a manifest would breakpnpm install). - One source of truth for chains.
@luxwallet/chainsowns chain metadata and emitschains.jsonso native Kotlin/Swift read identical data. Do not hardcode chain ids elsewhere — import from@luxwallet/chains. - One way to reach a chain.
@luxwallet/rpc:https://<gateway>/v1/rpc/<route>, default gatewayapi.hanzo.ai, brand override + per-chain override. No/api/prefix, no/v2. - One crypto implementation.
@luxwallet/cryptois a facade over theluxfi/cryptoC core (WASM for JS, UniFFI for native). NEVER@noble/post-quantum. NEVER reuse the precompile wire format fromlux/wallet/.../features/wallet/pq(audited WRONG).
@luxwallet/store — the pre-composed headless wallet
The one wallet store the desktops consume (create / import / reveal / balance /
send). It replaces the npm-deprecated @luxwallet/core (published in error from
a parallel monorepo); consumers swap the import @luxwallet/core →
@luxwallet/store with no code change — the public API is identical. It is a
thin zustand composition over pure modules; nothing brand-specific lives inside.
Injected seams (the ONLY coupling to the outside — via WalletEngineConfig):
crypto: () => Promise<CryptoEngine>— a minimal structural interface (keccak256,keys.deriveSecp256k1,keys.serviceIdentity,secp256k1get/sign/recover). The existingLuxCryptoWASM loader satisfies it structurally with zero changes, so the store carries NO crypto dependency and never imports@luxfi/crypto.keychain: Keychain— secret-at-rest seam.tauriKeychain(bridge)over the hostsecure_storage_*commands;memoryKeychain()for browser dev/tests.chains: ChainProvider— brand chain lookup +RpcClientLike(satisfied by@luxwallet/rpc'sRpcClient).
Field invariants (wallets exist in the field — breaking these bricks them):
- persist key defaults to
lux-wallet; the persisted slice isaccounts/selectedAccountId/selectedChainIdONLY. Secrets NEVER hit persisted state — they live only behind the keychain seam. - keychain keys are exactly
luxwallet:<id>:mnemonic/luxwallet:<id>:privateKey. - EVM keys: BIP-44
m/44'/60'/0'/0/0secp256k1, EIP-55 checksummed addresses; PQ identity viaserviceIdentity(mnemonic, 'wallet/0'). sendEvmis an EIP-1559 (type-2) tx with recid normalization (sig[64] may be 27/28 or 0/1) and a recovered-signer self-check that MUST pass before broadcast.
Internal modules are orthogonal and pure where possible: hex (toHex/fromHex),
account (mnemonic/pk → derived account + secrets, pure), evm (getBalance +
signAndSendEvm, pure), keychain (the secret seam), store (zustand persist
composition only — no devtools). SIWx is NOT here; it lives in
@luxwallet/connect. Deps: @luxwallet/chains (types); peers ethers 6 +
zustand 5 (react is zustand's own optional peer, provided by the app).
Cross-language plan
| Layer | JS (web/ext) | Native (iOS/Android) |
|---|---|---|
| Crypto | luxfi/crypto → WASM → setBackend |
luxfi/crypto → UniFFI → setBackend |
| Chains | import @luxwallet/chains |
read emitted chains.json |
| UI | @hanzo/gui (separate luxwallet/ui repo) |
native, same design tokens |
Toolchain / conventions
- pnpm workspace, Node ≥ 20, ESM only.
- TypeScript project references (
tsc --build) — cross-package types resolve through each dep's emitted.d.ts.pnpm -r typecheckbuilds the graph in order. Test files are excluded from the composite build and typechecked by vitest at run time. - Per-package
tsconfig.jsonextendstsconfig.base.json. - vitest per package. Tests assert the REAL parts (chains lookup, rpc URL
building, evm unsigned-tx shape, keyring bookkeeping, AccountID framing).
Stubs that throw
"not yet bound"/"builder todo"are asserted to throw. - Export conditions:
react-native+browser/defaultso native runtimes consume the same packages.
Hygiene gate
pnpm verify (scripts/verify-clean.mjs) fails the build if:
@luxwallet/cryptodepends on@noble/post-quantum(or any@noble/*crypto lib), or has any runtime dependency at all (it is a dependency-free facade);- any package pulls a GPL/AGPL dep.
Note: viem (in @luxwallet/tx) transitively uses @noble/curves +
@noble/hashes for classical EVM crypto — that is allowed (MIT, not PQ,
not in @luxwallet/crypto). Only @noble/post-quantum is forbidden.
What is real vs stubbed
REAL: chain registry + chains.json emit; gateway RPC URL resolution +
minimal EVM JSON-RPC client; EVM unsigned EIP-1559 tx builder (viem);
keyring account store + BIP-44 path derivation + AccountID byte-framing;
the crypto CryptoBackend interface + size/scheme constants; the umbrella
barrel.
STUBBED (interface exact, impl throws): all @luxwallet/crypto primitives
until a WASM/native backend is injected; keyring keypair derivation,
cSHAKE AccountID hashing, and keystore seal/unseal (go through the crypto
backend); the P/X/UTXO/SVM tx builders.
Rules
- Update the relevant
LLM.md, never create scratch summary files. - Import chain data from
@luxwallet/chains; never hardcode ids. - All crypto through
@luxwallet/crypto→luxfi/crypto. No second crypto lib, no@noble/post-quantum. - Builders emit UNSIGNED bytes only; signing is keyring + crypto.
- Keep
builderStatushonest —readyonly with a tested round-trip.