feat: add <TokenCreate /> and <ChainSelector /> (slice of #73) - #151
feat: add <TokenCreate /> and <ChainSelector /> (slice of #73)#151MayurK-cmd wants to merge 1 commit into
Conversation
|
|
||
| const filtered = useMemo(() => { | ||
| let list = chains; | ||
| if (shouldShowToggle && !showTestnets) { |
There was a problem hiding this comment.
Should-fix. shouldShowToggle gates both the toggle button (L133) and this testnet filter. So if a consumer passes showTestnetToggle={false} while the chain list contains testnets, the toggle is hidden AND this filter is skipped, so testnets are shown permanently with no way to hide them. The two concerns (render the toggle vs. filter the list) should be separated, e.g. always apply the !showTestnets filter when testnets exist and only let the prop control the button's visibility.
| <button | ||
| key={chain.chainId.toString()} | ||
| onClick={() => handleSelect(chain.chainId)} | ||
| aria-pressed={isSelected} |
There was a problem hiding this comment.
Should-fix (a11y). The container is role="radiogroup" (L121) but each option is a plain <button aria-pressed> rather than role="radio" / aria-checked, and there is no arrow-key roving-tabindex. A screen reader announces a radio group whose children are toggle buttons, and keyboard users get no arrow navigation. The axe smoke test will not catch this. Either give each row real radio semantics (role="radio", aria-checked, arrow-key handling) or drop the radiogroup role and expose them as a plain list of toggle buttons.
| import { PendingOverlay, Progress } from "./progress"; | ||
| import { ResultCard } from "./result-card"; | ||
|
|
||
| const STEPS: StepDescriptor[] = [ |
There was a problem hiding this comment.
Should-fix. STEPS has no submitting entry, but step becomes "submitting" during deploy. In progress.tsx L16 currentIndex = steps.findIndex(s => s.id === current) then returns -1, so every step computes isComplete=false and isCurrent=false: the whole stepper renders as inert while the spinner shows (verified with a screenshot). Suggest mapping submitting onto the Review index so the stepper stays on step 3 during deployment.
| const [result, setResult] = useState<DeployResult | null>(null); | ||
|
|
||
| const selectedChain = useMemo( | ||
| () => chains.find((c) => c.chainId === selectedChainId), |
There was a problem hiding this comment.
Should-fix. chainId is typed number | string, but this lookup (and chain-selector.tsx L74/L79) compares with strict ===. A consumer passing defaultChainId={"1"} against numeric chainId: 1 (as in defaultEvmChains) silently never matches, so selectedChain is undefined and no row highlights. Normalize both sides (e.g. String(a) === String(b)) before comparing.
| const whole = split[0] ?? ""; | ||
| const frac = split[1] ?? ""; | ||
| if (!whole) return "0"; | ||
| const padded = (frac + "0".repeat(decimals)).slice(0, decimals); |
There was a problem hiding this comment.
Should-fix. This silently truncates fractional digits beyond decimals, and the validators (^\d+(\.\d+)?$) never reject over-precision. So formatBaseUnits("1.9999999", 2) returns 199 (1.99) with no error, and that understated amount is what tx-preview.tsx shows as the raw value and what gets handed to onDeploy. Consider rejecting inputs with more fractional digits than decimals in the validator so the truncation is never silent.
|
|
||
| export function ResultCard({ chain, result, className }: ResultCardProps) { | ||
| const addressUrl = explorerAddressUrl(chain, result.address); | ||
| const txUrl = explorerTxUrl(chain, result.txHash); |
There was a problem hiding this comment.
Should-fix. explorerTxUrl / explorerAddressUrl only null-check explorerHost, so an empty txHash or address still yields a truthy URL like https://etherscan.io/tx/, and the txUrl && (...) guard renders a clickable link to a broken page (plus truncate("") renders an empty <code>). Worth guarding the value too, i.e. return null when the hash/address is empty.
| } else { | ||
| // Reject values that would overflow an EVM uint256 when scaled. | ||
| const whole = BigInt(supply.split(".")[0]); | ||
| if (whole >= BigInt("115792089237316195423570985008687907853269984665640564039457")) { |
There was a problem hiding this comment.
Nice-to-have. This cap is an unexplained magic bigint that is (2^256-1)/10^18, i.e. it hardcodes 18 decimals regardless of data.decimals. For a token with fewer decimals it over-rejects valid supplies. Deriving it from the actual decimals and naming the constant would read more clearly, e.g. (2n ** 256n - 1n) / 10n ** BigInt(data.decimals).
| return errors; | ||
| } | ||
|
|
||
| export function validateSolanaTokenForm(data: SolanaTokenFormData): FieldError[] { |
There was a problem hiding this comment.
Nice-to-have (DRY). validateSolanaTokenForm is a near-verbatim copy of validateEvmTokenForm; the name and symbol blocks are identical and only the decimals range and supply cap differ. Consider extracting a shared validateNameAndSymbol(data) plus a small per-chain { decimalsMax, supplyCap } config so only the chain-specific bits live in each path.
| */ | ||
| export interface DeployRequest { | ||
| chain: Chain; | ||
| family: "evm" | "solana"; |
There was a problem hiding this comment.
Nice-to-have. DeployRequest.family duplicates chain.ecosystem (set verbatim in token-create.tsx L117), so consumers have two sources of truth that can disagree. Could drop family and have consumers branch on req.chain.ecosystem.
What
Implements the component — a guided flow for deploying a new ERC-20 (EVM) or SPL (Solana) token — plus the prerequisite . Ships via both the shadcn registry at registry/w3-kit/ and a new programmatic @w3-kit/ui package entry.
Why
Closes the assigned slice of #73 (Chain-agnostic components). The issue calls for a single UI that lets users create a token on EVM or Solana, with chain-agnostic field rendering, transaction preview, progress, and explorer-link result. This PR delivers that slice with factored out as a reusable component.
How
Out of scope (deliberate)
Checklist