Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a feature to manually add custom tokens #604

Merged
merged 9 commits into from
Oct 25, 2024
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
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
Loading