Skip to content

Commit 66aaab4

Browse files
feat: add TokenSelect and CryptoInput Components (#916)
* feat: refer antdesign select component refactor TokenSelect * feat: refactor TokenSelect component docs; update CryptoInput component style; CI error fix * feat: refactor TokenSelect component props; CryptoInput component add default balance footer; improve types docs * test: fill TokenSelect and CryptoInput component test case * feat: add CryptoInput test cases and docs;fix some docs errors * feat: add USDT/ETH token assets; improve CryptoInput docs; refactor demos with Token assets * docs: add changeset * feat: modify CryptoInput demo * feat: refactor CryptoInput component value and balance type, support bigint auto format * feat: CryptoInput component add swap mode demo and docs improve * Update .changeset/lucky-toes-film.md Co-authored-by: 愚指导 <[email protected]> * feat: refactor assets/token export method; CryptoInput component use unique decimal instance; * feat: refactor CryptoInput value & footer & header type * feat: CryptoInput Component support size change * feat: CryptoInput component size change support * feat: CryptoInput component add different size style * feat: CryptoInput Component add lineHeight style * test: CryptoInput component test case improve * test: improve test case * feat: remove CryptoInput Component ConfigProvider * chore: sync pnpm-lock.yaml --------- Co-authored-by: 愚指导 <[email protected]>
1 parent f1b85b9 commit 66aaab4

34 files changed

+1425
-17
lines changed

.changeset/lucky-toes-film.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ant-design/web3-assets': minor
3+
'@ant-design/web3-common': minor
4+
'@ant-design/web3': minor
5+
---
6+
7+
feat: new components TokenSelect and CryptoInput

packages/assets/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
"import": "./dist/esm/solana/index.js",
2121
"require": "./dist/lib/solana/index.js",
2222
"types": "./dist/esm/solana/index.d.ts"
23+
},
24+
"./tokens": {
25+
"import": "./dist/esm/tokens/index.js",
26+
"require": "./dist/lib/tokens/index.js",
27+
"types": "./dist/esm/tokens/index.d.ts"
2328
}
2429
},
2530
"sideEffects": false,

packages/assets/src/tokens/eth.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type Token } from '@ant-design/web3-common';
2+
import { EthereumColorful } from '@ant-design/web3-icons';
3+
4+
import { BSC, Mainnet } from '../chains/ethereum';
5+
6+
export const ETH: Token = {
7+
name: 'Ethereum',
8+
symbol: 'ETH',
9+
decimal: 18,
10+
icon: <EthereumColorful />,
11+
availableChains: [
12+
{
13+
chain: Mainnet,
14+
},
15+
{
16+
chain: BSC,
17+
contract: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
18+
},
19+
],
20+
};

packages/assets/src/tokens/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './eth';
2+
export * from './usdt';

packages/assets/src/tokens/usdt.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { type Token } from '@ant-design/web3-common';
2+
import { USDTColorful } from '@ant-design/web3-icons';
3+
4+
import { Arbitrum, BSC, Mainnet, Optimism, Polygon } from '../chains/ethereum';
5+
6+
export const USDT: Token = {
7+
name: 'Tether USD',
8+
symbol: 'USDT',
9+
decimal: 6,
10+
icon: <USDTColorful />,
11+
availableChains: [
12+
{
13+
chain: Mainnet,
14+
contract: '0xdac17f958d2ee523a2206206994597c13d831ec7',
15+
},
16+
{
17+
chain: Polygon,
18+
contract: '0x3813e82e6f7098b9583FC0F33a962D02018B6803',
19+
},
20+
{
21+
chain: BSC,
22+
contract: '0x55d398326f99059fF775485246999027B3197955',
23+
},
24+
{
25+
chain: Arbitrum,
26+
contract: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
27+
},
28+
{
29+
chain: Optimism,
30+
contract: '0x7f5c764cbc14f9669b88837ca1490cca17c31607',
31+
},
32+
],
33+
};

