Skip to content

Commit d178cd9

Browse files
authored
feat: add a feature to manually add custom tokens (#604)
1 parent d576c34 commit d178cd9

File tree

30 files changed

+822
-79
lines changed

30 files changed

+822
-79
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const DEFAULT_GAS_WANTED = 10_000_000;
2+
3+
export const TRANSACTION_MESSAGE_SEND_OF_REGISTER = '200000000ugnot';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export * from './address-book-validation-error';
12
export * from './password-validation-error';
3+
export * from './token-validation-error';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { BaseError } from '../base';
2+
3+
const ERROR_VALUE = {
4+
INVALID_REALM_PATH: {
5+
status: 4000,
6+
type: 'INVALID_REALM_PATH',
7+
message: 'Invalid realm path',
8+
},
9+
ALREADY_ADDED: {
10+
status: 4000,
11+
type: 'ALREADY_ADDED',
12+
message: 'Already added',
13+
},
14+
};
15+
16+
type ErrorType = keyof typeof ERROR_VALUE;
17+
18+
export class TokenValidationError extends BaseError {
19+
constructor(errorType: ErrorType) {
20+
super(ERROR_VALUE[errorType]);
21+
Object.setPrototypeOf(this, TokenValidationError.prototype);
22+
}
23+
}

packages/adena-extension/src/common/provider/adena/adena-provider.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import React, { createContext, useMemo } from 'react';
2-
import axios from 'axios';
31
import { AdenaStorage } from '@common/storage';
2+
import { useWindowSize } from '@hooks/use-window-size';
3+
import { ChainRepository } from '@repositories/common';
4+
import { TokenRepository } from '@repositories/common/token';
5+
import { FaucetRepository } from '@repositories/faucet/faucet';
6+
import { TransactionHistoryRepository } from '@repositories/transaction';
47
import {
58
WalletAccountRepository,
69
WalletAddressRepository,
710
WalletEstablishRepository,
811
WalletRepository,
912
} from '@repositories/wallet';
13+
import { FaucetService } from '@services/faucet';
14+
import { ChainService, TokenService } from '@services/resource';
15+
import { TransactionHistoryService, TransactionService } from '@services/transaction';
1016
import {
1117
WalletAccountService,
1218
WalletAddressBookService,
1319
WalletBalanceService,
1420
WalletEstablishService,
1521
WalletService,
1622
} from '@services/wallet';
17-
import { ChainService, TokenService } from '@services/resource';
18-
import { ChainRepository } from '@repositories/common';
19-
import { TransactionHistoryService, TransactionService } from '@services/transaction';
20-
import { TokenRepository } from '@repositories/common/token';
21-
import { TransactionHistoryRepository } from '@repositories/transaction';
22-
import { FaucetService } from '@services/faucet';
23-
import { FaucetRepository } from '@repositories/faucet/faucet';
24-
import { useWindowSize } from '@hooks/use-window-size';
25-
import { useRecoilValue } from 'recoil';
2623
import { NetworkState } from '@states';
24+
import axios from 'axios';
25+
import React, { createContext, useMemo } from 'react';
26+
import { useRecoilValue } from 'recoil';
2727
import { GnoProvider } from '../gno/gno-provider';
2828

2929
export interface AdenaContextProps {
@@ -83,7 +83,7 @@ export const AdenaProvider: React.FC<React.PropsWithChildren<unknown>> = ({ chil
8383
);
8484

8585
const tokenRepository = useMemo(
86-
() => new TokenRepository(localStorage, axiosInstance, currentNetwork),
86+
() => new TokenRepository(localStorage, axiosInstance, currentNetwork, gnoProvider),
8787
[localStorage, axiosInstance, currentNetwork],
8888
);
8989

packages/adena-extension/src/common/provider/gno/gno-provider.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { GnoJSONRPCProvider } from '@gnolang/gno-js-client';
22
import {
3-
BlockInfo,
4-
base64ToUint8Array,
5-
newRequest,
63
ABCIEndpoint,
74
ABCIResponse,
8-
RPCResponse,
9-
parseABCI,
5+
base64ToUint8Array,
6+
BlockInfo,
107
BroadcastTxCommitResult,
118
BroadcastTxSyncResult,
9+
newRequest,
10+
parseABCI,
11+
RPCResponse,
1212
TransactionEndpoint,
1313
} from '@gnolang/tm2-js-client';
1414
import fetchAdapter from '@vespaiach/axios-fetch-adapter';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
export const parseGRC20ByABCIRender = (
2+
response: string,
3+
): {
4+
tokenName: string;
5+
tokenSymbol: string;
6+
tokenDecimals: number;
7+
totalSupply: bigint;
8+
knownAccounts: bigint;
9+
} => {
10+
if (!response) {
11+
throw new Error('failed parse grc20 token render');
12+
}
13+
14+
const regex =
15+
/#\s(?<tokenName>.+)\s\(\$(?<tokenSymbol>.+)\)\s*\* \*\*Decimals\*\*: (?<tokenDecimals>\d+)\s*\* \*\*Total supply\*\*: (?<totalSupply>\d+)\s*\* \*\*Known accounts\*\*: (?<knownAccounts>\d+)/;
16+
17+
const match = response.match(regex);
18+
19+
if (!match || !match?.groups) {
20+
throw new Error('failed parse grc20 token render');
21+
}
22+
23+
return {
24+
tokenName: match.groups.tokenName,
25+
tokenSymbol: match.groups.tokenSymbol,
26+
tokenDecimals: parseInt(match.groups.tokenDecimals, 10),
27+
totalSupply: BigInt(match.groups.totalSupply),
28+
knownAccounts: BigInt(match.groups.knownAccounts),
29+
};
30+
};
31+
32+
/**
33+
* realm's path format: {Domain}/{Type}/{Namespace}/{Remain Path...}
34+
*/
35+
export const parseReamPathItemsByPath = (
36+
realmPath: string,
37+
): {
38+
domain: string;
39+
type: string;
40+
namespace: string;
41+
remainPath: string;
42+
} => {
43+
const pathItems = realmPath.split('/');
44+
if (pathItems.length < 4) {
45+
throw new Error('not available realm path, path size less than 4');
46+
}
47+
48+
const [domain, type, namespace, ...remainPathItems] = pathItems;
49+
50+
const availableDomains = ['gno.land'];
51+
if (!availableDomains.includes(domain)) {
52+
throw new Error('not available realm path, domain is ' + domain);
53+
}
54+
55+
const availableTypes = ['p', 'r'];
56+
if (!availableTypes.includes(type)) {
57+
throw new Error('not available realm path, type is ' + type);
58+
}
59+
60+
return {
61+
domain,
62+
type,
63+
namespace,
64+
remainPath: remainPathItems.join('/'),
65+
};
66+
};
67+
68+
export const parseGRC20ByFileContents = (
69+
contents: string,
70+
): {
71+
tokenName: string;
72+
tokenSymbol: string;
73+
tokenDecimals: number;
74+
} | null => {
75+
const newBankerRegex = /grc20\.NewBanker\(([^)]+)\)/;
76+
const match = contents.match(newBankerRegex);
77+
const matchLine = match?.[1] || null;
78+
79+
if (!matchLine) {
80+
return null;
81+
}
82+
83+
const args = matchLine.split(',').map((arg) => arg.trim());
84+
if (args.length < 3) {
85+
return null;
86+
}
87+
88+
const tokenName = args[0].startsWith('"') ? args[0].slice(1, -1) : args[0];
89+
const tokenSymbol = args[1].startsWith('"') ? args[1].slice(1, -1) : args[1];
90+
const tokenDecimals = isNaN(Number(args[2])) ? 0 : Number(args[2]);
91+
92+
return {
93+
tokenName,
94+
tokenSymbol,
95+
tokenDecimals,
96+
};
97+
};

packages/adena-extension/src/components/pages/additional-token/additional-token-info/additional-token-info.spec.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import React from 'react';
2+
3+
import { GlobalPopupStyle } from '@styles/global-style';
4+
import theme from '@styles/theme';
5+
import { render } from '@testing-library/react';
26
import { RecoilRoot } from 'recoil';
37
import { ThemeProvider } from 'styled-components';
4-
import { render } from '@testing-library/react';
5-
import theme from '@styles/theme';
6-
import { GlobalPopupStyle } from '@styles/global-style';
78
import AdditionalTokenInfo, { AdditionalTokenInfoProps } from './additional-token-info';
89

910
describe('AdditionalTokenInfo Component', () => {
1011
it('AdditionalTokenInfo render', () => {
1112
const args: AdditionalTokenInfoProps = {
13+
isLoading: false,
1214
symbol: 'GNOT',
1315
path: 'gno.land/gnot',
1416
decimals: '6',

packages/adena-extension/src/components/pages/additional-token/additional-token-info/additional-token-info.styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SkeletonBoxStyle } from '@components/atoms';
12
import mixins from '@styles/mixins';
23
import { fonts, getTheme } from '@styles/theme';
34
import styled from 'styled-components';
@@ -37,3 +38,8 @@ export const AdditionalTokenInfoItemWrapper = styled.div`
3738
white-space: nowrap;
3839
}
3940
`;
41+
42+
export const TokenInfoValueLoadingBox = styled(SkeletonBoxStyle)`
43+
width: 40px;
44+
height: 10px;
45+
`;
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,50 @@
11
import React from 'react';
2-
import { AdditionalTokenInfoWrapper, AdditionalTokenInfoItemWrapper } from './additional-token-info.styles';
2+
import {
3+
AdditionalTokenInfoItemWrapper,
4+
AdditionalTokenInfoWrapper,
5+
TokenInfoValueLoadingBox,
6+
} from './additional-token-info.styles';
37

48
export interface AdditionalTokenInfoProps {
9+
isLoading: boolean;
510
symbol: string;
611
path: string;
712
decimals: string;
813
}
914

1015
export interface AdditionalTokenInfoBlockProps {
16+
isLoading: boolean;
1117
title: string;
1218
value: string;
1319
}
1420

1521
const AdditionalTokenInfoBlock: React.FC<AdditionalTokenInfoBlockProps> = ({
1622
title,
1723
value,
24+
isLoading,
1825
}) => {
1926
return (
2027
<AdditionalTokenInfoItemWrapper>
2128
<span className='title'>{title}:</span>
22-
<span className='value'>{value}</span>
29+
30+
{isLoading ? <TokenInfoValueLoadingBox /> : <span className='value'>{value}</span>}
2331
</AdditionalTokenInfoItemWrapper>
2432
);
2533
};
2634

2735
const AdditionalTokenInfo: React.FC<AdditionalTokenInfoProps> = ({
36+
isLoading,
2837
symbol,
2938
path,
3039
decimals,
3140
}) => {
3241
return (
3342
<AdditionalTokenInfoWrapper>
34-
<AdditionalTokenInfoBlock
35-
title='Token Symbol'
36-
value={symbol}
37-
/>
38-
<AdditionalTokenInfoBlock
39-
title='Token Path'
40-
value={path}
41-
/>
42-
<AdditionalTokenInfoBlock
43-
title='Token Decimals'
44-
value={decimals}
45-
/>
43+
<AdditionalTokenInfoBlock title='Token Symbol' value={symbol} isLoading={isLoading} />
44+
<AdditionalTokenInfoBlock title='Token Path' value={path} isLoading={isLoading} />
45+
<AdditionalTokenInfoBlock title='Token Decimals' value={decimals} isLoading={isLoading} />
4646
</AdditionalTokenInfoWrapper>
4747
);
4848
};
4949

50-
export default AdditionalTokenInfo;
50+
export default AdditionalTokenInfo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
3+
import { render } from '@testing-library/react';
4+
import { RecoilRoot } from 'recoil';
5+
import { ThemeProvider } from 'styled-components';
6+
7+
import { GlobalPopupStyle } from '@styles/global-style';
8+
import theme from '@styles/theme';
9+
10+
import AdditionalTokenPathInput, {
11+
AdditionalTokenPathInputProps,
12+
} from './additional-token-path-input';
13+
14+
describe('AdditionalTokenPathInput Component', () => {
15+
it('AdditionalTokenPathInput render', () => {
16+
const args: AdditionalTokenPathInputProps = {
17+
keyword: '',
18+
onChangeKeyword: () => {
19+
return;
20+
},
21+
errorMessage: 'error',
22+
};
23+
24+
render(
25+
<RecoilRoot>
26+
<GlobalPopupStyle />
27+
<ThemeProvider theme={theme}>
28+
<AdditionalTokenPathInput {...args} />
29+
</ThemeProvider>
30+
</RecoilRoot>,
31+
);
32+
});
33+
});

0 commit comments

Comments
 (0)