From 893ae57f71b4b4c0d0007e89d52cc7a5db06bd60 Mon Sep 17 00:00:00 2001 From: Nikita Yutanov Date: Tue, 30 Jul 2024 18:54:16 +0300 Subject: [PATCH] feat(idea/frontend): program balance (#1604) --- .../src/features/balance/assets/plus.svg | 3 + idea/frontend/src/features/balance/index.ts | 4 +- .../balance/ui/balance/balance.module.scss | 10 ++ .../features/balance/ui/balance/balance.tsx | 26 +++++ .../src/features/balance/ui/balance/index.ts | 3 + .../frontend/src/features/balance/ui/index.ts | 3 +- .../balance/ui/program-balance/index.ts | 3 + .../program-balance.module.scss | 5 + .../ui/program-balance/program-balance.tsx | 35 ++++++ .../transfer-balance-modal.module.scss | 13 +++ .../transfer-balance-modal.tsx | 103 ++++++++++++------ .../ui/program-table/program-table.tsx | 17 +-- idea/frontend/src/features/voucher/consts.ts | 14 +-- .../issue-voucher-modal.tsx | 5 +- .../update-voucher-modal.tsx | 5 +- .../ui/accounts-modal/accounts-modal.tsx | 8 +- idea/frontend/src/hooks/index.ts | 2 - idea/frontend/src/hooks/use-sign-and-send.tsx | 38 ++++--- idea/frontend/src/hooks/useBalanceTransfer.ts | 64 ----------- idea/frontend/src/pages/program/program.tsx | 8 +- idea/frontend/src/shared/config/consts.ts | 13 +++ .../src/widgets/header/ui/topSide/TopSide.tsx | 33 +++--- yarn.lock | 29 +---- 23 files changed, 247 insertions(+), 197 deletions(-) create mode 100644 idea/frontend/src/features/balance/assets/plus.svg create mode 100644 idea/frontend/src/features/balance/ui/balance/balance.module.scss create mode 100644 idea/frontend/src/features/balance/ui/balance/balance.tsx create mode 100644 idea/frontend/src/features/balance/ui/balance/index.ts create mode 100644 idea/frontend/src/features/balance/ui/program-balance/index.ts create mode 100644 idea/frontend/src/features/balance/ui/program-balance/program-balance.module.scss create mode 100644 idea/frontend/src/features/balance/ui/program-balance/program-balance.tsx delete mode 100644 idea/frontend/src/hooks/useBalanceTransfer.ts diff --git a/idea/frontend/src/features/balance/assets/plus.svg b/idea/frontend/src/features/balance/assets/plus.svg new file mode 100644 index 0000000000..95ca840dec --- /dev/null +++ b/idea/frontend/src/features/balance/assets/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/idea/frontend/src/features/balance/index.ts b/idea/frontend/src/features/balance/index.ts index 80ad4b54a3..4f355e02dc 100644 --- a/idea/frontend/src/features/balance/index.ts +++ b/idea/frontend/src/features/balance/index.ts @@ -1,3 +1,3 @@ -import { TransferBalance } from './ui'; +import { TransferBalance, ProgramBalance } from './ui'; -export { TransferBalance }; +export { TransferBalance, ProgramBalance }; diff --git a/idea/frontend/src/features/balance/ui/balance/balance.module.scss b/idea/frontend/src/features/balance/ui/balance/balance.module.scss new file mode 100644 index 0000000000..db549d9e80 --- /dev/null +++ b/idea/frontend/src/features/balance/ui/balance/balance.module.scss @@ -0,0 +1,10 @@ +.value { + font-family: Kanit; + font-weight: 600; +} + +.unit { + font-family: Kanit; + font-weight: 300; + color: rgba(255, 255, 255, 0.7); +} diff --git a/idea/frontend/src/features/balance/ui/balance/balance.tsx b/idea/frontend/src/features/balance/ui/balance/balance.tsx new file mode 100644 index 0000000000..52092c84a7 --- /dev/null +++ b/idea/frontend/src/features/balance/ui/balance/balance.tsx @@ -0,0 +1,26 @@ +import { useBalanceFormat } from '@gear-js/react-hooks'; +import { Balance as BalanceType } from '@polkadot/types/interfaces'; + +import styles from './balance.module.scss'; + +type Props = { + value: BalanceType | undefined; +}; + +function Balance({ value }: Props) { + const { getFormattedBalance } = useBalanceFormat(); + + if (!value) return null; + + const balance = getFormattedBalance(value); + + return ( +

+ {balance.value} +   + {balance.unit} +

+ ); +} + +export { Balance }; diff --git a/idea/frontend/src/features/balance/ui/balance/index.ts b/idea/frontend/src/features/balance/ui/balance/index.ts new file mode 100644 index 0000000000..d35c5d0857 --- /dev/null +++ b/idea/frontend/src/features/balance/ui/balance/index.ts @@ -0,0 +1,3 @@ +import { Balance } from './balance'; + +export { Balance }; diff --git a/idea/frontend/src/features/balance/ui/index.ts b/idea/frontend/src/features/balance/ui/index.ts index 3b589a4992..ef61899e9b 100644 --- a/idea/frontend/src/features/balance/ui/index.ts +++ b/idea/frontend/src/features/balance/ui/index.ts @@ -1,3 +1,4 @@ import { TransferBalance } from './transfer-balance'; +import { ProgramBalance } from './program-balance'; -export { TransferBalance }; +export { TransferBalance, ProgramBalance }; diff --git a/idea/frontend/src/features/balance/ui/program-balance/index.ts b/idea/frontend/src/features/balance/ui/program-balance/index.ts new file mode 100644 index 0000000000..1741372bcd --- /dev/null +++ b/idea/frontend/src/features/balance/ui/program-balance/index.ts @@ -0,0 +1,3 @@ +import { ProgramBalance } from './program-balance'; + +export { ProgramBalance }; diff --git a/idea/frontend/src/features/balance/ui/program-balance/program-balance.module.scss b/idea/frontend/src/features/balance/ui/program-balance/program-balance.module.scss new file mode 100644 index 0000000000..b65f098799 --- /dev/null +++ b/idea/frontend/src/features/balance/ui/program-balance/program-balance.module.scss @@ -0,0 +1,5 @@ +.balance { + display: flex; + align-items: center; + gap: 8px; +} diff --git a/idea/frontend/src/features/balance/ui/program-balance/program-balance.tsx b/idea/frontend/src/features/balance/ui/program-balance/program-balance.tsx new file mode 100644 index 0000000000..d42fd4ef87 --- /dev/null +++ b/idea/frontend/src/features/balance/ui/program-balance/program-balance.tsx @@ -0,0 +1,35 @@ +import { HexString } from '@gear-js/api'; +import { useAccount, useBalance } from '@gear-js/react-hooks'; +import { Button } from '@gear-js/ui'; + +import { useModalState } from '@/hooks'; + +import PlusSVG from '../../assets/plus.svg?react'; +import { Balance } from '../balance'; +import { TransferBalanceModal } from '../transfer-balance-modal'; +import styles from './program-balance.module.scss'; + +type Props = { + id: HexString; +}; + +function ProgramBalance({ id }: Props) { + const { account } = useAccount(); + const { balance } = useBalance(id); + + const [isModalOpen, openModal, closeModal] = useModalState(); + + return ( + <> +
+ + + {account &&
+ + {isModalOpen && } + + ); +} + +export { ProgramBalance }; diff --git a/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.module.scss b/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.module.scss index 251aaa7e7d..922ac60bb2 100644 --- a/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.module.scss +++ b/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.module.scss @@ -10,3 +10,16 @@ grid-template-columns: 1fr 1fr; gap: 32px; } + +.balance { + margin-top: 8px; + + display: flex; + gap: 8px; + + .text { + font-family: Kanit; + font-weight: 300; + color: rgba(255, 255, 255, 0.7); + } +} diff --git a/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.tsx b/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.tsx index c90df4a1b6..91a5d14068 100644 --- a/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.tsx +++ b/idea/frontend/src/features/balance/ui/transfer-balance-modal/transfer-balance-modal.tsx @@ -1,66 +1,97 @@ -import { decodeAddress } from '@gear-js/api'; -import { useAccount, useBalanceFormat } from '@gear-js/react-hooks'; -import { Button, Checkbox, Modal } from '@gear-js/ui'; -import { yupResolver } from '@hookform/resolvers/yup'; +import { useAccount, useApi, useDeriveBalancesAll } from '@gear-js/react-hooks'; +import { Button, Modal } from '@gear-js/ui'; +import { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; -import * as yup from 'yup'; +import { z } from 'zod'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; -import { Input, ValueField } from '@/shared/ui/form'; -import { isAccountAddressValid } from '@/shared/helpers'; -import { useBalanceTransfer } from '@/hooks'; +import { Checkbox, Input, ValueField } from '@/shared/ui'; +import { useBalanceSchema, useLoading, useSignAndSend } from '@/hooks'; +import { ACCOUNT_ADDRESS_SCHEMA } from '@/shared/config'; import SubmitSVG from '../../assets/submit.svg?react'; +import { Balance } from '../balance'; import styles from './transfer-balance-modal.module.scss'; -const defaultValues = { address: '', value: '', keepAlive: true }; +const FIELD_NAME = { + ADDRESS: 'address', + VALUE: 'value', + KEEP_ALIVE: 'keepAlive', +} as const; -const schema = yup.object().shape({ - address: yup - .string() - .test('is-address-valid', 'Invalid address', isAccountAddressValid) - .required('This field is required'), - value: yup.string().required('This field is required'), - keepAlive: yup.boolean().required(), -}); +const DEFAULT_VALUES = { + [FIELD_NAME.VALUE]: '', + [FIELD_NAME.KEEP_ALIVE]: true, +}; + +function useSchema() { + const balanceSchema = useBalanceSchema(); -const resolver = yupResolver(schema); + return z.object({ + // address can be a program id too, but we don't need to validate it's existence. account address schema should do the work + [FIELD_NAME.ADDRESS]: ACCOUNT_ADDRESS_SCHEMA, + [FIELD_NAME.VALUE]: balanceSchema, + [FIELD_NAME.KEEP_ALIVE]: z.boolean(), + }); +} type Props = { + defaultAddress?: string; close: () => void; }; -const TransferBalanceModal = ({ close }: Props) => { +const TransferBalanceModal = ({ defaultAddress = '', close }: Props) => { + const { api, isApiReady } = useApi(); const { account } = useAccount(); - const { getChainBalanceValue } = useBalanceFormat(); - const transferBalance = useBalanceTransfer(); + const balance = useDeriveBalancesAll(account?.address); + + const [isLoading, enableLoading, disableLoading] = useLoading(); + const signAndSend = useSignAndSend(); + + const schema = useSchema(); + + const form = useForm({ + defaultValues: { ...DEFAULT_VALUES, [FIELD_NAME.ADDRESS]: defaultAddress }, + resolver: zodResolver(schema), + }); - const methods = useForm({ defaultValues, resolver }); - const { register } = methods; + const handleSubmit = form.handleSubmit(({ address, value, keepAlive }) => { + if (!isApiReady) throw new Error('API is not initialized'); - const handleSubmit = ({ address, value, keepAlive }: typeof defaultValues) => { - if (!account) return; + enableLoading(); - const chainValue = getChainBalanceValue(value).toFixed(); - const signSource = account.meta.source; const onSuccess = close; + const onError = disableLoading; - transferBalance(account.address, decodeAddress(address), chainValue, { keepAlive, signSource, onSuccess }); - }; + const extrinsic = keepAlive + ? api.tx.balances.transferKeepAlive(address, value) + : api.tx.balances.transferAllowDeath(address, value); + + signAndSend(extrinsic, 'Transfer', { onSuccess, onError }); + }); return ( - -
+ +
- - - + + +
+ + +
+

Your transferrable balance:

+ +
+
+ +
-
diff --git a/idea/frontend/src/features/program/ui/program-table/program-table.tsx b/idea/frontend/src/features/program/ui/program-table/program-table.tsx index fb76e93b1c..7aa66cf9a7 100644 --- a/idea/frontend/src/features/program/ui/program-table/program-table.tsx +++ b/idea/frontend/src/features/program/ui/program-table/program-table.tsx @@ -17,9 +17,10 @@ import styles from './program-table.module.scss'; type Props = { program: Program | LocalProgram | undefined; isProgramReady: boolean; + renderBalance: () => JSX.Element; }; -const ProgramTable = ({ program, isProgramReady }: Props) => { +const ProgramTable = ({ program, isProgramReady, renderBalance }: Props) => { const { codeId } = program || {}; const blockId = program && 'blockHash' in program ? program.blockHash : undefined; @@ -39,6 +40,8 @@ const ProgramTable = ({ program, isProgramReady }: Props) => { return (
+ {renderBalance()} + @@ -46,12 +49,6 @@ const ProgramTable = ({ program, isProgramReady }: Props) => { - - {'timestamp' in program && ( - - - - )}
@@ -66,6 +63,12 @@ const ProgramTable = ({ program, isProgramReady }: Props) => { )} + + {'timestamp' in program && ( + + + + )}
); diff --git a/idea/frontend/src/features/voucher/consts.ts b/idea/frontend/src/features/voucher/consts.ts index ceaf5073be..3e03c3d61e 100644 --- a/idea/frontend/src/features/voucher/consts.ts +++ b/idea/frontend/src/features/voucher/consts.ts @@ -1,8 +1,3 @@ -import { decodeAddress } from '@gear-js/api'; -import { z } from 'zod'; - -import { isAccountAddressValid } from '@/shared/helpers'; - import { Values } from './types'; const FIELD_NAME = { @@ -29,11 +24,4 @@ const DEFAULT_FILTER_VALUES = { status: '', }; -const ADDRESS_SCHEMA = z - .string() - .trim() - .min(0) - .refine((value) => isAccountAddressValid(value), 'Invalid address') - .transform((value) => decodeAddress(value)); - -export { FIELD_NAME, VOUCHER_TYPE, DEFAULT_VALUES, DEFAULT_FILTER_VALUES, ADDRESS_SCHEMA }; +export { FIELD_NAME, VOUCHER_TYPE, DEFAULT_VALUES, DEFAULT_FILTER_VALUES }; diff --git a/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx b/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx index 3da3c1efc1..bdfc547f08 100644 --- a/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx +++ b/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx @@ -10,8 +10,9 @@ import { useLoading, useBalanceSchema, useSignAndSend } from '@/hooks'; import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; import { Input, ValueField } from '@/shared/ui'; +import { ACCOUNT_ADDRESS_SCHEMA } from '@/shared/config'; -import { ADDRESS_SCHEMA, DEFAULT_VALUES, FIELD_NAME, VOUCHER_TYPE } from '../../consts'; +import { DEFAULT_VALUES, FIELD_NAME, VOUCHER_TYPE } from '../../consts'; import { useDurationSchema, useVoucherType } from '../../hooks'; import { Values } from '../../types'; import { DurationForm } from '../duration-form'; @@ -34,7 +35,7 @@ const IssueVoucherModal = ({ programId, close, onSubmit = () => {} }: Props) => const durationSchema = useDurationSchema(); const schema = z.object({ - [FIELD_NAME.ACCOUNT_ADDRESS]: ADDRESS_SCHEMA, + [FIELD_NAME.ACCOUNT_ADDRESS]: ACCOUNT_ADDRESS_SCHEMA, [FIELD_NAME.VALUE]: balanceSchema, [FIELD_NAME.DURATION]: durationSchema, }); diff --git a/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx b/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx index 8fe3305c10..69b9b35bba 100644 --- a/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx +++ b/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx @@ -11,8 +11,9 @@ import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; import { asOptionalField } from '@/shared/helpers'; import { Input, ValueField } from '@/shared/ui'; +import { ACCOUNT_ADDRESS_SCHEMA } from '@/shared/config'; -import { ADDRESS_SCHEMA, DEFAULT_VALUES, FIELD_NAME } from '../../consts'; +import { DEFAULT_VALUES, FIELD_NAME } from '../../consts'; import { useDurationSchema } from '../../hooks'; import { Values, Voucher } from '../../types'; import { DurationForm } from '../duration-form'; @@ -35,7 +36,7 @@ const UpdateVoucherModal = ({ voucher, close, onSubmit }: Props) => { const durationSchema = useDurationSchema(); const schema = z.object({ - [FIELD_NAME.ACCOUNT_ADDRESS]: asOptionalField(ADDRESS_SCHEMA), + [FIELD_NAME.ACCOUNT_ADDRESS]: asOptionalField(ACCOUNT_ADDRESS_SCHEMA), [FIELD_NAME.VALUE]: asOptionalField(balanceSchema), [FIELD_NAME.DURATION]: asOptionalField(durationSchema), }); diff --git a/idea/frontend/src/features/wallet/ui/accounts-modal/accounts-modal.tsx b/idea/frontend/src/features/wallet/ui/accounts-modal/accounts-modal.tsx index 03edd6c634..7f33922f87 100644 --- a/idea/frontend/src/features/wallet/ui/accounts-modal/accounts-modal.tsx +++ b/idea/frontend/src/features/wallet/ui/accounts-modal/accounts-modal.tsx @@ -1,4 +1,3 @@ -import { decodeAddress } from '@gear-js/api'; import { useAccount, useAlert } from '@gear-js/react-hooks'; import { Button, Modal, buttonStyles } from '@gear-js/ui'; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; @@ -81,11 +80,6 @@ const AccountsModal = ({ close }: Props) => { handleAccountClick(_account); }; - const handleCopy = () => { - const decodedAddress = decodeAddress(address); - copyToClipboard(decodedAddress, alert); - }; - const accountBtnClasses = cx( buttonStyles.large, styles.accountButton, @@ -95,7 +89,7 @@ const AccountsModal = ({ close }: Props) => { return (
  • -
  • ); }); diff --git a/idea/frontend/src/hooks/index.ts b/idea/frontend/src/hooks/index.ts index b133344897..7445b8298b 100644 --- a/idea/frontend/src/hooks/index.ts +++ b/idea/frontend/src/hooks/index.ts @@ -10,7 +10,6 @@ import { useProgramActions } from './useProgramActions'; import { useAddMetadata } from './use-add-metadata'; import { useAddProgramName } from './use-add-program-name'; import { useAddCodeName } from './use-add-code-name'; -import { useBalanceTransfer } from './useBalanceTransfer'; import { useEventSubscriptions } from './useEventSubscriptions'; import { useGasCalculate } from './useGasCalculate'; import { useStateRead } from './useStateRead'; @@ -43,7 +42,6 @@ export { useAddMetadata, useAddProgramName, useAddCodeName, - useBalanceTransfer, useEventSubscriptions, useOnboarding, useNodeVersion, diff --git a/idea/frontend/src/hooks/use-sign-and-send.tsx b/idea/frontend/src/hooks/use-sign-and-send.tsx index 4106c190f1..526c6743fe 100644 --- a/idea/frontend/src/hooks/use-sign-and-send.tsx +++ b/idea/frontend/src/hooks/use-sign-and-send.tsx @@ -1,16 +1,19 @@ import { DEFAULT_ERROR_OPTIONS, DEFAULT_SUCCESS_OPTIONS, useAccount, useAlert } from '@gear-js/react-hooks'; -import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { AddressOrPair, SubmittableExtrinsic } from '@polkadot/api/types'; import { Event } from '@polkadot/types/interfaces'; import { ISubmittableResult } from '@polkadot/types/types'; import { web3FromSource } from '@polkadot/extension-dapp'; import { ReactNode } from 'react'; +import { getErrorMessage } from '@/shared/helpers'; + import { useExtrinsicFailedMessage } from './use-extrinsic-failed-message'; type Extrinsic = SubmittableExtrinsic<'promise', ISubmittableResult>; type Options = { successAlert: ReactNode; + addressOrPair?: AddressOrPair; onSuccess: () => void; onError: () => void; onFinally: () => void; @@ -19,6 +22,7 @@ type Options = { const DEFAULT_OPTIONS = { successAlert: 'Success', + addressOrPair: undefined, onSuccess: () => {}, onError: () => {}, onFinally: () => {}, @@ -76,32 +80,30 @@ function useSignAndSend() { } }; - const signAndSend = (extrinsic: Extrinsic, method: string, options?: Partial) => { + return (extrinsic: Extrinsic, method: string, options?: Partial) => { if (!account) throw new Error('Account is not found'); const { address, meta } = account; const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options }; - const { onError, onFinally } = optionsWithDefaults; + const { onError, onFinally, addressOrPair } = optionsWithDefaults; const alertTitle = `${extrinsic.method.section}.${extrinsic.method.method}`; const alertId = alert.loading(`SignIn`, { title: alertTitle }); - web3FromSource(meta.source) - .then(({ signer }) => - extrinsic.signAndSend(address, { signer }, (result) => - handleStatus(result, method, optionsWithDefaults, alertId), - ), - ) - .catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - alert.update(alertId, message, DEFAULT_ERROR_OPTIONS); - - onError(); - onFinally(); - }); - }; + const statusCallback = (result: ISubmittableResult) => handleStatus(result, method, optionsWithDefaults, alertId); + + const signAndSend = () => + addressOrPair + ? extrinsic.signAndSend(addressOrPair, statusCallback) + : web3FromSource(meta.source).then(({ signer }) => extrinsic.signAndSend(address, { signer }, statusCallback)); + + signAndSend().catch((error) => { + alert.update(alertId, getErrorMessage(error), DEFAULT_ERROR_OPTIONS); - return signAndSend; + onError(); + onFinally(); + }); + }; } export { useSignAndSend }; diff --git a/idea/frontend/src/hooks/useBalanceTransfer.ts b/idea/frontend/src/hooks/useBalanceTransfer.ts deleted file mode 100644 index 39c7a445e6..0000000000 --- a/idea/frontend/src/hooks/useBalanceTransfer.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AddressOrPair } from '@polkadot/api/types'; -import { EventRecord } from '@polkadot/types/interfaces'; -import { web3FromSource } from '@polkadot/extension-dapp'; -import { useApi, useAlert } from '@gear-js/react-hooks'; - -import { Method } from '@/features/explorer'; -import { getExtrinsicFailedMessage } from '@/shared/helpers'; - -type Options = { - signSource?: string; - keepAlive?: boolean; - onSuccess?: () => void; -}; - -const useBalanceTransfer = () => { - const { api, isApiReady } = useApi(); - const alert = useAlert(); - - const handleEventsStatus = (events: EventRecord[], onSuccess?: () => void) => { - if (!isApiReady) return Promise.reject(new Error('API is not initialized')); - - events.forEach(({ event }) => { - const { method, section } = event; - - const alertOptions = { title: `${section}.${method}` }; - - if (method === Method.Transfer) { - alert.success('Balance transfered successfully', alertOptions); - - if (onSuccess) onSuccess(); - } else if (method === Method.ExtrinsicFailed) { - alert.error(getExtrinsicFailedMessage(api, event), alertOptions); - } - }); - }; - - const transferBalance = (from: AddressOrPair, to: string, value: string, options?: Options) => { - try { - if (!isApiReady) throw new Error('API is not initialized'); - - const { signSource, keepAlive, onSuccess } = options || {}; - - const extrinsic = keepAlive - ? api.tx.balances.transferKeepAlive(to, value) - : api.tx.balances.transferAllowDeath(to, value); - - if (signSource) { - web3FromSource(signSource).then(({ signer }) => - extrinsic.signAndSend(from, { signer }, ({ events }) => handleEventsStatus(events, onSuccess)), - ); - } else { - extrinsic.signAndSend(from, ({ events }) => handleEventsStatus(events, onSuccess)); - } - } catch (error) { - const { message } = error as Error; - - alert.error(message); - } - }; - - return transferBalance; -}; - -export { useBalanceTransfer }; diff --git a/idea/frontend/src/pages/program/program.tsx b/idea/frontend/src/pages/program/program.tsx index 191590e858..fda26ec56a 100644 --- a/idea/frontend/src/pages/program/program.tsx +++ b/idea/frontend/src/pages/program/program.tsx @@ -17,6 +17,8 @@ import { ProgramVouchers } from '@/features/voucher'; import { IDL, ProgramEvents, useSails } from '@/features/sails'; import { ProgramMessages } from '@/features/message'; +import { ProgramBalance } from '@/features/balance'; + import styles from './program.module.scss'; const TABS = ['Metadata/Sails', 'Messages', 'Vouchers', 'Events']; @@ -104,7 +106,11 @@ const Program = () => { )} - + } + />
    {renderTabs()}
    diff --git a/idea/frontend/src/shared/config/consts.ts b/idea/frontend/src/shared/config/consts.ts index 6415b75fd5..35f09e2896 100644 --- a/idea/frontend/src/shared/config/consts.ts +++ b/idea/frontend/src/shared/config/consts.ts @@ -1,3 +1,8 @@ +import { decodeAddress } from '@gear-js/api'; +import { z } from 'zod'; + +import { isAccountAddressValid } from '../helpers'; + const API_URL = import.meta.env.VITE_API_URL as string; const INDEXER_API_URL = import.meta.env.VITE_INDEXER_API_URL as string; const NODES_API_URL = import.meta.env.VITE_NODES_API_URL as string; @@ -91,6 +96,13 @@ enum AnimationTimeout { Big = 1000, } +const ACCOUNT_ADDRESS_SCHEMA = z + .string() + .trim() + .min(0) + .refine((value) => isAccountAddressValid(value), 'Invalid address') + .transform((value) => decodeAddress(value)); + export { API_URL, INDEXER_API_URL, @@ -106,6 +118,7 @@ export { ACCOUNT_ERRORS, PROGRAM_ERRORS, GENESIS, + ACCOUNT_ADDRESS_SCHEMA, LocalStorage, GasMethod, TransactionName, diff --git a/idea/frontend/src/widgets/header/ui/topSide/TopSide.tsx b/idea/frontend/src/widgets/header/ui/topSide/TopSide.tsx index 68d1371198..3671c37dfb 100644 --- a/idea/frontend/src/widgets/header/ui/topSide/TopSide.tsx +++ b/idea/frontend/src/widgets/header/ui/topSide/TopSide.tsx @@ -7,7 +7,7 @@ import { useApi, useAlert, useAccount } from '@gear-js/react-hooks'; import { TooltipWrapper, buttonStyles } from '@gear-js/ui'; import { getTestBalance } from '@/api'; -import { useBalanceTransfer, useChain } from '@/hooks'; +import { useChain, useSignAndSend } from '@/hooks'; import { RecentBlocks } from '@/features/recentBlocks'; import { HCAPTCHA_SITE_KEY, AnimationTimeout, GEAR_BALANCE_TRANSFER_VALUE } from '@/shared/config'; import TestBalanceSVG from '@/shared/assets/images/actions/testBalance.svg?react'; @@ -18,41 +18,36 @@ import { TotalIssuance } from '../totalIssuance'; import styles from './TopSide.module.scss'; const TopSide = () => { + const { api, isApiReady } = useApi(); const alert = useAlert(); - const { api, isApiReady } = useApi(); const { account } = useAccount(); + const address = account?.address; + const { isDevChain, isTestBalanceAvailable } = useChain(); + const signAndSend = useSignAndSend(); const [captchaToken, setCaptchaToken] = useState(''); const [totalIssuance, setTotalIssuance] = useState(''); const captchaRef = useRef(null); - const address = account?.address; - - const transferBalance = useBalanceTransfer(); - const getBalanceFromService = () => { - if (address) getTestBalance({ address, token: captchaToken }).catch(({ message }: Error) => alert.error(message)); - }; - - const handleTestBalanceClick = () => { - if (captchaToken) { - getBalanceFromService(); + if (!address) throw new Error('Account is not found'); - return; - } - - captchaRef.current?.execute(); + getTestBalance({ address, token: captchaToken }).catch(({ message }: Error) => alert.error(message)); }; + const handleTestBalanceClick = () => (captchaToken ? getBalanceFromService() : captchaRef.current?.execute()); + const getBalanceFromAlice = async () => { - if (!address) return; + if (!isApiReady) throw new Error('API is not initialized'); + if (!address) throw new Error('Account is not found'); - const alice = await GearKeyring.fromSuri('//Alice'); + const alicePair = await GearKeyring.fromSuri('//Alice'); + const extrinsic = api.tx.balances.transferKeepAlive(address, GEAR_BALANCE_TRANSFER_VALUE); - transferBalance(alice, address, GEAR_BALANCE_TRANSFER_VALUE); + signAndSend(extrinsic, 'Transfer', { addressOrPair: alicePair }); }; const handleExpire = () => setCaptchaToken(''); diff --git a/yarn.lock b/yarn.lock index ef5c8475ce..168d996608 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -2583,7 +2586,7 @@ __metadata: pg: 8.10.0 ts-node-dev: ^2.0.0 typeorm: ^0.3.17 - typescript: ^5.1.6 + typescript: 5.5.3 languageName: unknown linkType: soft @@ -18898,7 +18901,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.5.4, typescript@npm:^5.1.6, typescript@npm:^5.3.3": +"typescript@npm:5.5.4, typescript@npm:^5.3.3": version: 5.5.4 resolution: "typescript@npm:5.5.4" bin: @@ -18908,16 +18911,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.1.6, typescript@npm:^5.3.3": - version: 5.4.3 - resolution: "typescript@npm:5.4.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 53c879c6fa1e3bcb194b274d4501ba1985894b2c2692fa079db03c5a5a7140587a1e04e1ba03184605d35f439b40192d9e138eb3279ca8eee313c081c8bcd9b0 - languageName: node - linkType: hard - "typescript@patch:typescript@4.7.4#~builtin": version: 4.7.4 resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=65a307" @@ -18978,7 +18971,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@5.5.4#~builtin, typescript@patch:typescript@^5.1.6#~builtin, typescript@patch:typescript@^5.3.3#~builtin": +"typescript@patch:typescript@5.5.4#~builtin, typescript@patch:typescript@^5.3.3#~builtin": version: 5.5.4 resolution: "typescript@patch:typescript@npm%3A5.5.4#~builtin::version=5.5.4&hash=d73830" bin: @@ -18988,16 +18981,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^5.1.6#~builtin, typescript@patch:typescript@^5.3.3#~builtin": - version: 5.4.3 - resolution: "typescript@patch:typescript@npm%3A5.4.3#~builtin::version=5.4.3&hash=d73830" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 2373c693f3b328f3b2387c3efafe6d257b057a142f9a79291854b14ff4d5367d3d730810aee981726b677ae0fd8329b23309da3b6aaab8263dbdccf1da07a3ba - languageName: node - linkType: hard - "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2"