packages/common/src/locale/en_US.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ const localeValues: RequiredLocale = {
5555
copyTips: 'Copy Address',
5656
copiedTips: 'Address Copied!',
5757
},
58+
TokenSelect: {
59+
placeholder: 'Please select token',
60+
},
61+
CryptoInput: {
62+
placeholder: 'Please enter amount',
63+
maxButtonText: 'Max',
64+
},
5865
};
5966

6067
export default localeValues;

packages/common/src/locale/zh_CN.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ const localeValues: RequiredLocale = {
5252
copyTips: '复制地址',
5353
copiedTips: '地址复制成功!',
5454
},
55+
TokenSelect: {
56+
placeholder: '请选择代币',
57+
},
58+
CryptoInput: {
59+
placeholder: '请输入代币数量',
60+
maxButtonText: '最大',
61+
},
5562
};
5663

5764
export default localeValues;

packages/common/src/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,22 @@ export interface RequiredLocale {
266266
copyTips: string;
267267
copiedTips: string;
268268
};
269+
TokenSelect: {
270+
placeholder: string;
271+
};
272+
CryptoInput: {
273+
placeholder: string;
274+
maxButtonText: string;
275+
};
269276
}
270277

271278
export interface Locale {
272279
ConnectButton?: Partial<RequiredLocale['ConnectButton']>;
273280
ConnectModal?: Partial<RequiredLocale['ConnectModal']>;
274281
NFTCard?: Partial<RequiredLocale['NFTCard']>;
275282
Address?: Partial<RequiredLocale['Address']>;
283+
TokenSelect?: Partial<RequiredLocale['TokenSelect']>;
284+
CryptoInput?: Partial<RequiredLocale['CryptoInput']>;
276285
}
277286

