Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ dist/
build/
out/

# Staged sources for @w3-kit/ui package (regenerated by build.mjs)
packages/w3-kit/src/components/
packages/w3-kit/src/lib/
packages/w3-kit/src/index.ts
packages/w3-kit/src/chain-selector.ts
packages/w3-kit/src/token-create.ts

# Logs
logs
*.log
Expand Down
8 changes: 8 additions & 0 deletions components/chain-selector/ChainSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client";

import type { Chain, ChainSelectorProps } from "../../registry/w3-kit/chain-selector/types";

export type { Chain, ChainSelectorProps };

export { ChainSelector } from "../../registry/w3-kit/chain-selector/chain-selector";
export { default } from "../../registry/w3-kit/chain-selector/chain-selector";
48 changes: 48 additions & 0 deletions components/chain-selector/chain-selector.learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Chain Selector — Learn

## What is a chain?

"Chain" is shorthand for _blockchain network_. Every wallet, transaction, token, and contract lives on exactly one chain at a time. When you switch chains in a wallet app, the _same address_ becomes a _different account_ on the new network with its own balance and history.

A chain isn't just a cryptocurrency — it's a whole state machine with its own rules. Ethereum, Polygon, and Arbitrum all run the EVM (Ethereum Virtual Machine), so they share tooling, but their consensus rules, gas markets, and block confirmations differ. Solana is a separate VM family; an Ethereum-style contract won't run there.

## Why a chain selector exists in dApps

Most tokenized actions — swaps, mints, transfers — are chain-specific. Before TokenCreate can deploy an ERC-20, it needs to know _which_ EVM chain you're deploying to. Before it can mint an SPL token, it needs to know which Solana cluster (mainnet, devnet). A single UI component that fans out into multiple downstream paths needs an upfront choice point.

This component is that choice point.

## How this component works

`<ChainSelector />` is presentational — it doesn't talk to a wallet or any chain RPC. It receives a list of `Chain` objects and emits the selected one's `chainId` via `onSelect`. Consumers (TokenCreate, the transaction history, etc.) read the chosen chain's `ecosystem` field to route the user into the right sub-flow.

### Grouping

Chains are split into **EVM** and **Solana** groups based on the `ecosystem` field. Within each group, the rows are sorted as-given. This grouping exists because downstream code uses `chain.ecosystem === "evm"` to switch form rendering.

### Search

When `searchable` is set, the list filters across `name`, `symbol`, `currency`, and `chainId`. Solana clusters and EVM chain IDs both stringify cleanly, so typing `137` finds Polygon, `8453` finds Base, and `mainnet` finds Solana mainnet.

### Testnet toggle

Some chains have `testnet: true`. The toggle is shown when _any_ chain in the list is a testnet. Off by default — most users want mainnet, and toggling off a testnet should never accidentally hide the chain forever.

### Accessibility

The list is a `role="radiogroup"` (one chain selection at a time) with each row a `button` that sets `aria-pressed`. The `aria-labelledby` references on each group let a screen reader user hear "EVM section" and "Solana section".

## What the consumer has to do

`<ChainSelector />` only emits `chainId`. It is the consumer's job to:

1. Look up the matching `Chain` object in the array passed in.
2. Read `chain.ecosystem` and route accordingly.
3. Read `chain.explorerHost` (if any) to build post-deploy links.
4. Read `chain.testnet` to warn the user or skip explorer links on test networks.

This component is intentionally dumb about RPCs so it can be used in marketing pages (no wallet connected) as well as inside live dApps.

## Security considerations

A real dApp should _never_ trust a chain selector's output alone — the user could have a stale wallet state. Treat the selector as intent ("the user wants chain 8453") and verify on submission ("the wallet is currently connected to chain 8453"). If there's a mismatch, prompt a chain switch rather than silently re-routing.
5 changes: 5 additions & 0 deletions components/chain-selector/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
type Chain,
type ChainEcosystem,
type ChainSelectorProps,
} from "../../registry/w3-kit/chain-selector/types";
7 changes: 7 additions & 0 deletions components/chain-selector/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
defaultEvmChains,
defaultSolanaChains,
defaultChains,
explorerTxUrl,
explorerAddressUrl,
} from "../../registry/w3-kit/chain-selector/utils";
12 changes: 12 additions & 0 deletions components/token-create/TokenCreate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

