Skip to content

Commit

Permalink
feat: add a feature to manually add custom tokens (#604)
Browse files Browse the repository at this point in the history
  • Loading branch information
jinoosss authored Oct 25, 2024
1 parent d576c34 commit d178cd9
Show file tree
Hide file tree
Showing 30 changed files with 822 additions and 79 deletions.
3 changes: 3 additions & 0 deletions packages/adena-extension/src/common/constants/tx.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DEFAULT_GAS_WANTED = 10_000_000;

export const TRANSACTION_MESSAGE_SEND_OF_REGISTER = '200000000ugnot';
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './address-book-validation-error';
export * from './password-validation-error';
export * from './token-validation-error';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BaseError } from '../base';

const ERROR_VALUE = {
INVALID_REALM_PATH: {
status: 4000,
type: 'INVALID_REALM_PATH',
message: 'Invalid realm path',
},
ALREADY_ADDED: {
status: 4000,
type: 'ALREADY_ADDED',
message: 'Already added',
},
};

type ErrorType = keyof typeof ERROR_VALUE;

export class TokenValidationError extends BaseError {
constructor(errorType: ErrorType) {
super(ERROR_VALUE[errorType]);
Object.setPrototypeOf(this, TokenValidationError.prototype);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import React, { createContext, useMemo } from 'react';
import axios from 'axios';
import { AdenaStorage } from '@common/storage';
import { useWindowSize } from '@hooks/use-window-size';
import { ChainRepository } from '@repositories/common';
import { TokenRepository } from '@repositories/common/token';
import { FaucetRepository } from '@repositories/faucet/faucet';
import { TransactionHistoryRepository } from '@repositories/transaction';
import {
WalletAccountRepository,
WalletAddressRepository,
WalletEstablishRepository,
WalletRepository,
} from '@repositories/wallet';
import { FaucetService } from '@services/faucet';
import { ChainService, TokenService } from '@services/resource';
import { TransactionHistoryService, TransactionService } from '@services/transaction';
import {
WalletAccountService,
WalletAddressBookService,
WalletBalanceService,
WalletEstablishService,
WalletService,
} from '@services/wallet';
import { ChainService, TokenService } from '@services/resource';
import { ChainRepository } from '@repositories/common';
import { TransactionHistoryService, TransactionService } from '@services/transaction';
import { TokenRepository } from '@repositories/common/token';
import { TransactionHistoryRepository } from '@repositories/transaction';
import { FaucetService } from '@services/faucet';
import { FaucetRepository } from '@repositories/faucet/faucet';
import { useWindowSize } from '@hooks/use-window-size';
import { useRecoilValue } from 'recoil';
import { NetworkState } from '@states';
import axios from 'axios';
import React, { createContext, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { GnoProvider } from '../gno/gno-provider';

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

const tokenRepository = useMemo(
() => new TokenRepository(localStorage, axiosInstance, currentNetwork),
() => new TokenRepository(localStorage, axiosInstance, currentNetwork, gnoProvider),
[localStorage, axiosInstance, currentNetwork],
);

Expand Down
10 changes: 5 additions & 5 deletions packages/adena-extension/src/common/provider/gno/gno-provider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { GnoJSONRPCProvider } from '@gnolang/gno-js-client';
import {
BlockInfo,
base64ToUint8Array,
newRequest,
ABCIEndpoint,
ABCIResponse,
RPCResponse,
parseABCI,
base64ToUint8Array,
BlockInfo,
BroadcastTxCommitResult,
BroadcastTxSyncResult,
newRequest,
parseABCI,
RPCResponse,
TransactionEndpoint,
} from '@gnolang/tm2-js-client';
import fetchAdapter from '@vespaiach/axios-fetch-adapter';
Expand Down
97 changes: 97 additions & 0 deletions packages/adena-extension/src/common/utils/parse-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export const parseGRC20ByABCIRender = (
response: string,
): {
tokenName: string;
tokenSymbol: string;
tokenDecimals: number;
totalSupply: bigint;
knownAccounts: bigint;
} => {
if (!response) {
throw new Error('failed parse grc20 token render');
}

const regex =
/#\s(?<tokenName>.+)\s\(\$(?<tokenSymbol>.+)\)\s*\* \*\*Decimals\*\*: (?<tokenDecimals>\d+)\s*\* \*\*Total supply\*\*: (?<totalSupply>\d+)\s*\* \*\*Known accounts\*\*: (?<knownAccounts>\d+)/;

const match = response.match(regex);

if (!match || !match?.groups) {
throw new Error('failed parse grc20 token render');
}

return {
tokenName: match.groups.tokenName,
tokenSymbol: match.groups.tokenSymbol,
tokenDecimals: parseInt(match.groups.tokenDecimals, 10),
totalSupply: BigInt(match.groups.totalSupply),
knownAccounts: BigInt(match.groups.knownAccounts),
};
};

/**
* realm's path format: {Domain}/{Type}/{Namespace}/{Remain Path...}
*/
export const parseReamPathItemsByPath = (
realmPath: string,
): {
domain: string;
type: string;
namespace: string;
remainPath: string;
} => {
const pathItems = realmPath.split('/');
if (pathItems.length < 4) {
throw new Error('not available realm path, path size less than 4');
}

const [domain, type, namespace, ...remainPathItems] = pathItems;

const availableDomains = ['gno.land'];
if (!availableDomains.includes(domain)) {
throw new Error('not available realm path, domain is ' + domain);
}

const availableTypes = ['p', 'r'];
if (!availableTypes.includes(type)) {
throw new Error('not available realm path, type is ' + type);
}

return {
domain,
type,
namespace,
remainPath: remainPathItems.join('/'),
};
};

export const parseGRC20ByFileContents = (
contents: string,
): {
tokenName: string;
tokenSymbol: string;
tokenDecimals: number;
} | null => {
const newBankerRegex = /grc20\.NewBanker\(([^)]+)\)/;
const match = contents.match(newBankerRegex);
const matchLine = match?.[1] || null;

if (!matchLine) {
return null;
}

const args = matchLine.split(',').map((arg) => arg.trim());
if (args.length < 3) {
return null;
}

const tokenName = args[0].startsWith('"') ? args[0].slice(1, -1) : args[0];
const tokenSymbol = args[1].startsWith('"') ? args[1].slice(1, -1) : args[1];
const tokenDecimals = isNaN(Number(args[2])) ? 0 : Number(args[2]);

return {
tokenName,
tokenSymbol,
tokenDecimals,
};
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';

import { GlobalPopupStyle } from '@styles/global-style';
import theme from '@styles/theme';
import { render } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { ThemeProvider } from 'styled-components';
import { render } from '@testing-library/react';
import theme from '@styles/theme';
import { GlobalPopupStyle } from '@styles/global-style';
import AdditionalTokenInfo, { AdditionalTokenInfoProps } from './additional-token-info';

describe('AdditionalTokenInfo Component', () => {
it('AdditionalTokenInfo render', () => {
const args: AdditionalTokenInfoProps = {
isLoading: false,
symbol: 'GNOT',
path: 'gno.land/gnot',
decimals: '6',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SkeletonBoxStyle } from '@components/atoms';
import mixins from '@styles/mixins';
import { fonts, getTheme } from '@styles/theme';
import styled from 'styled-components';
Expand Down Expand Up @@ -37,3 +38,8 @@ export const AdditionalTokenInfoItemWrapper = styled.div`
white-space: nowrap;
}
`;

export const TokenInfoValueLoadingBox = styled(SkeletonBoxStyle)`
width: 40px;
height: 10px;
`;
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
import React from 'react';
import { AdditionalTokenInfoWrapper, AdditionalTokenInfoItemWrapper } from './additional-token-info.styles';
import {
AdditionalTokenInfoItemWrapper,
AdditionalTokenInfoWrapper,
TokenInfoValueLoadingBox,
} from './additional-token-info.styles';

export interface AdditionalTokenInfoProps {
isLoading: boolean;
symbol: string;
path: string;
decimals: string;
}

export interface AdditionalTokenInfoBlockProps {
isLoading: boolean;
title: string;
value: string;
}

const AdditionalTokenInfoBlock: React.FC<AdditionalTokenInfoBlockProps> = ({
title,
value,
isLoading,
}) => {
return (
<AdditionalTokenInfoItemWrapper>
<span className='title'>{title}:</span>
<span className='value'>{value}</span>

{isLoading ? <TokenInfoValueLoadingBox /> : <span className='value'>{value}</span>}
</AdditionalTokenInfoItemWrapper>
);
};

const AdditionalTokenInfo: React.FC<AdditionalTokenInfoProps> = ({
isLoading,
symbol,
path,
decimals,
}) => {
return (
<AdditionalTokenInfoWrapper>
<AdditionalTokenInfoBlock
title='Token Symbol'
value={symbol}
/>
<AdditionalTokenInfoBlock
title='Token Path'
value={path}
/>
<AdditionalTokenInfoBlock
title='Token Decimals'
value={decimals}
/>
<AdditionalTokenInfoBlock title='Token Symbol' value={symbol} isLoading={isLoading} />
<AdditionalTokenInfoBlock title='Token Path' value={path} isLoading={isLoading} />
<AdditionalTokenInfoBlock title='Token Decimals' value={decimals} isLoading={isLoading} />
</AdditionalTokenInfoWrapper>
);
};

export default AdditionalTokenInfo;
export default AdditionalTokenInfo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

import { render } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { ThemeProvider } from 'styled-components';

import { GlobalPopupStyle } from '@styles/global-style';
import theme from '@styles/theme';

import AdditionalTokenPathInput, {
AdditionalTokenPathInputProps,
} from './additional-token-path-input';

describe('AdditionalTokenPathInput Component', () => {
it('AdditionalTokenPathInput render', () => {
const args: AdditionalTokenPathInputProps = {
keyword: '',
onChangeKeyword: () => {
return;
},
errorMessage: 'error',
};

render(
<RecoilRoot>
<GlobalPopupStyle />
<ThemeProvider theme={theme}>
<AdditionalTokenPathInput {...args} />
</ThemeProvider>
</RecoilRoot>,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Meta, StoryObj } from '@storybook/react';
import AdditionalTokenPathInput, {
type AdditionalTokenPathInputProps,
} from './additional-token-path-input';

export default {
title: 'components/additional-token/AdditionalTokenPathInput',
component: AdditionalTokenPathInput,
} as Meta<typeof AdditionalTokenPathInput>;

export const Default: StoryObj<AdditionalTokenPathInputProps> = {
args: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from 'styled-components';

import { View } from '@components/atoms';
import { fonts, getTheme } from '@styles/theme';

export const AdditionalTokenPathInputWrapper = styled(View)`
width: 100%;
.search-input {
height: 48px;
padding: 13px 16px;
background-color: ${getTheme('neutral', '_9')};
border: 1px solid ${getTheme('neutral', '_7')};
color: ${getTheme('neutral', '_1')};
border-radius: 30px;
${fonts.body2Reg};
&.error {
border-color: ${getTheme('red', '_5')};
}
}
.error-message {
padding: 0 8px;
height: 18px;
}
`;
Loading

0 comments on commit d178cd9

Please sign in to comment.