Dashboard, SDK: ERC20 assets#7321
Conversation
🦋 Changeset detectedLatest commit: d227bc3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. |
WalkthroughAdds ERC20 asset support and rewards features: new asset type "ERC20Asset", bifurcated token creation/launch flows (ERC20Asset vs DropERC20), form/schema and UI updates for dual sale modes, rewards page and helpers, a tokens SDK export barrel, and assorted component prop and UI tweaks. Includes a release changeset. Changes
Sequence Diagram(s)mermaid UI->>UI: User selects saleMode (erc20-asset or drop-erc20) Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/thirdweb/src/assets/index.ts (1)
1-4: Same extension concern & future export growth
- Ensure the
.jssuffix plays nicely with the compiler/bundler (see previous note).- As
assetsutilities grow, consider grouping exports in anindex.tsbarrel only once they stabilise; otherwise tree-shaking may pull in unfinished stubs.packages/thirdweb/scripts/generate/abis/assets/AssetEntrypointERC20.json (1)
1-34: Nit: store ABI as generated artefact, not sourceCommitted raw ABI arrays bloat the repo (~4 KB here) and create noisy diffs. If this file is generated by a script in
/scripts/generate, prefer ignoring it in Git and generating during build/publish instead.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (19)
packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/events/AssetCreated.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/events/AssetDistributed.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/events/ImplementationAdded.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/events/RewardLockerUpdated.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/events/RouterUpdated.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/read/getImplementation.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/read/getRewardLocker.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/read/getRouter.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/addImplementation.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/buyAsset.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAsset.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAssetById.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAssetByImplementationConfig.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/distributeAsset.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/initialize.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/listAsset.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/sellAsset.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/setRewardLocker.tsis excluded by!**/__generated__/**packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/setRouter.tsis excluded by!**/__generated__/**
📒 Files selected for processing (4)
packages/thirdweb/scripts/generate/abis/assets/AssetEntrypointERC20.json(1 hunks)packages/thirdweb/src/assets/create-token.ts(1 hunks)packages/thirdweb/src/assets/index.ts(1 hunks)packages/thirdweb/src/exports/assets.ts(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/thirdweb/src/assets/create-token.ts
[error] 1-1: Don't use '{}' as a type.
Prefer explicitly define the object shape. '{}' means "any non-nullable value".
(lint/complexity/noBannedTypes)
[error] 3-3: This parameter is unused.
Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend params with an underscore.
(lint/correctness/noUnusedVariables)
⏰ Context from checks skipped due to timeout of 90000ms (8)
- GitHub Check: Size
- GitHub Check: E2E Tests (pnpm, vite)
- GitHub Check: Lint Packages
- GitHub Check: Build Packages
- GitHub Check: E2E Tests (pnpm, webpack)
- GitHub Check: E2E Tests (pnpm, esbuild)
- GitHub Check: Unit Tests
- GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/thirdweb/src/exports/assets.ts (1)
1-1: Double-check relative path & extensionThe re-export targets
"../assets/index.js", but the source file isindex.tsin the same tree.
This pattern is okay iftscemits.jsfiles beside.d.tsand the runtime resolution happens on compiled output. Otherwise you’ll hit module-not-found in tests or ts-node. Verify the build pipeline supports “import with .js extension from TS”.packages/thirdweb/scripts/generate/abis/assets/AssetEntrypointERC20.json (1)
7-12:Details
❌ Incorrect review comment
Struct/event field size mismatch – potential ABI desync
Struct
addImplementation.configusesbytes createHookData, butImplementationAddedevent declaresbytes32 createHookData.
If the underlying Solidity contract really emitsbytes32, calls encodingbyteswill still compile but the event parser will truncate data.Confirm the canonical Solidity source and align both the function parameter and event field to the same type.
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "Searching for the ImplementationAdded event in Solidity files..." rg -n "event ImplementationAdded" --glob "*.sol" echo echo "Context around event ImplementationAdded:" rg -A3 -B3 "event ImplementationAdded" --glob "*.sol" echo echo "Searching for function addImplementation in Solidity files..." rg -n "function addImplementation" --glob "*.sol" echo echo "Context around function addImplementation:" rg -A3 -B3 "function addImplementation" --glob "*.sol" echo echo "Searching for structs related to ImplementationConfig or config:" rg -n "struct .*Implementation.*" --glob "*.sol" echo echo "Context around those struct definitions:" rg -A3 -B3 "struct .*Implementation.*" --glob "*.sol"Length of output: 358
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "Searching for the ImplementationAdded event in JSON ABI files..." rg -n '"name":\s*"ImplementationAdded"' --glob "*.json" echo echo "Context around the ImplementationAdded event:" rg -A5 -B5 '"name":\s*"ImplementationAdded"' --glob "*.json" echo echo "Inspecting createHookData field in this event:" rg -A2 -B2 '"createHookData"' --glob "*.json"Length of output: 216
No
ImplementationAddedevent in ABI – review comment can be ignoredThe ABI at
packages/thirdweb/scripts/generate/abis/assets/AssetEntrypointERC20.jsononly contains function definitions and does not declare anyImplementationAddedevent (nor abytes32 createHookDatafield). There is no mismatch to resolve.Likely an incorrect or invalid review comment.
size-limit report 📦
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7321 +/- ##
==========================================
- Coverage 56.34% 56.33% -0.02%
==========================================
Files 905 907 +2
Lines 58834 58965 +131
Branches 4150 4159 +9
==========================================
+ Hits 33151 33216 +65
- Misses 25577 25643 +66
Partials 106 106
🚀 New features to boost your workflow:
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
This PR has been inactive for 7 days. It is now marked as stale and will be closed in 2 days if no further activity occurs. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
.changeset/young-carrots-burn.md (2)
1-3: Confirm semver: likely needs a minor bump, not patch.Per semver, adding new public functions/events and expanded functionality (initialize functions, new events, multi-recipient distribution) is a feature addition → should be a minor release. Please confirm this isn’t just internal plumbing with no public surface changes.
Proposed change:
--- -"thirdweb": patch +"thirdweb": minor ---
5-5: Make the changeset description actionable for changelog readers.“ERC20 assets” is too terse for release notes. Summarize the user-facing changes in 1–5 bullets and reference the PR.
Proposed update:
-ERC20 assets +Add ERC20 asset management features: +- Add initialize functions across asset-related contracts +- Emit new events: AssetDistributed, RouterUpdated, RewardLockerUpdated +- Add router/reward locker support checks +- Enhance distributeToken to support multiple recipients +- Update ABIs and utilities for parameter encoding and transaction preparation + +Refs: #7321
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
.changeset/young-carrots-burn.md(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-20T18:54:15.781Z
Learnt from: MananTank
PR: thirdweb-dev/js#7081
File: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/create-token-page-impl.tsx:110-118
Timestamp: 2025-05-20T18:54:15.781Z
Learning: In the thirdweb dashboard's token asset creation flow, the `transferBatch` function from `thirdweb/extensions/erc20` accepts the raw quantity values from the form without requiring explicit conversion to wei using `toUnits()`. The function appears to handle this conversion internally or is designed to work with the values in the format they're already provided.
Applied to files:
.changeset/young-carrots-burn.md
📚 Learning: 2025-07-18T19:19:55.613Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-18T19:19:55.613Z
Learning: Applies to packages/thirdweb/src/wallets/** : EIP-1193, EIP-5792, EIP-7702 standard support in wallet modules
Applied to files:
.changeset/young-carrots-burn.md
🪛 LanguageTool
.changeset/young-carrots-burn.md
[grammar] ~1-~1: Hier könnte ein Fehler sein.
Context: --- "thirdweb": patch --- ERC20 assets
(QB_NEW_DE)
[grammar] ~5-~5: Hier könnte ein Fehler sein.
Context: --- "thirdweb": patch --- ERC20 assets
(QB_NEW_DE)
Merge activity
|
<!--
## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):
## Notes for the reviewer
Anything important to call out? Be sure to also clarify these in your comments.
## How to test
Unit tests, playground, etc.
-->
<!-- start pr-codex -->
---
## PR-Codex overview
This PR focuses on enhancing the `thirdweb` package by introducing new functionalities and updates for managing ERC20 assets, including the implementation of various events, functions, and utility methods for asset deployment and interaction.
### Detailed summary
- Added `initialize` functions to various asset-related contracts.
- Introduced event handling for `AssetDistributed`, `RouterUpdated`, and `RewardLockerUpdated`.
- Implemented methods for checking router and reward locker support.
- Enhanced the `distributeToken` function to handle multiple recipients.
- Updated ABI files for new function signatures and events.
- Improved utility functions for encoding parameters and transaction preparation.
> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/initialize.ts`, `packages/thirdweb/src/extensions/assets/__generated__/FeeManager/write/initialize.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/listAsset.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/distributeAsset.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAsset.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/sellAsset.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/buyAsset.ts`, `packages/thirdweb/src/extensions/assets/__generated__/ERC20Asset/write/initialize.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/addImplementation.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAssetById.ts`, `packages/thirdweb/src/extensions/assets/__generated__/AssetInfraDeployer/write/deployInfraProxyDeterministic.ts`, `packages/thirdweb/package.json`, `packages/thirdweb/src/extensions/assets/__generated__/AssetEntrypointERC20/write/createAssetByImplementationConfig.ts`, `packages/thirdweb/src/assets/bootstrap.ts`
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **New Features**
* Added on-page Rewards workflow (Claim Rewards page, UI, and sidebar link) and expanded token creation flows for ERC20 assets with dedicated sale/airdrop options.
* **UI / UX**
* Improved token sale & distribution UIs, grouped recent transfers, loading placeholders, styling tweaks, chain prioritization in network selectors, customizable wallet fallback icon, and chart legend/value display.
* **API / Props**
* Several component props and callbacks updated to include chain/contract info and router availability.
* **Chores**
* Release metadata added for an ERC20 assets patch.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
There was a problem hiding this comment.
Actionable comments posted: 7
🔭 Outside diff range comments (2)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx (2)
108-116: Guard against invalid/zero supply in distribution calculationsIf supply is 0 or NaN, downstream calculations produce NaN/Infinity and can let the Next button be enabled erroneously. Add an early validation error here (complements the schema change).
function getDistributionError(form: TokenDistributionForm) { - const supply = Number(form.watch("supply")); + const supply = Number(form.watch("supply")); + if (!Number.isFinite(supply) || supply <= 0) { + return "Enter a valid total supply"; + }
143-154: Harden bar chart math against NaN/Infinity and improve numeric coercionUse SafeNumber consistently and avoid division by zero.
- const totalSupply = Number(props.distributionFormValues.supply); - const totalAirdropSupply = - props.distributionFormValues.airdropAddresses.reduce( - (acc, curr) => acc + Number(curr.quantity), - 0, - ); - const airdropPercentage = (totalAirdropSupply / totalSupply) * 100; + const totalSupply = SafeNumber(props.distributionFormValues.supply as string); + const totalAirdropSupply = + props.distributionFormValues.airdropAddresses.reduce( + (acc, curr) => acc + SafeNumber(curr.quantity), + 0, + ); + const airdropPercentage = + totalSupply > 0 ? (totalAirdropSupply / totalSupply) * 100 : 0; const salePercentage = Number( props.distributionFormValues.saleMode === "erc20-asset:pool" ? props.distributionFormValues.erc20Asset_poolMode .saleAllocationPercentage : props.distributionFormValues.dropERC20Mode.saleAllocationPercentage, ); const ownerPercentage = Math.max(100 - airdropPercentage - salePercentage, 0); const tokenAllocations: Segment[] = [ { color: "hsl(var(--chart-1))", label: "Owner", percent: ownerPercentage, - value: `${ownerPercentage}%`, + value: `${ownerPercentage}%`, }, { color: "hsl(var(--chart-3))", label: "Airdrop", percent: airdropPercentage, - value: `${airdropPercentage}%`, + value: `${airdropPercentage}%`, }, { color: "hsl(var(--chart-4))", label: "Sale", percent: salePercentage, - value: `${salePercentage}%`, + value: `${salePercentage}%`, }, ];Optionally format to 2 decimals for cleaner display: use toFixed(2) before string interpolation.
Also applies to: 156-176
🧹 Nitpick comments (24)
apps/dashboard/src/@/hooks/project-contracts.ts (6)
14-19: Centralize contractType and make it optional instead of| undefinedDefining the union inline increases drift risk as more asset types are added. Prefer a local type alias (or reuse a shared type barrel if one exists) and mark the field optional.
Apply this diff here:
- contractType: - | "ERC20Asset" - | "DropERC721" - | "DropERC1155" - | "DropERC20" - | undefined; + contractType?: ContractType;And add a local type alias near the top of the file (or import from a shared types barrel if available):
type ContractType = "ERC20Asset" | "DropERC721" | "DropERC1155" | "DropERC20";
13-13: Use optional property instead of union-with-undefined for deploymentTypeThis is more idiomatic and signals the field can be omitted (JSON.stringify will skip it).
- deploymentType: "asset" | undefined; + deploymentType?: "asset";
7-8: Add a mutationKey for React Query devtools clarity and dedupingNot required, but helpful for debugging and potential deduplication.
return useMutation({ + mutationKey: ["project-contracts", "add"], mutationFn: async (params: {
35-38: Avoid leaking raw server errors to the browser consoleThis hook runs on the client; logging raw backend errors can expose internals. Log only in dev and throw a user-safe message.
- if (!res.ok) { - console.error(res.error); - throw new Error(res.error); - } + if (!res.ok) { + if (process.env.NODE_ENV !== "production") { + console.error(res.error); + } + throw new Error("Failed to add contract to project"); + }
54-58: Add explicit return type to align with TS guidelinesThe dashboard guidelines prefer explicit return types. Add the Promise shape to the signature.
-export async function removeContractFromProject(params: { +export async function removeContractFromProject(params: { teamId: string; projectId: string; contractId: string; -}) { +}): Promise<{ result: { success: boolean } }> {
64-66: Mirror error handling pattern from the add hook and avoid exposing raw messagesFor consistency and to avoid surfacing backend messages directly to users, guard logs in dev and throw a generic error.
- if (!res.ok) { - throw new Error(res.error); - } + if (!res.ok) { + if (process.env.NODE_ENV !== "production") { + console.error(res.error); + } + throw new Error("Failed to remove contract from project"); + }.changeset/young-carrots-burn.md (1)
5-5: Make the changeset description user-facing and specific.“ERC20 assets” is too terse for release notes. Briefly summarize the new surface (tokens export), key ABIs, and dashboard capability so downstream consumers understand what’s in this patch.
Apply this diff to improve the description:
-ERC20 assets +Add ERC20 asset support: +- expose a new SDK surface at `thirdweb/tokens` +- add ERC20Entrypoint/RewardLocker/PoolRouter/FeeManager ABIs for generation +- enable rewards claim flow in the dashboard +- no breaking API changesapps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx (3)
1-1: Mark this as a Server Component.Per dashboard guidelines, add
import "server-only";to ensure this page isn’t accidentally bundled for the client.+import "server-only"; import { notFound, redirect } from "next/navigation";
70-77: Avoid passing client instances across the server/client boundary.To keep the payload lean and avoid serialization pitfalls, prefer passing primitives (address, chain slug/id) to the client component and creating the client-side contract within it. You’re already doing the server-side variant above for reward discovery; mirror that pattern client-side within
ClaimRewardsPage.Example direction (requires adjusting
ClaimRewardsPageprops):- <ClaimRewardsPage - assetContractClient={info.clientContract} - entrypointContractClient={getContract({ - address: entrypointContractAddress, - chain: chain, - client: info.clientContract.client, - })} - reward={reward} - unclaimedFees={unclaimedFees} - chainSlug={info.chainMetadata.slug} - /> + <ClaimRewardsPage + assetAddress={info.clientContract.address} + entrypointAddress={entrypointContractAddress} + chainSlug={info.chainMetadata.slug} + reward={reward} + unclaimedFees={unclaimedFees} + />Then, inside
ClaimRewardsPage(client), construct the contract viagetContractusing the client-side SDK.
35-39: Minor: handle missing entrypoint defensively.If
getDeployedEntrypointERC20fails or returns no address on unsupported chains, consider short-circuiting tonotFound()early to avoid downstream errors.apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx (1)
30-54: DRY the async delay with a small sleep helperYou repeat the 1s delay snippet several times. Centralize to a tiny helper for readability.
Apply this diff within the selected ranges to use a helper:
- airdropTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, + airdropTokens: async () => { + await sleep(1000); + }, - approveAirdropTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, + approveAirdropTokens: async () => { + await sleep(1000); + }, - deployContract: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); + deployContract: async () => { + await sleep(1000); return { contractAddress: "0x123" }; },- airdropTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, + airdropTokens: async () => { + await sleep(1000); + }, - deployContract: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); + deployContract: async () => { + await sleep(1000); return { contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }; }, - setClaimConditions: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, + setClaimConditions: async () => { + await sleep(1000); + }, - mintTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, + mintTokens: async () => { + await sleep(1000); + },- deployContract: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); + deployContract: async () => { + await sleep(1000); throw new Error("Failed to deploy contract"); },- deployContract: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); + deployContract: async () => { + await sleep(1000); throw new Error( "You have reached your storage limit. Please add a valid payment method to continue using the service.", ); },Additionally add this helper near the top (outside the selected ranges):
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));Also applies to: 42-54, 78-81, 99-104
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx (1)
209-214: Replace inline styles with Tailwind arbitrary values to match dashboard guidelinesInline style is discouraged. Use Tailwind’s arbitrary value syntax with your existing CSS vars.
Apply this diff:
- fallbackIcon={ - <div - className="size-3 rounded-full" - style={{ - backgroundColor: recipientColor, - }} - /> - } + fallbackIcon={ + <div className="size-3 rounded-full bg-[hsl(var(--chart-1))]" /> + }- fallbackIcon={ - <div - className="size-3 rounded-full" - style={{ - backgroundColor: developerColor, - }} - /> - } + fallbackIcon={ + <div className="size-3 rounded-full bg-[hsl(var(--chart-2))]" /> + }Also applies to: 227-233
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts (3)
25-26: Grammar nit in validation messageMinor copy edit for user-facing error.
- message: "Amount must be number larger than or equal to 0", + message: "Amount must be a number greater than or equal to 0",
41-50: Avoid double Number() conversion; reuse parsed valueYou already computed numValue. Reuse it for getInitialTickValue for clarity.
- const tick = getInitialTickValue({ - startingPricePerToken: Number(value), - }); + const tick = getInitialTickValue({ + startingPricePerToken: numValue, + });
31-36: Optional: validate airdrop quantities are numeric and non-negativeRight now quantity is any string; you later coerce with SafeNumber. A light schema guard improves UX.
airdropAddresses: z.array( z.object({ address: addressSchema, - quantity: z.string(), + quantity: z + .string() + .refine((v) => { + const n = Number(v); + return Number.isFinite(n) && n >= 0; + }, "Quantity must be a non-negative number"), }), ),apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx (2)
205-222: Constrain percentage input to non-negative valuesYou cap max at 100; also cap min at 0 to guard accidental negatives.
- <DecimalInput - maxValue={100} + <DecimalInput + minValue={0} + maxValue={100}
149-157: Unusedclientprop inside PoolConfigPoolConfig receives and forwards client but does not use it. Consider removing from the component signature and call to reduce surface area.
-function PoolConfig(props: { - form: TokenDistributionForm; - chainId: string; - client: ThirdwebClient; -}) { +function PoolConfig(props: { + form: TokenDistributionForm; + chainId: string; +}) {- <PoolConfig - chainId={props.chainId} - client={props.client} - form={props.form} - /> + <PoolConfig chainId={props.chainId} form={props.form} />Also applies to: 94-98
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx (2)
95-156: Step orchestration is correct; small async hygiene improvementBoth branches build steps deterministically and gate optional steps correctly (approve-airdrop before airdrop). To avoid unhandled rejections from the async runner, prefix the calls with void (and see the companion comment about not rethrowing inside executeSteps).
- executeSteps(initialSteps, 0, isGasless); + void executeSteps(initialSteps, 0, isGasless); @@ - executeSteps(initialSteps, 0, isGasless); + void executeSteps(initialSteps, 0, isGasless);
217-233: Don’t rethrow inside executeSteps to prevent unhandled promise rejectionsThe error state is already surfaced via updateStatus and UI retry. Rethrowing here causes an unhandled rejection since callers don’t await/catch. Remove the throw.
updateStatus(i, { message: errorMessage, type: "error", }); - throw error; + // Do not rethrow; error is reflected in step status and UI handles retries.apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx (5)
146-153: Avoid unhandled rejections on background addContractToProjectmutateAsync returns a Promise; if you don't await it, swallow/capture errors to prevent unhandled rejections.
- addContractToProject.mutateAsync({ + void addContractToProject + .mutateAsync({ chainId: params.values.chain, contractAddress: contractAddress, contractType: "ERC20Asset", deploymentType: "asset", projectId: props.projectId, teamId: props.teamId, - }); + }) + .catch((err) => console.warn("addContractToProject failed", err));
272-279: Do the same for DropERC20 addContractToProjectMirror the background error handling here as well.
- addContractToProject.mutateAsync({ + void addContractToProject + .mutateAsync({ chainId: values.chain, contractAddress: contractAddress, contractType: "DropERC20", deploymentType: "asset", projectId: props.projectId, teamId: props.teamId, - }); + }) + .catch((err) => console.warn("addContractToProject failed", err));
443-449: Make bridge trigger fire-and-forget safelycreateTokenOnUniversalBridge returns a Promise. If you’re intentionally not awaiting, prefix with void and catch to avoid unhandled rejections.
- createTokenOnUniversalBridge({ + void createTokenOnUniversalBridge({ chainId: params.chainId, client: props.client, tokenAddress: params.contractAddress, - }); + }).catch((err) => + console.warn("createTokenOnUniversalBridge failed", err), + );
90-94: Name consistency: Erc20Asset_deployContract → ERC20Asset_deployContractNit but helps keep internal helpers consistent with the group name.
- async function Erc20Asset_deployContract(params: { + async function ERC20Asset_deployContract(params: { @@ - deployContract: Erc20Asset_deployContract, + deployContract: ERC20Asset_deployContract,Also applies to: 431-435
90-168: Consider adding explicit return types to exported helpersFor readability and to match the repo guidelines, annotate returns:
- ERC20Asset_deployContract: Promise<{ contractAddress: string }>
- ERC20Asset_airdropTokens: Promise
- ERC20Asset_approveAirdropTokens: Promise
- DropERC20_*: Promise except deployContract which returns Promise<{ contractAddress: string }>
Also applies to: 170-226, 248-292, 294-424
| supply: z.string().min(1, "Supply is required"), | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Validate supply is a positive numeric value to prevent NaN/zero edge cases downstream
Many consumers cast supply with Number(...) and do math, which can yield NaN/Infinity and bypass errors. Enforce numeric > 0 at the schema level.
- supply: z.string().min(1, "Supply is required"),
+ supply: z
+ .string()
+ .min(1, "Supply is required")
+ .refine((v) => {
+ const n = Number(v);
+ return Number.isFinite(n) && n > 0;
+ }, "Supply must be a positive number"),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| supply: z.string().min(1, "Supply is required"), | |
| }); | |
| supply: z | |
| .string() | |
| .min(1, "Supply is required") | |
| .refine((v) => { | |
| const n = Number(v); | |
| return Number.isFinite(n) && n > 0; | |
| }, "Supply must be a positive number"), | |
| }); |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts
around lines 82-83, the schema currently accepts supply as a non-empty string
which allows consumers to cast to Number(...) and get NaN/0/Infinity later;
change the schema to coerce and validate a positive number: use z.preprocess to
convert incoming strings to Number and then validate with z.number().positive()
(with a clear error message like "Supply must be a number greater than 0") so
downstream math never receives NaN/zero.
| reportContractDeployed({ | ||
| address: contractAddress, | ||
| chainId: Number(params.values.chain), | ||
| contractName: "DropERC20", | ||
| deploymentType: "asset", | ||
| publisher: account.address, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Fix analytics: wrong contractName for ERC20Asset deployment
You're reporting ERC20Asset deployments as "DropERC20". This skews analytics and downstream reporting.
reportContractDeployed({
address: contractAddress,
chainId: Number(params.values.chain),
- contractName: "DropERC20",
+ contractName: "ERC20Asset",
deploymentType: "asset",
publisher: account.address,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| reportContractDeployed({ | |
| address: contractAddress, | |
| chainId: Number(params.values.chain), | |
| contractName: "DropERC20", | |
| deploymentType: "asset", | |
| publisher: account.address, | |
| }); | |
| reportContractDeployed({ | |
| address: contractAddress, | |
| chainId: Number(params.values.chain), | |
| contractName: "ERC20Asset", | |
| deploymentType: "asset", | |
| publisher: account.address, | |
| }); |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx
around lines 155–162, the analytics event is reporting ERC20Asset deployments
with contractName "DropERC20"; change the contractName string to "ERC20Asset" so
the reportContractDeployed call uses contractName: "ERC20Asset" (keep chainId,
deploymentType and publisher unchanged) to correct analytics; run and verify any
affected analytics dashboards/tests after the change.
| async function ERC20Asset_airdropTokens(params: { | ||
| values: CreateAssetFormValues; | ||
| gasless: boolean; | ||
| }) { | ||
| const { values, gasless } = params; | ||
| const account = getAccount(gasless); | ||
| const contract = getDeployedContract({ chain: values.chain }); | ||
|
|
||
| const airdropTx = await distributeToken({ | ||
| chain: contract.chain, | ||
| client: props.client, | ||
| contents: values.airdropAddresses.map((recipient) => ({ | ||
| amount: BigInt(recipient.quantity), | ||
| recipient: recipient.address, | ||
| })), | ||
| tokenAddress: contract.address, | ||
| }); | ||
|
|
||
| await sendAndConfirmTransaction({ | ||
| account, | ||
| transaction: airdropTx, | ||
| }); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm distributeToken expects base units vs human-readable amounts
Here, amount is built via BigInt(recipient.quantity). If the form captures human-readable quantities (e.g., "1.5"), this will throw or mis-scale. The tokens API may accept human-readable amounts as strings and convert internally (similar to transfer/transferBatch).
Run this to inspect local usage patterns and the function signature:
If distributeToken accepts human-readable amounts, prefer:
- amount: recipient.quantity (string), letting the SDK handle decimals; or
- if base units are required, fetch decimals and use toUnits(amount, decimals).
🏁 Script executed:
#!/bin/bash
# Find distributeToken usage and typical parameter shapes
rg -nP -C3 '\bdistributeToken\s*\('
# Also inspect approve usage to confirm preferred 'amount' vs 'amountWei'
rg -nP -C3 '\bapprove\s*\(\s*\{' | sed -n '1,200p'Length of output: 18675
🏁 Script executed:
#!/bin/bash
# Display the beginning of distribute-token.ts to inspect types and parameter expectations
sed -n '1,200p' packages/thirdweb/src/tokens/distribute-token.tsLength of output: 944
🏁 Script executed:
#!/bin/bash
# Locate DistributeContent type definition
rg -nP "export type DistributeContent" -n packages/thirdweb/src/tokens/types.ts
# Print the definition for context
sed -n '1,200p' packages/thirdweb/src/tokens/types.tsLength of output: 1335
Align amount with distributeToken’s human-readable inputs
The SDK’s distributeToken implementation calls
toUnits(a.amount.toString(), 18)internally, so you must pass the human-readable token amount (string or number)—not a base-unit BigInt. Wrapping recipient.quantity in BigInt:
- Forbids fractional quantities (
BigInt("1.5")throws) - If
quantitywere already in base units, it would be scaled again
Recommended fixes in ERC20Asset_airdropTokens (around lines 178–184):
• If your form’s quantity is an integer or decimal string, drop the BigInt and pass it directly:
- contents: values.airdropAddresses.map((recipient) => ({
- amount: BigInt(recipient.quantity),
+ contents: values.airdropAddresses.map((recipient) => ({
+ amount: recipient.quantity, // human-readable number or numeric string
recipient: recipient.address,
})),• If you need to support decimals but keep a bigint type locally, convert at the call site and cast:
import { toUnits } from "thirdweb/utils/units";
const decimals = 18; // or fetch from the token metadata
contents: values.airdropAddresses.map((r) => ({
amount: BigInt(toUnits(r.quantity.toString(), decimals)),
recipient: r.address,
}));• Verify your CreateAssetFormValues.airdropAddresses[].quantity type (string vs. number) and adjust accordingly.
| const approvalTx = approve({ | ||
| amountWei: toWei(totalAmountToAirdrop.toString()), | ||
| contract: contract, | ||
| spender: entrypoint.address, | ||
| }); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Approve with amount, not amountWei, to avoid decimals mismatch
Using toWei assumes 18 decimals; ERC20 decimals can vary. The ERC20 approve helper supports human-readable amounts and performs conversion internally.
- const approvalTx = approve({
- amountWei: toWei(totalAmountToAirdrop.toString()),
- contract: contract,
- spender: entrypoint.address,
- });
+ const approvalTx = approve({
+ amount: totalAmountToAirdrop.toString(),
+ contract,
+ spender: entrypoint.address,
+ });If this is the only toWei usage, also remove the unused import:
- toWei,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const approvalTx = approve({ | |
| amountWei: toWei(totalAmountToAirdrop.toString()), | |
| contract: contract, | |
| spender: entrypoint.address, | |
| }); | |
| const approvalTx = approve({ | |
| amount: totalAmountToAirdrop.toString(), | |
| contract, | |
| spender: entrypoint.address, | |
| }); |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx
around lines 216–221, the approve call incorrectly passes an amountWei (using
toWei) which assumes 18 decimals; change the call to pass a human-readable
amount instead (e.g., amount: totalAmountToAirdrop.toString()) so the ERC20
helper can convert using the token's actual decimals, and if toWei is now unused
remove its import to avoid dead code.
| <SelectItem value="bonding-curve">Bonding Curve</SelectItem> | ||
| <SelectItem value="fixed-price" disabled> | ||
| <span className="flex items-center gap-2"> | ||
| Dynamic Bonding Curve{" "} | ||
| <Badge variant="secondary">Coming Soon</Badge> | ||
| </span> | ||
| </SelectItem> | ||
| <SelectItem value="dutch-auction" disabled> | ||
| <span className="flex items-center gap-2"> | ||
| Fixed Price <Badge variant="secondary">Coming Soon</Badge> | ||
| </span> | ||
| </SelectItem> |
There was a problem hiding this comment.
Fix mislabeled pricing strategy options
“Dynamic Bonding Curve” is shown under value="fixed-price" and “Fixed Price” under value="dutch-auction". This is confusing for users.
- <SelectItem value="fixed-price" disabled>
+ <SelectItem value="dynamic-bonding-curve" disabled>
<span className="flex items-center gap-2">
- Dynamic Bonding Curve{" "}
+ Dynamic Bonding Curve{" "}
<Badge variant="secondary">Coming Soon</Badge>
</span>
</SelectItem>
- <SelectItem value="dutch-auction" disabled>
+ <SelectItem value="fixed-price" disabled>
<span className="flex items-center gap-2">
- Fixed Price <Badge variant="secondary">Coming Soon</Badge>
+ Fixed Price <Badge variant="secondary">Coming Soon</Badge>
</span>
</SelectItem>If “dutch-auction” is planned, add it as a separate disabled option with the correct label.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <SelectItem value="bonding-curve">Bonding Curve</SelectItem> | |
| <SelectItem value="fixed-price" disabled> | |
| <span className="flex items-center gap-2"> | |
| Dynamic Bonding Curve{" "} | |
| <Badge variant="secondary">Coming Soon</Badge> | |
| </span> | |
| </SelectItem> | |
| <SelectItem value="dutch-auction" disabled> | |
| <span className="flex items-center gap-2"> | |
| Fixed Price <Badge variant="secondary">Coming Soon</Badge> | |
| </span> | |
| </SelectItem> | |
| <SelectItem value="bonding-curve">Bonding Curve</SelectItem> | |
| <SelectItem value="dynamic-bonding-curve" disabled> | |
| <span className="flex items-center gap-2"> | |
| Dynamic Bonding Curve{" "} | |
| <Badge variant="secondary">Coming Soon</Badge> | |
| </span> | |
| </SelectItem> | |
| <SelectItem value="fixed-price" disabled> | |
| <span className="flex items-center gap-2"> | |
| Fixed Price <Badge variant="secondary">Coming Soon</Badge> | |
| </span> | |
| </SelectItem> |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx
around lines 176 to 187, the SelectItem labels are swapped: the option with
value="fixed-price" is labeled "Dynamic Bonding Curve" and value="dutch-auction"
is labeled "Fixed Price". Fix by renaming the labels to match their values
(value="fixed-price" should show "Fixed Price" and value="dutch-auction" should
show "Dutch Auction" or similar), and if dutch-auction is planned but not
available, add a separate disabled SelectItem for it with the correct label and
an appropriate "Coming Soon" Badge so each value matches its displayed label.
| onLaunchSuccess: (params: { | ||
| chainId: number; | ||
| contractAddress: string; | ||
| }) => void; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Prop signature change for onLaunchSuccess: verify all call sites are updated
The new signature is (params: { chainId: number; contractAddress: string }) => void. Ensure no downstream usages still expect the zero-arg callback.
Use this script to find and inspect call sites:
🏁 Script executed:
#!/bin/bash
# Find usages of onLaunchSuccess prop across the repo (and check param shape)
rg -nP -C3 '\bonLaunchSuccess\s*=\s*\(' --type ts --type tsxLength of output: 90
To cover both .ts and .tsx files without relying on ripgrep’s file‐type detection, let’s broaden the search:
🏁 Script executed:
#!/bin/bash
# Find all usages of onLaunchSuccess across TS and TSX files
rg -nP -C3 'onLaunchSuccess' -g '*.ts' -g '*.tsx'Length of output: 13512
Update Storybook stories to match the new onLaunchSuccess signature
All internal usages of onLaunchSuccess in the token launcher have been updated to accept a { chainId, contractAddress } parameter, but the Storybook examples still use the old zero-arg callback. Please update them to take (and ignore) the new params:
• apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx
– Lines 63, 84, 107: change
onLaunchSuccess: () => {}
to
onLaunchSuccess: (_params) => {}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx
around lines 63, 84, and 107, the Storybook stories still use the old zero-arg
onLaunchSuccess signature; update each onLaunchSuccess: () => {} to accept the
new parameter shape by changing them to onLaunchSuccess: (_params) => {} so the
stories match the component's new signature and ignore the passed { chainId,
contractAddress } argument.
| token0: { | ||
| address: string; | ||
| amount: bigint; | ||
| symbol: string; | ||
| }; | ||
| token1: { | ||
| address: string; | ||
| amount: bigint; | ||
| symbol: string; | ||
| }; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Token amount formatting assumes 18 decimals — add decimals to avoid misreporting values
Using toTokens(amount, 18) will display incorrect amounts for non-18-decimal tokens (e.g., USDC 6). This is a correctness issue in a financial UI.
Apply these diffs to thread decimals through and format correctly:
export function ClaimRewardsPage(props: {
assetContractClient: ThirdwebContract;
entrypointContractClient: ThirdwebContract;
reward: NonNullable<Awaited<ReturnType<typeof getValidReward>>>;
unclaimedFees: {
token0: {
address: string;
amount: bigint;
symbol: string;
+ decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
+ decimals: number;
};
};
chainSlug: string;
}) { export function ClaimRewardsPageUI(props: {
unclaimedFees: {
token0: {
address: string;
amount: bigint;
symbol: string;
+ decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
+ decimals: number;
};
}; function TokenReward(props: {
token: {
address: string;
amount: bigint;
symbol: string;
+ decimals: number;
};
client: ThirdwebClient;
chain: Chain;
chainSlug: string;
}) {- <p className="font-bold text-sm">
- {toTokens(props.token.amount, 18)} {props.token.symbol}
- </p>
+ <p className="font-bold text-sm">
+ {toTokens(props.token.amount, props.token.decimals)} {props.token.symbol}
+ </p>Note: You’ll need to pass decimals from the data source (e.g., alongside address/symbol when constructing unclaimedFees).
Also applies to: 94-106, 272-281, 304-306

PR-Codex overview
This PR focuses on enhancing the
thirdwebpackage by introducing new functionalities and updates for managing ERC20 assets, including the implementation of various events, functions, and utility methods for asset deployment and interaction.Detailed summary
initializefunctions to various asset-related contracts.AssetDistributed,RouterUpdated, andRewardLockerUpdated.distributeTokenfunction to handle multiple recipients.Summary by CodeRabbit
New Features
UI / UX
API / Props
Chores