export { TokenCreate } from "../../registry/w3-kit/token-create/token-create";
export { default } from "../../registry/w3-kit/token-create/token-create";
export type {
TokenCreateProps,
TokenCreateStep,
EvmTokenFormData,
SolanaTokenFormData,
DeployRequest,
DeployResult,
} from "../../registry/w3-kit/token-create/types";
94 changes: 94 additions & 0 deletions components/token-create/token-create.learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Token Create — Learn

## What does "creating a token" mean?

A "token" is just an entry on a blockchain ledger that says _address X owns N units of asset T_. The contract (or program) that owns that entry defines the rules — how many decimals, whether new supply can be minted, whether holders can burn their tokens, who has authority to do those things.

On EVM chains, the dominant standard is **ERC-20**, defined by Ethereum's EIP-20 proposal in 2015. It specifies a contract interface with `name`, `symbol`, `decimals`, `totalSupply`, `balanceOf`, `transfer`, `transferFrom`, and `approve`. Anything that implements that interface — including tokens with optional extensions like `mint` or `burn` — is "an ERC-20".

On Solana, the equivalent is the **SPL Token** program, written by Solana Labs in 2019. It uses a different model: a _mint_ account owns the metadata (supply, decimals, authorities), and each holder gets a _token account_. SPL supports the same conceptual features as ERC-20 — fixed supply, mint authority, freeze authority — but the on-chain shape is different.

## What this component does

`<TokenCreate />` is a guided flow for issuing a new token. It doesn't talk to a wallet or chain RPC — that's the consumer's job. Instead, it:

1. Lets the user pick a chain (Ethereum, Polygon, Base, etc. _or_ Solana)
2. Renders ecosystem-appropriate form fields
3. Previews the deploy parameters
4. Calls `onDeploy(req)` and shows progress
5. Surfaces the deployed token address with copy + explorer links

The component never holds a private key and never signs a transaction. It's a UI scaffold.

### EVM path

```
name → string, e.g. "My Token"
symbol → string, e.g. "MTK"
decimals → 0-18, 18 = standard; stablecoins usually use 6
initialSupply → string, whole-token amount (component scales by decimals)
mintable → boolean enables _mint(address, amount) for the deployer
burnable → boolean enables burn(address, amount) for any holder
```

### Solana path

```
name → string
symbol → string
decimals → 0-9, 9 = standard; most tokens use 6 or 9
initialSupply → string
mintAuthority → "self" | "renounced"
```

"Renouncing" mint authority is Solana's equivalent of "burn the keys" — it's a one-way switch that makes supply permanently fixed. There's no per-holder burn operation in SPL; instead, supply destruction happens by sending tokens to a known dead address.

## The state machine

The component has 5 internal states; the visible stepper collapses them into 4:

```
chain → configure → preview → submitting → result
↑ ↓
└─── error ────┘
```

- **chain** — user picks a target chain
- **configure** — form fields, validated on `Preview`
- **preview** — read-only summary of the deploy parameters
- **submitting** — calls `onDeploy`, shows spinner
- **result** — token address with copy + explorer links

If `onDeploy` rejects, the component returns to **preview** with an error banner. The form state is preserved — the user can fix and re-submit without re-entering everything.

## Why `onDeploy` is a callback

There are many ways to actually deploy a token. ERC-20 alone has flavors:

- **OpenZeppelin** — the de-facto template. Industry standard.
- **Solmate** — gas-optimized, fewer features.
- **Custom Solidity** — your own audit, your own extensions.
- **Foundry/Hardhat scripts** — same OZ code, scripted deploy.

SPL is similarly fragmented: `@solana/spl-token`'s `createMint`, Metaplex's token-standard, the experimental Token-2022 program (Token Extensions).

A library can't know which the consumer wants. So `onDeploy` accepts the form data and lets the consumer do whatever they do. The component handles all the UX — the part that's actually reusable across stacks.

## Acceptance criteria checklist (per the issue)

- [x] Exported from `@w3-kit/ui` — see `packages/w3-kit/src/index.ts`
- [x] Uses `<ChainSelector />` and ERC-20 / SPL templates via `onDeploy`
- [x] Preview, submit, result, explorer link UI all rendered
- [x] shadcn/Tailwind styling matches other registry components
- [x] `.learn.md` present (this file)
- [x] Accessible: `radiogroup`, `aria-pressed`, `role="status"` for progress, copy/explorer links have `aria-label`s, field errors surface via `role="alert"`

## Security considerations

A real implementation MUST:

