From e6ab7d61c52d529c906af4d2156c5bc3b325e68b Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 18 May 2023 16:13:53 +0700 Subject: [PATCH] feat: #27 check balance before minting or creating NFT --- package.json | 2 + src/config/configuration.ts | 2 +- src/enums/queryKeys.enum.ts | 1 + src/hooks/queries/useGetAccountBalance.ts | 48 +++++++++++++++++++ .../AdminNft/ButtonBulkCreateModal/index.tsx | 4 +- .../ButtonCreateModal/NftForm/index.tsx | 25 +++++++++- .../AdminNft/ButtonCreateModal/index.tsx | 3 +- .../ButtonCreateModal/NftForm/index.tsx | 27 +++++++++++ .../ButtonCreateModal/index.tsx | 13 ++--- src/services/casperdash/deploy.ts | 13 +++-- src/services/casperdash/request.ts | 32 +++++++++++++ src/services/casperdash/user/index.ts | 2 + src/services/casperdash/user/type.ts | 32 +++++++++++++ src/services/casperdash/user/user.service.ts | 8 ++++ src/utils/casper/account.ts | 7 +++ src/utils/casper/contract.ts | 2 +- src/utils/format.ts | 21 ++++++++ yarn.lock | 10 ++++ 18 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 src/hooks/queries/useGetAccountBalance.ts create mode 100644 src/services/casperdash/request.ts create mode 100644 src/services/casperdash/user/index.ts create mode 100644 src/services/casperdash/user/type.ts create mode 100644 src/services/casperdash/user/user.service.ts diff --git a/package.json b/package.json index c3cf6584..604c1b38 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@usedapptesthello/usewallet": "0.0.12", "@usedapptesthello/usewallet-core": "0.0.15", "axios": "^1.4.0", + "big.js": "^6.2.1", "casper-js-sdk": "^2.12.1", "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", @@ -83,6 +84,7 @@ "@testing-library/jest-dom": "^5.12.0", "@testing-library/react": "^11.2.7", "@types/axios": "^0.14.0", + "@types/big.js": "^6.1.6", "@types/jest": "^26.0.23", "@types/lodash": "^4.14.194", "@types/node": "^14.6.0", diff --git a/src/config/configuration.ts b/src/config/configuration.ts index d459cd9b..fc283839 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -3,7 +3,7 @@ export const Config = { apiBaseUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.melem.io/v1', metadataBaseUrl: 'http://localhost:4000/v1/nfts', nodeRPCUrl: '/rpc', - deployUrl: 'https://testnet-api.casperdash.io/deploy', + casperDashUrl: 'https://testnet-api.casperdash.io', networkName: 'casper-test', cep78: { contractWASM: diff --git a/src/enums/queryKeys.enum.ts b/src/enums/queryKeys.enum.ts index a0028c4a..b248f2b6 100644 --- a/src/enums/queryKeys.enum.ts +++ b/src/enums/queryKeys.enum.ts @@ -8,4 +8,5 @@ export enum QueryKeys { CAMPAIGNS = 'campaigns', CHECK_PHONE_VERFIED = 'check-phone-verfied', MY_PROFILE = 'my-profile', + ACCOUNT_BALANCE = 'account-balance', } diff --git a/src/hooks/queries/useGetAccountBalance.ts b/src/hooks/queries/useGetAccountBalance.ts new file mode 100644 index 00000000..4f765e3c --- /dev/null +++ b/src/hooks/queries/useGetAccountBalance.ts @@ -0,0 +1,48 @@ +import _get from 'lodash/get'; +import { useQuery, UseQueryOptions } from 'react-query'; + +import { QueryKeys } from '@/enums/queryKeys.enum'; +import { getAccounts } from '@/services/casperdash/user'; +import { hexToNumber } from '@/utils/format'; + +type GetAccountBalanceResponse = { + balanace: number; +}; + +export const useGetAccountBalance = ( + { publicKey }: { publicKey?: string | null }, + options?: Omit< + UseQueryOptions< + GetAccountBalanceResponse, + unknown, + GetAccountBalanceResponse, + [QueryKeys.ACCOUNT_BALANCE, string | null | undefined] + >, + 'queryKey' | 'queryFn' + > +) => { + return useQuery( + [QueryKeys.ACCOUNT_BALANCE, publicKey], + async () => { + const accounts = await getAccounts({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + publicKeys: [publicKey!], + }); + if (!accounts || accounts.length === 0) { + throw new Error('Can not get account'); + } + + const [account] = accounts; + const balanceHex = _get(account, 'balance.hex', ''); + + return { + balanace: hexToNumber(balanceHex), + }; + }, + { + ...options, + enabled: !!publicKey, + refetchOnWindowFocus: true, + } + ); +}; diff --git a/src/modules/AdminNft/ButtonBulkCreateModal/index.tsx b/src/modules/AdminNft/ButtonBulkCreateModal/index.tsx index 35ef3815..44ea247b 100644 --- a/src/modules/AdminNft/ButtonBulkCreateModal/index.tsx +++ b/src/modules/AdminNft/ButtonBulkCreateModal/index.tsx @@ -34,7 +34,7 @@ const ButtonBulkCreateModal = () => { return ( { - + Bulk Create NFT diff --git a/src/modules/AdminNft/ButtonCreateModal/NftForm/index.tsx b/src/modules/AdminNft/ButtonCreateModal/NftForm/index.tsx index 0da3ae10..b1cd7a17 100644 --- a/src/modules/AdminNft/ButtonCreateModal/NftForm/index.tsx +++ b/src/modules/AdminNft/ButtonCreateModal/NftForm/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { LoadingButton } from '@mui/lab'; +import { Divider } from '@mui/material'; import Box from '@mui/material/Box'; import { useAccount } from '@usedapptesthello/usewallet'; import { @@ -17,6 +18,7 @@ import { CEP78ClientInstance } from '@/contracts/cep78'; import { QueryKeys } from '@/enums/queryKeys.enum'; import { useMutateCreateNft } from '@/hooks/mutations'; import { useGetBenefits, useGetAllNftCollections } from '@/hooks/queries'; +import { useGetAccountBalance } from '@/hooks/queries/useGetAccountBalance'; import { CreateNftParams } from '@/services/admin/nft/types'; import { Benefit } from '@/types/benefit'; import { NftCollection } from '@/types/nft-collection'; @@ -25,11 +27,16 @@ type NftFormProps = { onSuccess: () => void; }; +const ESTIMATE_FEE = 20; + const NftForm = ({ onSuccess }: NftFormProps) => { const queryClient = useQueryClient(); const [isLoading, setIsLoading] = useState(false); const [isDisabled, setIsDisabled] = useState(false); const { publicKey } = useAccount(); + const { data: { balanace = 0 } = { balanace: 0 } } = useGetAccountBalance({ + publicKey, + }); const { data: { items = [] } = { items: [], total: 0 }, @@ -96,6 +103,13 @@ const NftForm = ({ onSuccess }: NftFormProps) => { return ( + + + Your Balance: + {balanace} CSPR + + + { })} /> + + + + + Estimate Fee: + {ESTIMATE_FEE} CSPR + + + { - + + Create NFT diff --git a/src/modules/AdminNftCollection/ButtonCreateModal/NftForm/index.tsx b/src/modules/AdminNftCollection/ButtonCreateModal/NftForm/index.tsx index 3a69d8f0..65c12bde 100644 --- a/src/modules/AdminNftCollection/ButtonCreateModal/NftForm/index.tsx +++ b/src/modules/AdminNftCollection/ButtonCreateModal/NftForm/index.tsx @@ -1,5 +1,7 @@ import { LoadingButton } from '@mui/lab'; +import { Divider } from '@mui/material'; import Box from '@mui/material/Box'; +import { useAccount } from '@usedapptesthello/usewallet'; import { FormContainer, SelectElement } from 'react-hook-form-mui'; import { useQueryClient } from 'react-query'; @@ -8,6 +10,7 @@ import ToastMessage from '@/components/Toast'; import { ContractType } from '@/enums/contractType.enum'; import { QueryKeys } from '@/enums/queryKeys.enum'; import { useMutateCreateNftCollection } from '@/hooks/mutations'; +import { useGetAccountBalance } from '@/hooks/queries/useGetAccountBalance'; import SelectBenefitsField from '@/modules/core/SelectBenefitsField'; import { CreateNftCollectionParams } from '@/services/admin/nft-collection/types'; @@ -15,6 +18,8 @@ type NftFormProps = { onSuccess?: () => void; }; +const ESTIMATE_FEE = 250; + const NftForm = ({ onSuccess }: NftFormProps) => { const queryClient = useQueryClient(); const createNftCollectionMutation = useMutateCreateNftCollection({ @@ -29,6 +34,10 @@ const NftForm = ({ onSuccess }: NftFormProps) => { onSuccess?.(); }, }); + const { publicKey } = useAccount(); + const { data: { balanace = 0 } = { balanace: 0 } } = useGetAccountBalance({ + publicKey, + }); const handleOnSubmitForm = async ( createNftCollectionParams: CreateNftCollectionParams @@ -46,6 +55,13 @@ const NftForm = ({ onSuccess }: NftFormProps) => { }} onSuccess={handleOnSubmitForm} > + + + Your Balance: + {balanace} CSPR + + + @@ -68,8 +84,19 @@ const NftForm = ({ onSuccess }: NftFormProps) => { + + + + Estimate Fee: + {ESTIMATE_FEE} CSPR + + + { return ( - + - - Create Nft Collection + + Create NFT Collection diff --git a/src/services/casperdash/deploy.ts b/src/services/casperdash/deploy.ts index 9223d914..3c3145b9 100644 --- a/src/services/casperdash/deploy.ts +++ b/src/services/casperdash/deploy.ts @@ -1,11 +1,10 @@ -import axios from 'axios'; - -import { Config } from '@/config'; +import request from './request'; export const deploy = async (signedDeploy: unknown) => { - const { - data: { deployHash }, - } = await axios.post(Config.deployUrl, signedDeploy); + const data = await request.post<{ deployHash: string }>( + '/deploy', + signedDeploy + ); - return deployHash; + return (<{ deployHash: string }>(data)).deployHash; }; diff --git a/src/services/casperdash/request.ts b/src/services/casperdash/request.ts new file mode 100644 index 00000000..069f4b7d --- /dev/null +++ b/src/services/casperdash/request.ts @@ -0,0 +1,32 @@ +import axios, { AxiosResponse } from 'axios'; +import qs from 'qs'; + +import { Config } from '@/config'; + +const casperDashRequest = axios.create({ + baseURL: Config.casperDashUrl, + timeout: 100 * 1000, + paramsSerializer: { + serialize: (params: Record) => + qs.stringify(params, { arrayFormat: 'repeat' }), + }, +}); + +casperDashRequest.interceptors.response.use( + (response: AxiosResponse) => response.data, + (error) => { + const { status } = error.response; + + if (status === 400) { + const { + data: { message }, + } = error.response; + + alert(message); + } + + return Promise.reject(error); + } +); + +export default casperDashRequest; diff --git a/src/services/casperdash/user/index.ts b/src/services/casperdash/user/index.ts new file mode 100644 index 00000000..ec1f03e9 --- /dev/null +++ b/src/services/casperdash/user/index.ts @@ -0,0 +1,2 @@ +export * from './type'; +export * from './user.service'; diff --git a/src/services/casperdash/user/type.ts b/src/services/casperdash/user/type.ts new file mode 100644 index 00000000..b9798f29 --- /dev/null +++ b/src/services/casperdash/user/type.ts @@ -0,0 +1,32 @@ +export type Account = { + _accountHash: string; + namedKeys: { + name: string; + key: string; + }[]; + mainPurse: string; + associatedKeys: { + accountHash: string; + weight: number; + }[]; + actionThresholds: { + deployment: number; + keyManagement: number; + }; + balance: { + hex: string; + type: string; + }; + publicKey: string; +}; + +export type GetAccountsResponse = Account[]; + +export type GetAccountsParams = { + publicKeys: string[]; +}; + +export type TBalance = { + hex: string; + type: string; +}; diff --git a/src/services/casperdash/user/user.service.ts b/src/services/casperdash/user/user.service.ts new file mode 100644 index 00000000..2abf6428 --- /dev/null +++ b/src/services/casperdash/user/user.service.ts @@ -0,0 +1,8 @@ +import { GetAccountsParams, GetAccountsResponse } from './type'; +import request from '../request'; + +export const getAccounts = async ({ + publicKeys, +}: GetAccountsParams): Promise => { + return request.post('/users', { publicKeys }); +}; diff --git a/src/utils/casper/account.ts b/src/utils/casper/account.ts index 1335ae72..795d82b4 100644 --- a/src/utils/casper/account.ts +++ b/src/utils/casper/account.ts @@ -20,6 +20,13 @@ export const getAccountInfo = async ( return blockState.Account; }; +export const getAccount = async (publicKeyHex: string) => { + const cliPublicKey = CLPublicKey.fromHex(publicKeyHex); + const accountInfo = await getAccountInfo(cliPublicKey); + + return accountInfo; +}; + /** * Returns a value under an on-chain account's storage. * @param accountInfo - On-chain account's info. diff --git a/src/utils/casper/contract.ts b/src/utils/casper/contract.ts index 00643163..9e806700 100644 --- a/src/utils/casper/contract.ts +++ b/src/utils/casper/contract.ts @@ -115,7 +115,7 @@ export const signDeployNft = async ( name, tokenAddress, tokenId, - paymentAmount = '30000000000', + paymentAmount = '20000000000', }: SignDeployNftParams, { isWaiting = false }: { isWaiting: boolean } = { isWaiting: false } ) => { diff --git a/src/utils/format.ts b/src/utils/format.ts index c75411a8..f9a7b266 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,3 +1,24 @@ +import Big from 'big.js'; +import { BigNumber } from 'ethers'; +const MOTE_RATE = 1000000000; + export const formatAddress = (address?: string) => { return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''; }; + +export const toCSPR = (rawAmount: number) => { + try { + const amount = Big(rawAmount) + .div(MOTE_RATE) + .round(0, Big.roundDown) + .toNumber(); + + return amount; + } catch (error) { + return 0; + } +}; + +export const hexToNumber = (balanceHex: string): number => { + return balanceHex ? toCSPR(BigNumber.from(balanceHex).toNumber()) : 0; +}; diff --git a/yarn.lock b/yarn.lock index af118414..36a14dcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2398,6 +2398,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/big.js@^6.1.6": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.6.tgz#3d417e758483d55345a03a087f7e0c87137ca444" + integrity sha512-0r9J+Zz9rYm2hOTwiMAVkm3XFQ4u5uTK37xrQMhc9bysn/sf/okzovWMYYIBMFTn/yrEZ11pusgLEaoarTlQbA== + "@types/bn.js@*": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" @@ -3195,6 +3200,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +big.js@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" + integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"