278287
export interface UniversalEIP6963Config {
@@ -286,6 +295,6 @@ export type Token = {
286295
decimal: number;
287296
availableChains: {
288297
chain: Chain;
289-
contract: string;
298+
contract?: string;
290299
}[];
291300
};

packages/web3/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@
5151
"antd": "^5.17.3",
5252
"bs58": "^5.0.0",
5353
"classnames": "^2.5.1",
54-
"copy-to-clipboard": "^3.3.3"
54+
"copy-to-clipboard": "^3.3.3",
55+
"decimal.js": "^10.4.3"
5556
},
5657
"devDependencies": {
5758
"@ant-design/web3-bitcoin": "workspace:*",
59+
"@ant-design/web3-eth-web3js": "workspace:*",
5860
"@ant-design/web3-ethers": "workspace:*",
5961
"@ant-design/web3-solana": "workspace:*",
6062
"@ant-design/web3-wagmi": "workspace:*",
61-
"@ant-design/web3-eth-web3js": "workspace:*",
6263
"@types/react": "^18.2.78",
6364
"@types/react-dom": "^18.2.25",
6465
"father": "^4.4.4",
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import React, { useState } from 'react';
2+
import { ETH, USDT } from '@ant-design/web3-assets/tokens';
3+
import { fireEvent, render } from '@testing-library/react';
4+
import { describe, expect, it, vi } from 'vitest';
5+
6+
import type { CryptoInputProps } from '..';
7+
import { CryptoInput } from '..';
8+
9+
// Mock tokens
10+
const mockTokens = [ETH, USDT];
11+
12+
describe('CryptoInput component', () => {
13+
it('should render the component with placeholder text', () => {
14+
const { baseElement } = render(<CryptoInput tokenList={mockTokens} />);
15+
16+
expect(baseElement.querySelector('.ant-input-number-input')?.getAttribute('placeholder')).toBe(
17+
'Please enter amount',
18+
);
19+
});
20+
21+
it('should display correct header', () => {
22+
// no header
23+
const { baseElement, rerender } = render(<CryptoInput tokenList={mockTokens} />);
24+
25+
expect(baseElement.querySelector('.web3-crypto-input-wrapper')?.children?.length).toBe(2);
26+
expect(baseElement.querySelector('.web3-crypto-input-header')).toBeNull();
27+
28+
// custom header
29+
rerender(
30+
<CryptoInput
31+
tokenList={mockTokens}
32+
header={<div className="custom-header">Custom header</div>}
33+
/>,
34+
);
35+
36+
expect(baseElement.querySelector('.web3-crypto-input-wrapper')?.children?.length).toBe(3);
37+
38+
const headerEle = baseElement.querySelector('.web3-crypto-input-header');
39+
40+
expect(headerEle).not.toBeNull();
41+
expect(headerEle!.textContent).toBe('Custom header');
42+
});
43+
44+
it('should display correct footer', () => {
45+
// close footer
46+
const { baseElement, rerender } = render(<CryptoInput tokenList={mockTokens} footer={false} />);
47+
48+
expect(baseElement.querySelector('.web3-crypto-input-wrapper')?.children?.length).toBe(1);
49+
50+
// custom footer
51+
rerender(
52+
<CryptoInput
53+
tokenList={mockTokens}
54+
footer={<div className="custom-footer">Custom footer</div>}
55+
/>,
56+
);
57+
58+
expect(baseElement.querySelector('.web3-crypto-input-wrapper')?.children?.length).toBe(2);
59+
expect(baseElement.querySelector('.custom-footer')).not.toBeNull();
60+
expect(baseElement.querySelector('.custom-footer')?.textContent).toBe('Custom footer');
61+
62+
// default footer
63+
rerender(<CryptoInput tokenList={mockTokens} />);
64+
65+
expect(baseElement.querySelector('.web3-crypto-input-footer')).not.toBeNull();
66+
});
67+
68+
it('should display the token list when clicked', () => {
69+
const { baseElement } = render(<CryptoInput tokenList={mockTokens} />);
70+
71+
fireEvent.mouseDown(baseElement.querySelector('.ant-select-selector') as Element);
72+
73+
const selectOptions = baseElement.querySelectorAll('.ant-select-item');
74+
75+
expect(selectOptions.length).toBe(2);
76+
77+
expect(selectOptions[0].textContent).includes('Ethereum');
78+
expect(selectOptions[1].textContent).includes('Tether USD');
79+
});
80+
81+
it('should call onChange with selected token and amount input', () => {
82+
const TestComponent = (props: CryptoInputProps) => {
83+
const [crypto, setCrypto] = useState<CryptoInputProps['value']>();
84+
85+
return (
86+
<CryptoInput
87+
tokenList={mockTokens}
88+
value={crypto}
89+
onChange={(newCrypto) => {
90+
setCrypto(newCrypto);
91+
92+
props.onChange?.(newCrypto);
93+
}}
94+
/>
95+
);
96+
};
97+
98+
const handleChange = vi.fn();
99+
100+
const { baseElement } = render(<TestComponent onChange={handleChange} />);
101+
102+
fireEvent.mouseDown(baseElement.querySelector('.ant-select-selector') as Element);
103+
104+
const selectOptions = baseElement.querySelectorAll('.ant-select-item');
105+
106+
fireEvent.click(selectOptions[0]);
107+
108+
expect(handleChange).toHaveBeenCalledWith({ token: mockTokens[0] });
109+
110+
fireEvent.change(baseElement.querySelector('.ant-input-number-input') as Element, {
111+
target: { value: '10' },
112+
});
113+
114+
expect(handleChange).toHaveBeenCalledWith({
115+
token: mockTokens[0],
116+
amount: 10000000000000000000n,
117+
inputString: '10',
118+
});
119+
});
120+
121+
it('should correct handle token decimals', () => {
122+
const TestComponent = (props: CryptoInputProps) => {
123+
const [crypto, setCrypto] = useState<CryptoInputProps['value']>();
124+
125+
return (
126+
<CryptoInput
127+
tokenList={mockTokens}
128+
value={crypto}
129+
onChange={(newCrypto) => {
130+
setCrypto(newCrypto);
131+
132+
props.onChange?.(newCrypto);
133+
}}
134+
/>
135+
);
136+
};
137+
138+
const handleChange = vi.fn();
139+
140+
const { baseElement } = render(<TestComponent onChange={handleChange} />);
141+
142+
fireEvent.mouseDown(baseElement.querySelector('.ant-select-selector') as Element);
143+
144+
const selectOptions = baseElement.querySelectorAll('.ant-select-item');
145+
146+
fireEvent.click(selectOptions[0]);
147+
148+
const inputEle = baseElement.querySelector('.ant-input-number-input') as Element;
149+
150+
/**
151+
* check token amount value
152+
* first input some value and the input element should display the same value
153+
* then check onChange callback is called with correct amount
154+
* then blur the input element, when input value decimals is over token decimals, it should cut correctly
155+
*/
156+
function checkValue(orginInputValue: string, expectInputValue: string, expectAmount: bigint) {
157+
fireEvent.change(inputEle, {
158+
target: { value: orginInputValue },
159+
});
160+
161+
expect(inputEle.getAttribute('value')).toBe(orginInputValue);
162+
163+
expect(handleChange).toHaveBeenCalledWith({
164+
token: mockTokens[0],
165+
amount: expectAmount,
166+
inputString: expectInputValue,
167+
});
168+
169+
fireEvent.blur(inputEle);
170+
171+
expect(inputEle.getAttribute('value')).toBe(expectInputValue);
172+
}
173+
174+
// smaller than token decimals
175+
checkValue('0.012345678', '0.012345678', 12345678000000000n);
176+
177+
// equal to token decimals
178+
checkValue('0.012345678901234567', '0.012345678901234567', 12345678901234567n);
179+
180+
// over token decimals and cut correctly
181+
checkValue('0.01234567890123456789', '0.012345678901234567', 12345678901234567n);
182+
});
183+
184+
it('should calculate correct total price', () => {
185+
const TestComponent = (props: CryptoInputProps) => {
186+
const [crypto, setCrypto] = useState<CryptoInputProps['value']>({ token: mockTokens[0] });
187+
188+
return (
189+
<CryptoInput
190+
tokenList={mockTokens}
191+
value={crypto}
192+
onChange={setCrypto}
193+
balance={{ amount: 100000000000000000000n, unit: '$', price: 3894.57 }}
194+
{...props}
195+
/>
196+
);
197+
};
198+
199+
const { baseElement, rerender } = render(<TestComponent />);
200+
201+
// set token amount to 10
202+
fireEvent.change(baseElement.querySelector('.ant-input-number-input') as Element, {
203+
target: { value: '10.012345678' },
204+
});
205+
206+
expect(baseElement.querySelector('.total-price')?.textContent).toBe('$ 38993.78110716846');
207+
expect(baseElement.querySelector('.token-balance')?.textContent).includes('100');
208+
209+
// change token amount to max
210+
fireEvent.click(baseElement.querySelector('.max-button') as Element);
211+
212+
expect(baseElement.querySelector('.ant-input-number-input')?.getAttribute('value')).toBe('100');
213+
expect(baseElement.querySelector('.total-price')?.textContent).toBe('$ 389457');
214+
215+
// set token amount to null
216+
fireEvent.change(baseElement.querySelector('.ant-input-number-input') as Element, {
217+
target: { value: null },
218+
});
219+
220+
expect(baseElement.querySelector('.total-price')?.textContent).toBe('-');
221+
222+
// change token balance to undefined
223+
rerender(<TestComponent balance={undefined} />);
224+
expect(baseElement.querySelector('.total-price')?.textContent).toBe('-');
225+
});
226+
});

0 commit comments

Comments
 (0)