1. Verify the connected wallet is on the _exact_ chain the user picked in the UI. Race conditions between chain switches and submission are real.
2. For ERC-20 with `mintable: true`, deploy behind an Ownable constructor and explicitly revoke the deployer's ownership if you want the contract to be controlled by a multisig/timelock.
3. For Solana with `mintAuthority: "renounced"`, treat the renounce call as irreversible — there's no recovery from a renounced mint authority.
4. Never log `initialSupply`, `decimals`, or `address` values to analytics in cleartext — these are mildly linkable to deployer identity if crossed with the tx hash.
5. Use `navigator.clipboard.writeText` defensively (the component does) — Safari's clipboard API silently rejects without user-gesture context.
9 changes: 9 additions & 0 deletions components/token-create/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
type EvmTokenFormData,
type SolanaTokenFormData,
type DeployRequest,
type DeployResult,
type TokenCreateProps,
type TokenCreateStep,
type StepDescriptor,
} from "../../registry/w3-kit/token-create/types";
8 changes: 8 additions & 0 deletions components/token-create/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
EVM_TOKEN_NAME_MAX,
TOKEN_SYMBOL_MAX,
validateEvmTokenForm,
validateSolanaTokenForm,
formatBaseUnits,
type FieldError,
} from "../../registry/w3-kit/token-create/utils";
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"build": "tsc -p packages/create-w3-kit",
"lint": "eslint .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"smoke:a11y": "tsx scripts/smoke-a11y.mjs"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
Expand Down
64 changes: 64 additions & 0 deletions packages/w3-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# @w3-kit/ui

Programmatic entry point for the w3-kit Web3 component library.

- **shadcn registry** — install components individually from `registry/w3-kit/`.
- **package** — `@w3-kit/ui` exports the same components for direct npm consumption.

## Install

```bash
npm install @w3-kit/ui
```

## Usage

```tsx
import { TokenCreate, ChainSelector, defaultChains } from "@w3-kit/ui";

export default function Page() {
return (
<TokenCreate
chains={defaultChains}
onDeploy={async ({ chain, family, data }) => {
// Your deploy logic, e.g. via viem or @solana/spl-token.
return { address: "0x…", txHash: "0x…" };
}}
/>
);
}
```

## Exports

### `<TokenCreate />`

A guided flow for deploying a new token. Renders a chain picker → ecosystem-specific form (EVM ERC-20 or Solana SPL) → transaction preview → progress → result with explorer links.

| Prop | Type | Description |
| ---------------- | ----------------------------------------------- | -------------------------------------------------- |
| `chains` | `Chain[]` | Pass-through to `<ChainSelector />`. |
| `defaultChainId` | `number \| string` | Initial selection. |
| `onDeploy` | `(req: DeployRequest) => Promise<DeployResult>` | Your deploy logic. Resolves with address + txHash. |
| `className` | `string` | Optional extra classes on the root container. |

### `<ChainSelector />`

| Prop | Type | Description |
| ------------------- | ------------------------------------- | ------------------------------------- |
| `chains` | `Chain[]` | EVM and/or Solana chains. |
| `selectedChainId` | `number \| string` | Currently selected chain's `chainId`. |
| `onSelect` | `(chainId: number \| string) => void` | Fired on user selection. |
| `searchable` | `boolean` | Optional search input. |
| `showTestnetToggle` | `boolean` | Override the auto-detected toggle. |

`@w3-kit/ui` also exports helper utilities:

- `defaultChains`, `defaultEvmChains`, `defaultSolanaChains`
- `explorerAddressUrl(chain, address)`, `explorerTxUrl(chain, txHash)`
- `validateEvmTokenForm`, `validateSolanaTokenForm`, `formatBaseUnits`

## Related

- The shadcn registry at `registry/w3-kit/` is the canonical distribution channel.
- Each component's contract, security notes, and educational commentary live in `components/<name>/<name>.learn.md`.
50 changes: 50 additions & 0 deletions packages/w3-kit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@w3-kit/ui",
"version": "0.1.0",
"description": "@w3-kit/ui — Web3 component library (programmatic entry; the shadcn-compatible registry lives at registry/w3-kit/).",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./token-create": {
"types": "./dist/token-create.d.ts",
"import": "./dist/token-create.js"
},
"./chain-selector": {
"types": "./dist/chain-selector.d.ts",
"import": "./dist/chain-selector.js"
},
"./styles.css": "./dist/styles.css"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "node ./scripts/build.mjs",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^4.9.0"
},
"publishConfig": {
"access": "public"
},
"license": "MIT"
}
Loading
Loading