Skip to content

Commit b79c906

Browse files
authored
[Job Launcher] Multitoken (#3164)
* feat: add multitoken support * feat: add InvalidChainId error and update getTokens response structure * feat: update payment module to streamline token handling and improve UI interactions * feat: update API endpoint for fetching available tokens * fix: correct import paths in payment service module * Remove unnecesary check for HMT address and fix lint problems
1 parent b22f74e commit b79c906

19 files changed

Lines changed: 239 additions & 103 deletions

File tree

packages/apps/job-launcher/client/src/components/Icons/chains.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ChainId } from '@human-protocol/sdk';
22
import { ReactElement } from 'react';
33

44
import { BinanceSmartChainIcon } from './BinanceSmartChainIcon';
5+
import { DollarSignIcon } from './DollarSignIcon';
56
import { EthereumIcon } from './EthereumIcon';
67
import { HumanIcon } from './HumanIcon';
78
import { PolygonIcon } from './PolygonIcon';
@@ -18,4 +19,6 @@ export const CHAIN_ICONS: { [chainId in ChainId]?: ReactElement } = {
1819

1920
export const TOKEN_ICONS: Record<string, ReactElement> = {
2021
HMT: <HumanIcon />,
22+
USDC: <DollarSignIcon />,
23+
USDT: <DollarSignIcon />,
2124
};

packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
usePublicClient,
2626
} from 'wagmi';
2727
import { TokenSelect } from '../../../components/TokenSelect';
28-
import { NETWORK_TOKENS } from '../../../constants/chains';
2928
import { useTokenRate } from '../../../hooks/useTokenRate';
3029
import { useCreateJobPageUI } from '../../../providers/CreateJobPageUIProvider';
3130
import * as jobService from '../../../services/job';
@@ -55,7 +54,7 @@ export const CryptoPayForm = ({
5554
const { data: signer } = useWalletClient();
5655
const publicClient = usePublicClient();
5756
const { user } = useAppSelector((state) => state.auth);
58-
const { data: rate } = useTokenRate('hmt', 'usd');
57+
const { data: rate } = useTokenRate(tokenSymbol || 'hmt', 'usd');
5958

6059
useEffect(() => {
6160
const fetchJobLauncherData = async () => {
@@ -103,6 +102,11 @@ export const CryptoPayForm = ({
103102
return totalAmount - accountAmount;
104103
}, [payWithAccountBalance, totalAmount, accountAmount]);
105104

105+
const handleTokenChange = (symbol: string, address: string) => {
106+
setTokenSymbol(symbol);
107+
setTokenAddress(address);
108+
};
109+
106110
const handlePay = async () => {
107111
if (signer && tokenAddress && amount && jobRequest.chainId && tokenSymbol) {
108112
setIsLoading(true);
@@ -225,16 +229,8 @@ export const CryptoPayForm = ({
225229
)}
226230
<TokenSelect
227231
chainId={chain?.id}
228-
value={tokenAddress}
229-
onChange={(e) => {
230-
const symbol = e.target.value as string;
231-
setTokenSymbol(symbol);
232-
setTokenAddress(
233-
NETWORK_TOKENS[
234-
jobRequest.chainId! as keyof typeof NETWORK_TOKENS
235-
]?.[symbol.toLowerCase()],
236-
);
237-
}}
232+
value={tokenSymbol}
233+
onTokenChange={handleTokenChange}
238234
/>
239235
<FormControl fullWidth>
240236
<TextField

packages/apps/job-launcher/client/src/components/Jobs/Create/FiatPayForm.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export const FiatPayForm = ({
8484
const [accountAmount] = useState(
8585
user?.balance ? Number(user?.balance?.amount) : 0,
8686
);
87-
const [tokenAddress, setTokenAddress] = useState<string>();
87+
const [tokenSymbol, setTokenSymbol] = useState<string>();
88+
89+
const handleTokenChange = (symbol: string, address: string) => {
90+
setTokenSymbol(symbol);
91+
};
8892

8993
useEffect(() => {
9094
const fetchJobLauncherData = async () => {
@@ -183,7 +187,7 @@ export const FiatPayForm = ({
183187
return;
184188
}
185189

186-
if (!tokenAddress) {
190+
if (!tokenSymbol) {
187191
onError('Please select a token.');
188192
return;
189193
}
@@ -224,15 +228,15 @@ export const FiatPayForm = ({
224228
fortuneRequest,
225229
CURRENCY.usd,
226230
fundAmount,
227-
tokenAddress,
231+
tokenSymbol,
228232
);
229233
} else if (jobType === JobType.CVAT && cvatRequest) {
230234
await createCvatJob(
231235
chainId,
232236
cvatRequest,
233237
CURRENCY.usd,
234238
fundAmount,
235-
tokenAddress,
239+
tokenSymbol,
236240
);
237241
} else if (jobType === JobType.HCAPTCHA && hCaptchaRequest) {
238242
await createHCaptchaJob(chainId, hCaptchaRequest);
@@ -337,10 +341,8 @@ export const FiatPayForm = ({
337341
)}
338342
<TokenSelect
339343
chainId={jobRequest.chainId!}
340-
value={tokenAddress}
341-
onChange={(e) =>
342-
setTokenAddress(e.target.value as string)
343-
}
344+
value={tokenSymbol}
345+
onTokenChange={handleTokenChange}
344346
/>
345347
</FormControl>
346348
</Grid>
@@ -449,7 +451,7 @@ export const FiatPayForm = ({
449451
!amount ||
450452
(!payWithAccountBalance && !selectedCard) ||
451453
hasError ||
452-
!tokenAddress
454+
!tokenSymbol
453455
}
454456
>
455457
Pay now

packages/apps/job-launcher/client/src/components/TokenSelect/index.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,27 @@ import {
77
Select,
88
SelectProps,
99
} from '@mui/material';
10-
import { FC, useMemo } from 'react';
10+
import { FC, useEffect, useState } from 'react';
1111
import { TOKEN_ICONS } from '../../components/Icons/chains';
12-
import { SUPPORTED_TOKEN_SYMBOLS } from '../../constants';
13-
import { NETWORK_TOKENS } from '../../constants/chains';
12+
import * as paymentService from '../../services/payment';
1413

1514
type TokenSelectProps = SelectProps & {
1615
chainId: ChainId;
16+
onTokenChange: (symbol: string, address: string) => void;
1717
};
1818

1919
export const TokenSelect: FC<TokenSelectProps> = (props) => {
20-
const availableTokens = useMemo(() => {
21-
return SUPPORTED_TOKEN_SYMBOLS.filter(
22-
(symbol) =>
23-
NETWORK_TOKENS[props.chainId as keyof typeof NETWORK_TOKENS]?.[
24-
symbol.toLowerCase()
25-
],
26-
);
20+
const [availableTokens, setAvailableTokens] = useState<{
21+
[key: string]: string;
22+
}>({});
23+
24+
useEffect(() => {
25+
const fetchTokensData = async () => {
26+
const tokens = await paymentService.getTokensAvailable(props.chainId);
27+
setAvailableTokens(tokens);
28+
};
29+
30+
fetchTokensData();
2731
}, [props.chainId]);
2832

2933
return (
@@ -44,17 +48,22 @@ export const TokenSelect: FC<TokenSelectProps> = (props) => {
4448
},
4549
}}
4650
{...props}
51+
onChange={(e) => {
52+
const symbol = e.target.value as string;
53+
const address = availableTokens[symbol];
54+
props.onTokenChange(symbol, address);
55+
}}
4756
>
48-
{availableTokens.map((symbol) => {
49-
const IconComponent = TOKEN_ICONS[symbol];
57+
{Object.keys(availableTokens).map((symbol) => {
58+
const IconComponent = TOKEN_ICONS[symbol.toUpperCase()];
5059
return (
5160
<MenuItem value={symbol} key={symbol}>
5261
{IconComponent && (
5362
<ListItemIcon sx={{ color: '#320a8d' }}>
5463
{IconComponent}
5564
</ListItemIcon>
5665
)}
57-
{symbol}
66+
{symbol.toUpperCase()}
5867
</MenuItem>
5968
);
6069
})}

packages/apps/job-launcher/client/src/components/TopUpAccount/CryptoTopUpForm.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import React, { useMemo, useState } from 'react';
1515
import { Address } from 'viem';
1616
import { useAccount, useWalletClient, usePublicClient } from 'wagmi';
1717
import { TokenSelect } from '../../components/TokenSelect';
18-
import { NETWORK_TOKENS, SUPPORTED_CHAIN_IDS } from '../../constants/chains';
18+
import { SUPPORTED_CHAIN_IDS } from '../../constants/chains';
1919
import { useTokenRate } from '../../hooks/useTokenRate';
2020
import { useSnackbar } from '../../providers/SnackProvider';
2121
import * as paymentService from '../../services/payment';
@@ -27,19 +27,25 @@ export const CryptoTopUpForm = () => {
2727
const { isConnected, chain } = useAccount();
2828
const dispatch = useAppDispatch();
2929
const [tokenAddress, setTokenAddress] = useState<string>();
30+
const [tokenSymbol, setTokenSymbol] = useState<string>();
3031
const [amount, setAmount] = useState<string>();
3132
const [isSuccess, setIsSuccess] = useState(false);
3233
const [isLoading, setIsLoading] = useState(false);
3334
const publicClient = usePublicClient();
3435
const { data: signer } = useWalletClient();
35-
const { data: rate } = useTokenRate('hmt', 'usd');
36+
const { data: rate } = useTokenRate(tokenSymbol || 'hmt', 'usd');
3637
const { showError } = useSnackbar();
3738

3839
const totalAmount = useMemo(() => {
39-
if (!amount) return 0;
40+
if (!amount || !rate) return 0;
4041
return parseFloat(amount) * rate;
4142
}, [amount, rate]);
4243

44+
const handleTokenChange = (symbol: string, address: string) => {
45+
setTokenSymbol(symbol);
46+
setTokenAddress(address);
47+
};
48+
4349
const handleTopUpAccount = async () => {
4450
if (!signer || !chain || !tokenAddress || !amount) return;
4551

@@ -115,15 +121,8 @@ export const CryptoTopUpForm = () => {
115121
)}
116122
<TokenSelect
117123
chainId={chain?.id}
118-
value={tokenAddress}
119-
onChange={(e) => {
120-
const symbol = e.target.value as string;
121-
setTokenAddress(
122-
NETWORK_TOKENS[chain?.id as keyof typeof NETWORK_TOKENS]?.[
123-
symbol.toLowerCase()
124-
],
125-
);
126-
}}
124+
value={tokenSymbol}
125+
onTokenChange={handleTokenChange}
127126
/>
128127
<FormControl fullWidth>
129128
<TextField
@@ -154,7 +153,9 @@ export const CryptoTopUpForm = () => {
154153
justifyContent="space-between"
155154
alignItems="center"
156155
>
157-
<Typography color="text.secondary">HMT Price</Typography>
156+
<Typography color="text.secondary">
157+
{tokenSymbol?.toUpperCase()} Price
158+
</Typography>
158159
<Typography color="text.secondary">
159160
{rate?.toFixed(2)} USD
160161
</Typography>

packages/apps/job-launcher/client/src/services/payment.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { ChainId } from '@human-protocol/sdk';
12
import { WalletClient } from 'viem';
2-
33
import { PAYMENT_SIGNATURE_KEY } from '../constants/payment';
44
import {
55
BillingInfo,
@@ -70,6 +70,12 @@ export const getFee = async () => {
7070
return data;
7171
};
7272

73+
export const getTokensAvailable = async (chainId: ChainId) => {
74+
const { data } = await api.get(`/payment/tokens/${chainId}`);
75+
76+
return data;
77+
};
78+
7379
export const getOperatorAddress = async () => {
7480
const { data } = await api.get('/web3/operator-address');
7581

packages/apps/job-launcher/server/src/common/config/network-config.service.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChainId, NETWORKS } from '@human-protocol/sdk';
1+
import { ChainId } from '@human-protocol/sdk';
22
import { Injectable } from '@nestjs/common';
33
import { ConfigService } from '@nestjs/config';
44
import { Web3Env } from '../enums/web3';
@@ -8,13 +8,9 @@ import {
88
TESTNET_CHAIN_IDS,
99
} from '../constants';
1010

11-
export interface TokensList {
12-
[key: string]: string | undefined;
13-
}
1411
export interface NetworkDto {
1512
chainId: number;
1613
rpcUrl?: string;
17-
tokens: TokensList;
1814
}
1915

2016
interface NetworkMapDto {
@@ -34,10 +30,6 @@ export class NetworkConfigService {
3430
* The RPC URL for the Sepolia network.
3531
*/
3632
rpcUrl: this.configService.get<string>('RPC_URL_SEPOLIA'),
37-
tokens: {
38-
hmt: NETWORKS[ChainId.SEPOLIA]?.hmtAddress,
39-
usdc: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
40-
},
4133
},
4234
}),
4335
...(this.configService.get<string>('RPC_URL_POLYGON') && {
@@ -47,10 +39,6 @@ export class NetworkConfigService {
4739
* The RPC URL for the Polygon network.
4840
*/
4941
rpcUrl: this.configService.get<string>('RPC_URL_POLYGON'),
50-
tokens: {
51-
hmt: NETWORKS[ChainId.POLYGON]?.hmtAddress,
52-
usdt: '0x170a18b9190669cda08965562745a323c907e5ec',
53-
},
5442
},
5543
}),
5644
...(this.configService.get<string>('RPC_URL_POLYGON_AMOY') && {
@@ -60,9 +48,6 @@ export class NetworkConfigService {
6048
* The RPC URL for the Polygon Amoy network.
6149
*/
6250
rpcUrl: this.configService.get<string>('RPC_URL_POLYGON_AMOY'),
63-
tokens: {
64-
hmt: NETWORKS[ChainId.POLYGON_AMOY]?.hmtAddress,
65-
},
6651
},
6752
}),
6853
...(this.configService.get<string>('RPC_URL_BSC_MAINNET') && {
@@ -72,10 +57,6 @@ export class NetworkConfigService {
7257
* The RPC URL for the BSC Mainnet network.
7358
*/
7459
rpcUrl: this.configService.get<string>('RPC_URL_BSC_MAINNET'),
75-
tokens: {
76-
hmt: NETWORKS[ChainId.BSC_MAINNET]?.hmtAddress,
77-
usdt: '0x55d398326f99059fF775485246999027B3197955',
78-
},
7960
},
8061
}),
8162
...(this.configService.get<string>('RPC_URL_BSC_TESTNET') && {
@@ -85,9 +66,6 @@ export class NetworkConfigService {
8566
* The RPC URL for the BSC Testnet network.
8667
*/
8768
rpcUrl: this.configService.get<string>('RPC_URL_BSC_TESTNET'),
88-
tokens: {
89-
hmt: NETWORKS[ChainId.BSC_TESTNET]?.hmtAddress,
90-
},
9169
},
9270
}),
9371
...(this.configService.get<string>('RPC_URL_LOCALHOST') && {
@@ -97,9 +75,6 @@ export class NetworkConfigService {
9775
* The RPC URL for the Localhost network.
9876
*/
9977
rpcUrl: this.configService.get<string>('RPC_URL_LOCALHOST'),
100-
tokens: {
101-
hmt: NETWORKS[ChainId.LOCALHOST]?.hmtAddress,
102-
},
10378
},
10479
}),
10580
};

packages/apps/job-launcher/server/src/common/constants/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export enum ErrorPayment {
107107
UnsupportedToken = 'Unsupported token',
108108
InvalidRecipient = 'Invalid recipient',
109109
ChainIdMissing = 'ChainId is missing',
110+
InvalidChainId = 'Invalid chain id',
110111
}
111112

112113
/**

packages/apps/job-launcher/server/src/common/constants/payment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { ITokenId } from '../interfaces';
33
export const CoingeckoTokenId: ITokenId = {
44
hmt: 'human-protocol',
55
usdt: 'tether',
6+
usdc: 'usd-coin',
67
};

0 commit comments

Comments
 (0)