Skip to content

Commit 7fd00f1

Browse files
committed
feat: parse GRC20 information from Realm file contents as well
1 parent d5bf3fb commit 7fd00f1

File tree

3 files changed

+176
-42
lines changed

3 files changed

+176
-42
lines changed

packages/adena-extension/src/common/utils/parse-utils.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ export const parseGRC20ByABCIRender = (
77
totalSupply: bigint;
88
knownAccounts: bigint;
99
} => {
10+
if (!response) {
11+
throw new Error('failed parse grc20 token render');
12+
}
13+
1014
const regex =
1115
/#\s(?<tokenName>.+)\s\(\$(?<tokenSymbol>.+)\)\s*\* \*\*Decimals\*\*: (?<tokenDecimals>\d+)\s*\* \*\*Total supply\*\*: (?<totalSupply>\d+)\s*\* \*\*Known accounts\*\*: (?<knownAccounts>\d+)/;
1216

1317
const match = response.match(regex);
1418

15-
if (!match || !match.groups) {
19+
if (!match || !match?.groups) {
1620
throw new Error('failed parse grc20 token render');
1721
}
1822

@@ -60,3 +64,34 @@ export const parseReamPathItemsByPath = (
6064
remainPath: remainPathItems.join('/'),
6165
};
6266
};
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/pages/popup/wallet/manage-token-added/index.tsx

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22

3+
import { TokenValidationError } from '@common/errors';
34
import { parseReamPathItemsByPath } from '@common/utils/parse-utils';
5+
import { isGRC20TokenModel } from '@common/validation';
46
import AdditionalToken from '@components/pages/additional-token/additional-token';
57
import { AddingType } from '@components/pages/additional-token/additional-token-type-selector';
68
import { ManageTokenLayout } from '@components/pages/manage-token-layout';
79
import useAppNavigate from '@hooks/use-app-navigate';
10+
import { useDebounce } from '@hooks/use-debounce';
811
import { useGRC20Token } from '@hooks/use-grc20-token';
912
import { useGRC20Tokens } from '@hooks/use-grc20-tokens';
1013
import { useNetwork } from '@hooks/use-network';
@@ -23,50 +26,63 @@ const ManageTokenAddedContainer: React.FC = () => {
2326
const [addingType, setAddingType] = useState(AddingType.SEARCH);
2427
const [manualTokenPath, setManualTokenPath] = useState('');
2528

26-
useEffect(() => {
27-
document.body.addEventListener('click', closeSelectBox);
28-
return () => document.body.removeEventListener('click', closeSelectBox);
29-
}, [document.body]);
30-
31-
useEffect(() => {
32-
if (finished) {
33-
goBack();
34-
}
35-
}, [finished]);
36-
3729
const { data: grc20Tokens } = useGRC20Tokens();
3830

31+
const {
32+
debouncedValue: debouncedManualTokenPath,
33+
setDebouncedValue: setDebouncedManualTokenPath,
34+
isLoading: isLoadingDebounce,
35+
} = useDebounce(manualTokenPath, 500);
3936
const { data: manualGRC20Token, isFetching: isFetchingManualGRC20Token } =
40-
useGRC20Token(manualTokenPath);
37+
useGRC20Token(debouncedManualTokenPath);
38+
39+
const isValidManualGRC20Token = useMemo(() => {
40+
if (manualTokenPath === '') {
41+
return true;
42+
}
4143

42-
const isValidManualGRC20TokenPath = useMemo(() => {
4344
try {
4445
parseReamPathItemsByPath(manualTokenPath);
46+
return true;
4547
} catch {
4648
return false;
4749
}
48-
return true;
4950
}, [manualTokenPath]);
5051

51-
const isLoadingManualGRC20Token = useMemo(() => {
52-
if (!isValidManualGRC20TokenPath) {
53-
return false;
52+
const errorManualGRC20Token = useMemo(() => {
53+
if (manualTokenPath === '') {
54+
return null;
5455
}
5556

56-
return isFetchingManualGRC20Token;
57-
}, [isValidManualGRC20TokenPath, isFetchingManualGRC20Token]);
57+
if (!isValidManualGRC20Token || !manualGRC20Token) {
58+
return new TokenValidationError('INVALID_REALM_PATH');
59+
}
60+
61+
const isRegistered = tokenMetainfos.some((tokenMetaInfo) => {
62+
if (tokenMetaInfo.tokenId === manualTokenPath) {
63+
return true;
64+
}
65+
66+
if (isGRC20TokenModel(tokenMetaInfo)) {
67+
return tokenMetaInfo.pkgPath === manualTokenPath;
68+
}
5869

59-
const isErrorManualGRC20Token = useMemo(() => {
60-
if (manualTokenPath === '') {
6170
return false;
71+
});
72+
if (isRegistered) {
73+
return new TokenValidationError('ALREADY_ADDED');
6274
}
6375

64-
if (isLoadingManualGRC20Token) {
76+
return null;
77+
}, [tokenMetainfos, isLoadingDebounce, manualGRC20Token, manualTokenPath]);
78+
79+
const isLoadingManualGRC20Token = useMemo(() => {
80+
if (!isValidManualGRC20Token) {
6581
return false;
6682
}
6783

68-
return manualGRC20Token === null;
69-
}, [isLoadingManualGRC20Token, manualTokenPath, manualGRC20Token]);
84+
return isLoadingDebounce || isFetchingManualGRC20Token;
85+
}, [isValidManualGRC20Token, isLoadingDebounce, isFetchingManualGRC20Token]);
7086

7187
const tokenInfos: TokenInfo[] = useMemo(() => {
7288
if (!grc20Tokens) {
@@ -120,6 +136,7 @@ const ManageTokenAddedContainer: React.FC = () => {
120136
setAddingType(addingType);
121137
setKeyword('');
122138
setManualTokenPath('');
139+
setDebouncedManualTokenPath('');
123140
setSelectedTokenInfo(null);
124141
setOpened(false);
125142
setSelected(false);
@@ -144,6 +161,10 @@ const ManageTokenAddedContainer: React.FC = () => {
144161
}, []);
145162

146163
const onClickAdd = useCallback(async () => {
164+
if (errorManualGRC20Token) {
165+
return;
166+
}
167+
147168
if (!selected || !selectedTokenInfo || finished) {
148169
return;
149170
}
@@ -152,6 +173,17 @@ const ManageTokenAddedContainer: React.FC = () => {
152173
setFinished(true);
153174
}, [selected, selectedTokenInfo, finished]);
154175

176+
useEffect(() => {
177+
document.body.addEventListener('click', closeSelectBox);
178+
return () => document.body.removeEventListener('click', closeSelectBox);
179+
}, [document.body]);
180+
181+
useEffect(() => {
182+
if (finished) {
183+
goBack();
184+
}
185+
}, [finished]);
186+
155187
useEffect(() => {
156188
if (addingType === AddingType.SEARCH) {
157189
return;
@@ -189,7 +221,7 @@ const ManageTokenAddedContainer: React.FC = () => {
189221
tokenInfos={tokenInfos ?? []}
190222
manualTokenPath={manualTokenPath}
191223
isLoadingManualGRC20Token={isLoadingManualGRC20Token}
192-
isErrorManualGRC20Token={isErrorManualGRC20Token}
224+
errorManualGRC20Token={errorManualGRC20Token}
193225
selectedTokenInfo={selectedTokenInfo}
194226
selectAddingType={selectAddingType}
195227
onChangeKeyword={onChangeKeyword}

packages/adena-extension/src/repositories/common/token.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111

1212
import { GnoProvider } from '@common/provider/gno/gno-provider';
1313
import { makeRPCRequest } from '@common/utils/fetch-utils';
14-
import { parseGRC20ByABCIRender } from '@common/utils/parse-utils';
14+
import { parseGRC20ByABCIRender, parseGRC20ByFileContents } from '@common/utils/parse-utils';
1515
import {
1616
GRC20TokenModel,
1717
IBCNativeTokenModel,
@@ -180,22 +180,28 @@ export class TokenRepository {
180180
throw new Error('Gno provider not initialized.');
181181
}
182182

183-
const { tokenName, tokenSymbol, tokenDecimals } = await this.gnoProvider
184-
.getRenderOutput(packagePath, '')
185-
.then(parseGRC20ByABCIRender);
183+
const fileContents = await this.gnoProvider.getFileContent(packagePath).catch(() => null);
184+
const fileNames = fileContents?.split('\n') || [];
186185

187-
return {
188-
main: false,
189-
tokenId: packagePath,
190-
pkgPath: packagePath,
191-
networkId: this.networkId,
192-
display: false,
193-
type: 'grc20',
194-
name: tokenName,
195-
symbol: tokenSymbol,
196-
decimals: tokenDecimals,
197-
image: '',
198-
};
186+
if (fileContents === null || fileNames.length === 0) {
187+
throw new Error('Not available realm');
188+
}
189+
190+
const renderTokenInfo = await this.fetchGRC20TokenInfoQueryRender(packagePath).catch(
191+
() => null,
192+
);
193+
if (renderTokenInfo) {
194+
return renderTokenInfo;
195+
}
196+
197+
const fileTokenInfo = await this.fetchGRC20TokenInfoQueryFiles(packagePath, fileNames).catch(
198+
() => null,
199+
);
200+
if (fileTokenInfo) {
201+
return fileTokenInfo;
202+
}
203+
204+
throw new Error('Realm is not GRC20');
199205
}
200206

201207
public fetchAllGRC20Tokens = async (): Promise<GRC20TokenModel[]> => {
@@ -284,6 +290,67 @@ export class TokenRepository {
284290
.catch(() => []);
285291
};
286292

293+
private async fetchGRC20TokenInfoQueryRender(
294+
packagePath: string,
295+
): Promise<GRC20TokenModel | null> {
296+
if (!this.gnoProvider) {
297+
throw new Error('Gno provider not initialized.');
298+
}
299+
300+
const { tokenName, tokenSymbol, tokenDecimals } = await this.gnoProvider
301+
.getRenderOutput(packagePath, '')
302+
.then(parseGRC20ByABCIRender);
303+
304+
return {
305+
main: false,
306+
tokenId: packagePath,
307+
pkgPath: packagePath,
308+
networkId: this.networkId,
309+
display: false,
310+
type: 'grc20',
311+
name: tokenName,
312+
symbol: tokenSymbol,
313+
decimals: tokenDecimals,
314+
image: '',
315+
};
316+
}
317+
318+
private async fetchGRC20TokenInfoQueryFiles(
319+
packagePath: string,
320+
fileNames: string[],
321+
): Promise<GRC20TokenModel | null> {
322+
if (!this.gnoProvider) {
323+
throw new Error('Gno provider not initialized.');
324+
}
325+
326+
for (const fileName of fileNames) {
327+
const filePath = [packagePath, fileName].join('/');
328+
const contents = await this.gnoProvider.getFileContent(filePath).catch(() => null);
329+
if (!contents) {
330+
continue;
331+
}
332+
333+
const tokenInfo = parseGRC20ByFileContents(contents);
334+
335+
if (tokenInfo) {
336+
return {
337+
main: false,
338+
tokenId: packagePath,
339+
pkgPath: packagePath,
340+
networkId: this.networkId,
341+
display: false,
342+
type: 'grc20',
343+
name: tokenInfo.tokenName,
344+
symbol: tokenInfo.tokenSymbol,
345+
decimals: tokenInfo.tokenDecimals,
346+
image: '',
347+
};
348+
}
349+
}
350+
351+
return null;
352+
}
353+
287354
private static postRPCRequest = <T = any>(
288355
axiosInstance: AxiosInstance,
289356
url: string,

0 commit comments

Comments
 (0)