typescript 7.0.2 is the native compiler, so this is a pin and not a second toolchain. tsc -p tsconfig.json --noEmit: 0 diagnostics before and after. Co-authored-by: Hanzo Dev <[email protected]>
@luxwallet/create
Generate a branded Lux Wallet web app for a
new chain/org from one brand.json. Spinning up a wallet for a new chain is
one command.
# from a bundled brand
npx @luxwallet/create lux # → ./lux-wallet/apps/web
npx @luxwallet/create hanzo ./out # → ./out/apps/web
# from your own brand.json
npx @luxwallet/create ./my-brand.json ./acme-wallet
# brand-new chain, interactively (prompts for the core fields)
npx @luxwallet/create mychain
MIT, zero GPL. This generator never imports @luxfi/wallet (GPL) — it depends
only on @luxwallet/brand (MIT) for the brand
schema and validation.
What it stamps
<out>/
apps/web/
package.json # pins the (future) npm deps; name = <brand.id>-wallet-web
index.html # brand-neutral; title set at runtime
vite.config.ts
tsconfig.json
src/
main.tsx # loadBrandConfig('/brand.json') → render
App.tsx # IamProvider → WalletUIProvider → routes; /auth/callback
Login.tsx # IAM sign-in (HIP-0111) + optional SIWx wallet picker
vite-env.d.ts
public/
brand.json # THE per-brand artifact — validated BrandConfig
logo.svg # replace per brand
README.md # how to run/deploy THIS branded wallet
The generated app:
- mounts
@luxwallet/ui's screens viaWalletUIProviderwith the brand, - authenticates with Hanzo IAM via
@hanzo/iam(HIP-0111) — social, email, password throughuseIam().login()and a/auth/callbackPKCE exchange, - reaches chains through
@luxwallet/sdk(chains + gateway RPC), - ships the brand's
/brand.jsonsoloadBrandConfig()picks it up at runtime, - wires
ConnectWallet/ the login screen as the entry.
Template strategy — runtime brand.json (DRY)
There is one app shell for every brand. It is byte-for-byte identical across
brands — verified by a test. The shell reads public/brand.json at runtime
via @luxwallet/brand's loadBrandConfig(), which merges the brand onto the
live singleton, applies its theme tokens to CSS custom properties (--lw-<token>),
and sets the document title.
So the only per-brand artifacts are public/brand.json and public/logo.svg.
No per-brand forks, no per-brand source. Adding a brand never touches app code.
Templating is deliberately dependency-light:
- Files are copied verbatim from
templates/web/. - The handful that carry literal brand strings end in
*.tmpl; the generator replaces{{brand.<dotted.path>}}tokens and strips the.tmplsuffix. (Onlypackage.json— the npmname— and the per-appREADME.mdare templated. Everything else is brand-neutral and readsbrand.jsonat runtime.) public/brand.jsonis written from the validatedBrandConfig(defineBrandruns at the boundary).
Brand config (BrandConfig)
The schema is @luxwallet/brand's BrandConfig — the ONE source of white-label
truth. npx @luxwallet/create validates it via defineBrand before writing.
| field | required | meaning |
|---|---|---|
id |
✓ | stable brand id, e.g. lux |
name |
✓ | display name, e.g. Lux Wallet |
shortName |
✓ | compact label, e.g. Lux ("Continue with Lux") |
domain |
✓ | primary domain, e.g. lux.network |
iam.serverUrl |
✓ | OIDC server (HIP-0111), e.g. https://lux.id |
iam.clientId |
✓ | OAuth client id, <org>-<app>, e.g. lux-wallet |
iam.scopes |
✓ | OIDC scopes, e.g. ["openid","profile","email"] |
gateway.rpcBaseUrl |
✓ | gateway base; RPC = <rpcBaseUrl>/v1/rpc/<chainId> |
theme |
✓ | token → color; applied as --lw-<token> CSS vars |
chains |
✓ | chain ids from @luxwallet/chains to enable |
logo |
optional logo URL (default /logo.svg) |
See brands/_template.json for a copy-me starting
point documenting every field.
Example brands
brands/ ships four real brands. Chain ids are the canonical ones
from @luxwallet/chains; evmChainId is shown
for reference (the registry, not the brand, owns it):
| brand | iam.serverUrl |
gateway.rpcBaseUrl |
chains (id → evmChainId) |
|---|---|---|---|
lux |
https://lux.id |
https://api.lux.network |
lux-c-mainnet → 96369, lux-x-mainnet, lux-p-mainnet |
hanzo |
https://iam.hanzo.ai |
https://api.hanzo.ai |
hanzo-mainnet → 36963 |
zoo |
https://zoo.id |
https://api.zoo.network |
zoo-mainnet → 200200 |
pars |
https://pars.id |
https://api.pars.network |
pars-mainnet → 494949 |
Adding a NEW sovereign-L1 chain — end to end
Chain definitions live in one place: the @luxwallet/chains registry. The
generator stays focused on stamping the wallet; it does not define chains.
-
Define the chain in
@luxwallet/chains(sdk/packages/chains/src/registry.ts). Add oneevm({ id, name, evmChainId, symbol, mainnet })entry (or a non-EVMChainEntryfor X/P/Q/Z-style families). Per the sovereign-L1 model the registry setsnetworkId === evmChainIdandrpcRoute = String(evmChainId), so the gateway URL ishttps://<gateway>/v1/rpc/<evmChainId>. Example:evm({ id: "acme-mainnet", name: "Acme", evmChainId: 424242, symbol: "ACME", mainnet: true }), -
Reference its id in the brand's
chains[](abrands/*.jsonor your ownbrand.json):{ "id": "acme", "chains": ["acme-mainnet"], "...": "..." } -
Generate:
npx @luxwallet/create ./acme.json.
@luxwallet/sdk resolves the new chain's RPC through the brand gateway
automatically — one source of truth (the registry), no wallet-side chain config.
Programmatic API
import { generate, loadBrand } from "@luxwallet/create";
const brand = loadBrand("./brands/lux.json"); // or a bundled id, or a BrandConfig literal
const result = generate(brand, "/tmp/lux-wallet", { force: true });
// result.outDir, result.brand, result.files
Status — packages not yet on npm
@luxwallet/* and @hanzo/* are currently unpublished (developed in the
monorepo). The generated apps/web/package.json pins the future npm versions
with a //deps note. Until those are published:
- in a pnpm monorepo, set each
@luxwallet/*/@hanzo/*dep to"workspace:*"; - standalone, use
"file:../path/to/<pkg>".
Because of this, @luxwallet/create itself depends on @luxwallet/brand via
workspace:* (build it inside the luxwallet/sdk workspace, or pnpm link it).
What is verified headlessly (see test/): the CLI typechecks
(tsc --noEmit, clean); generate() produces the full tree; the emitted
brand.json round-trips through @luxwallet/brand's defineBrand; no template
tokens leak; and the app shell is byte-identical across brands. What needs the
npm packages published (or full workspace install) to verify: a complete
pnpm build of a generated Vite app, which pulls the @hanzo/gui peer tree
and React types.
License
MIT.