@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 via WalletUIProvider with the brand,
  • authenticates with Hanzo IAM via @hanzo/iam (HIP-0111) — social, email, password through useIam().login() and a /auth/callback PKCE exchange,
  • reaches chains through @luxwallet/sdk (chains + gateway RPC),
  • ships the brand's /brand.json so loadBrandConfig() 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 .tmpl suffix. (Only package.json — the npm name — and the per-app README.md are templated. Everything else is brand-neutral and reads brand.json at runtime.)
  • public/brand.json is written from the validated BrandConfig (defineBrand runs 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.

  1. Define the chain in @luxwallet/chains (sdk/packages/chains/src/registry.ts). Add one evm({ id, name, evmChainId, symbol, mainnet }) entry (or a non-EVM ChainEntry for X/P/Q/Z-style families). Per the sovereign-L1 model the registry sets networkId === evmChainId and rpcRoute = String(evmChainId), so the gateway URL is https://<gateway>/v1/rpc/<evmChainId>. Example:

    evm({ id: "acme-mainnet", name: "Acme", evmChainId: 424242, symbol: "ACME", mainnet: true }),
    
  2. Reference its id in the brand's chains[] (a brands/*.json or your own brand.json):

    { "id": "acme", "chains": ["acme-mainnet"], "...": "..." }
    
  3. 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.

S
Description
Generate a branded Lux Wallet (web + config) for a new chain/org from one brand.json
Readme MIT
77 KiB
Languages
TypeScript 82.2%
JavaScript 11.6%
Go Template 4.6%
HTML 1.6%