Skip to content

Commit 8fe6054

Browse files
committed
Dashboard: Add token selector in asset creation wizard
1 parent 24a556b commit 8fe6054

File tree

12 files changed

+201
-154
lines changed

12 files changed

+201
-154
lines changed

apps/dashboard/src/@/components/blocks/TokenSelector.stories.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,28 @@ function Variant(props: {
3535
label: string;
3636
selectedChainId?: number;
3737
}) {
38-
const [tokenAddress, setTokenAddress] = useState<string>("");
38+
const [token, setToken] = useState<
39+
| {
40+
address: string;
41+
chainId: number;
42+
}
43+
| undefined
44+
>(undefined);
45+
3946
return (
4047
<BadgeContainer label={props.label}>
4148
<TokenSelector
42-
tokenAddress={tokenAddress}
49+
addNativeTokenIfMissing={false}
50+
showCheck={false}
51+
selectedToken={token}
4352
chainId={props.selectedChainId}
4453
client={storybookThirdwebClient}
45-
onChange={setTokenAddress}
54+
onChange={(v) => {
55+
setToken({
56+
address: v.address,
57+
chainId: v.chainId,
58+
});
59+
}}
4660
/>
4761
</BadgeContainer>
4862
);

apps/dashboard/src/@/components/blocks/TokenSelector.tsx

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,103 @@
11
import { useCallback, useMemo } from "react";
2-
import type { ThirdwebClient } from "thirdweb";
2+
import {
3+
NATIVE_TOKEN_ADDRESS,
4+
type ThirdwebClient,
5+
getAddress,
6+
} from "thirdweb";
37
import { shortenAddress } from "thirdweb/utils";
8+
import { useAllChainsData } from "../../../hooks/chains/allChains";
49
import { useTokensData } from "../../../hooks/tokens/tokens";
510
import { replaceIpfsUrl } from "../../../lib/sdk";
611
import { fallbackChainIcon } from "../../../utils/chain-icons";
12+
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
713
import { cn } from "../../lib/utils";
814
import { Badge } from "../ui/badge";
915
import { Img } from "./Img";
1016
import { SelectWithSearch } from "./select-with-search";
1117

1218
type Option = { label: string; value: string };
1319

20+
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
21+
1422
export function TokenSelector(props: {
15-
tokenAddress: string | undefined;
16-
onChange: (tokenAddress: string) => void;
23+
selectedToken: { chainId: number; address: string } | undefined;
24+
onChange: (token: TokenMetadata) => void;
1725
className?: string;
1826
popoverContentClassName?: string;
1927
chainId?: number;
2028
side?: "left" | "right" | "top" | "bottom";
21-
disableChainId?: boolean;
29+
disableAddress?: boolean;
2230
align?: "center" | "start" | "end";
2331
placeholder?: string;
2432
client: ThirdwebClient;
2533
disabled?: boolean;
2634
enabled?: boolean;
35+
showCheck: boolean;
36+
addNativeTokenIfMissing: boolean;
2737
}) {
28-
const { tokens, isFetching } = useTokensData({
38+
const tokensQuery = useTokensData({
2939
chainId: props.chainId,
3040
enabled: props.enabled,
3141
});
3242

43+
const { idToChain } = useAllChainsData();
44+
45+
const tokens = useMemo(() => {
46+
if (!tokensQuery.data) {
47+
return [];
48+
}
49+
50+
if (props.addNativeTokenIfMissing) {
51+
const hasNativeToken = tokensQuery.data.some(
52+
(token) => token.address === checksummedNativeTokenAddress,
53+
);
54+
55+
if (!hasNativeToken && props.chainId) {
56+
return [
57+
{
58+
name:
59+
idToChain.get(props.chainId)?.nativeCurrency.name ??
60+
"Native Token",
61+
symbol:
62+
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
63+
decimals: 18,
64+
chainId: props.chainId,
65+
address: checksummedNativeTokenAddress,
66+
} satisfies TokenMetadata,
67+
...tokensQuery.data,
68+
];
69+
}
70+
}
71+
return tokensQuery.data;
72+
}, [
73+
tokensQuery.data,
74+
props.chainId,
75+
props.addNativeTokenIfMissing,
76+
idToChain,
77+
]);
78+
79+
const addressChainToToken = useMemo(() => {
80+
const value = new Map<string, TokenMetadata>();
81+
for (const token of tokens) {
82+
value.set(`${token.chainId}:${token.address}`, token);
83+
}
84+
return value;
85+
}, [tokens]);
86+
3387
const options = useMemo(() => {
34-
return tokens.allTokens.map((token) => {
35-
return {
36-
label: token.symbol,
37-
value: `${token.chainId}:${token.address}`,
38-
};
39-
});
40-
}, [tokens.allTokens]);
88+
return (
89+
tokens.map((token) => {
90+
return {
91+
label: token.symbol,
92+
value: `${token.chainId}:${token.address}`,
93+
};
94+
}) || []
95+
);
96+
}, [tokens]);
4197

4298
const searchFn = useCallback(
4399
(option: Option, searchValue: string) => {
44-
const token = tokens.addressChainToToken.get(option.value);
100+
const token = addressChainToToken.get(option.value);
45101
if (!token) {
46102
return false;
47103
}
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
55111
token.address.toLowerCase().includes(searchValue.toLowerCase())
56112
);
57113
},
58-
[tokens],
114+
[addressChainToToken],
59115
);
60116

61117
const renderOption = useCallback(
62118
(option: Option) => {
63-
const token = tokens.addressChainToToken.get(option.value);
119+
const token = addressChainToToken.get(option.value);
64120
if (!token) {
65121
return option.label;
66122
}
@@ -87,36 +143,46 @@ export function TokenSelector(props: {
87143
{token.symbol}
88144
</span>
89145

90-
{!props.disableChainId && (
91-
<Badge variant="outline" className="gap-2 max-sm:hidden">
146+
{!props.disableAddress && (
147+
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
92148
<span className="text-muted-foreground">Address</span>
93149
{shortenAddress(token.address, 4)}
94150
</Badge>
95151
)}
96152
</div>
97153
);
98154
},
99-
[tokens, props.disableChainId, props.client],
155+
[addressChainToToken, props.disableAddress, props.client],
100156
);
101157

158+
const selectedValue = props.selectedToken
159+
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
160+
: undefined;
161+
102162
return (
103163
<SelectWithSearch
104164
searchPlaceholder="Search by name or symbol"
105-
value={props.tokenAddress}
165+
value={selectedValue}
106166
options={options}
107167
onValueChange={(tokenAddress) => {
108-
props.onChange(tokenAddress);
168+
const token = addressChainToToken.get(tokenAddress);
169+
if (!token) {
170+
return;
171+
}
172+
props.onChange(token);
109173
}}
110174
closeOnSelect={true}
111-
showCheck={false}
175+
showCheck={props.showCheck}
112176
placeholder={
113-
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
177+
tokensQuery.isPending
178+
? "Loading Tokens..."
179+
: props.placeholder || "Select Token"
114180
}
115181
overrideSearchFn={searchFn}
116182
renderOption={renderOption}
117183
className={props.className}
118184
popoverContentClassName={props.popoverContentClassName}
119-
disabled={isFetching || props.disabled}
185+
disabled={tokensQuery.isPending || props.disabled}
120186
side={props.side}
121187
align={props.align}
122188
/>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/create-token-page-impl.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { defineDashboardChain } from "lib/defineDashboardChain";
1010
import { useRef } from "react";
1111
import { toast } from "sonner";
1212
import {
13+
NATIVE_TOKEN_ADDRESS,
1314
type ThirdwebClient,
15+
getAddress,
1416
getContract,
1517
sendAndConfirmTransaction,
1618
toUnits,
@@ -286,6 +288,11 @@ export function CreateTokenAssetPage(props: {
286288
formValues.saleEnabled && salePercent > 0
287289
? formValues.salePrice
288290
: "0",
291+
currencyAddress:
292+
getAddress(formValues.saleTokenAddress) ===
293+
getAddress(NATIVE_TOKEN_ADDRESS)
294+
? undefined
295+
: formValues.saleTokenAddress,
289296
startTime: new Date(),
290297
metadata: {
291298
name:

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/create-token-page.client.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

3-
import {} from "@/components/blocks/multi-step-status/multi-step-status";
4-
import {} from "@/components/ui/dialog";
53
import { zodResolver } from "@hookform/resolvers/zod";
6-
import {} from "lucide-react";
74
import { useState } from "react";
85
import { useForm } from "react-hook-form";
9-
import type { ThirdwebClient } from "thirdweb";
6+
import {
7+
NATIVE_TOKEN_ADDRESS,
8+
type ThirdwebClient,
9+
getAddress,
10+
} from "thirdweb";
1011
import { TokenDistributionFieldset } from "./distribution/token-distribution";
1112
import {
1213
type CreateAssetFormValues,
@@ -27,6 +28,8 @@ export type CreateTokenFunctions = {
2728
airdropTokens: (values: CreateAssetFormValues) => Promise<void>;
2829
};
2930

31+
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
32+
3033
export function CreateTokenAssetPageUI(props: {
3134
accountAddress: string;
3235
client: ThirdwebClient;
@@ -64,6 +67,7 @@ export function CreateTokenAssetPageUI(props: {
6467
values: {
6568
// sale fieldset
6669
saleAllocationPercentage: "0",
70+
saleTokenAddress: checksummedNativeTokenAddress,
6771
salePrice: "0.1",
6872
supply: "1000000",
6973
saleEnabled: false,
@@ -78,6 +82,13 @@ export function CreateTokenAssetPageUI(props: {
7882
<div>
7983
{step === "token-info" && (
8084
<TokenInfoFieldset
85+
onChainUpdated={() => {
86+
// if the chain is updated, set the sale token address to the native token address
87+
tokenDistributionForm.setValue(
88+
"saleTokenAddress",
89+
checksummedNativeTokenAddress,
90+
);
91+
}}
8192
client={props.client}
8293
form={tokenInfoForm}
8394
onNext={() => {
@@ -88,6 +99,7 @@ export function CreateTokenAssetPageUI(props: {
8899

89100
{step === "distribution" && (
90101
<TokenDistributionFieldset
102+
client={props.client}
91103
form={tokenDistributionForm}
92104
accountAddress={props.accountAddress}
93105
chainId={tokenInfoForm.watch("chain")}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/distribution/token-distribution.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type Segment,
77
} from "@/components/blocks/distribution-chart";
88
import { Input } from "@/components/ui/input";
9-
import {} from "lucide-react";
9+
import type { ThirdwebClient } from "thirdweb";
1010
import { Form } from "../../../../../../../../@/components/ui/form";
1111
import { StepCard } from "../create-token-card";
1212
import type {
@@ -22,6 +22,7 @@ export function TokenDistributionFieldset(props: {
2222
onPrevious: () => void;
2323
form: TokenDistributionForm;
2424
chainId: string;
25+
client: ThirdwebClient;
2526
}) {
2627
const { form } = props;
2728

@@ -60,7 +61,11 @@ export function TokenDistributionFieldset(props: {
6061
</div>
6162

6263
<TokenAirdropSection form={form} />
63-
<TokenSaleSection form={form} chainId={props.chainId} />
64+
<TokenSaleSection
65+
form={form}
66+
chainId={props.chainId}
67+
client={props.client}
68+
/>
6469
</div>
6570
</StepCard>
6671
</form>

0 commit comments

Comments
 (0)