Skip to content

Commit

Permalink
Improve vouchers (#1480)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitayutanov authored Feb 1, 2024
1 parent d1ef671 commit d4cad2f
Show file tree
Hide file tree
Showing 25 changed files with 327 additions and 112 deletions.
6 changes: 4 additions & 2 deletions idea/frontend/src/features/voucher/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
IssueVoucher,
VoucherSelect,
ProgramVoucherSelect,
CodeVoucherSelect,
VoucherTable,
VoucherBadge,
UseVoucherCheckboxDeprecated,
Expand All @@ -9,7 +10,8 @@ import {

export {
IssueVoucher,
VoucherSelect,
ProgramVoucherSelect,
CodeVoucherSelect,
VoucherTable,
VoucherBadge,
UseVoucherCheckboxDeprecated,
Expand Down
5 changes: 3 additions & 2 deletions idea/frontend/src/features/voucher/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { IssueVoucher } from './issue-voucher';
import { VoucherSelect } from './voucher-select';
import { ProgramVoucherSelect, CodeVoucherSelect } from './voucher-select';
import { VoucherTable, VoucherTableDeprecated } from './voucher-table';
import { VoucherBadge } from './voucher-badge';
import { UseVoucherCheckboxDeprecated } from './use-voucher-checkbox-deprecated';

export {
IssueVoucher,
VoucherSelect,
ProgramVoucherSelect,
CodeVoucherSelect,
VoucherTable,
VoucherBadge,
UseVoucherCheckboxDeprecated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
top: 0;
left: 0;
}

.expired rect {
fill: #f24a4a;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { HexString } from '@gear-js/api';
import { useAccountVouchers } from '@gear-js/react-hooks';
import { useIsAnyAccountVoucherActive } from '@gear-js/react-hooks';
import clsx from 'clsx';

import { withAccount, withDeprecatedFallback } from '@/shared/ui';
import { withDeprecatedFallback } from '@/shared/ui';

import BadgeSVG from '../../assets/badge.svg?react';
import { VoucherBadgeDeprecated } from './voucher-badge-deprecated';
Expand All @@ -11,14 +12,13 @@ type Props = {
programId: HexString;
};

const VoucherBadge = withAccount(
withDeprecatedFallback(({ programId }: Props) => {
const { vouchers } = useAccountVouchers(programId);
const voucherEntries = Object.entries(vouchers || {});
const vouchersCount = voucherEntries.length;
const VoucherBadge = withDeprecatedFallback(({ programId }: Props) => {
// TODO: take a look at performance, useVouchers is called for each program in a list
const { isAnyVoucherActive, isAnyVoucherActiveReady } = useIsAnyAccountVoucherActive(programId);

return vouchersCount ? <BadgeSVG className={styles.badge} /> : null;
}, VoucherBadgeDeprecated),
);
return isAnyVoucherActiveReady ? (
<BadgeSVG className={clsx(styles.badge, !isAnyVoucherActive && styles.expired)} />
) : null;
}, VoucherBadgeDeprecated);

export { VoucherBadge };
4 changes: 2 additions & 2 deletions idea/frontend/src/features/voucher/ui/voucher-select/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { VoucherSelect } from './voucher-select';
import { ProgramVoucherSelect, CodeVoucherSelect } from './voucher-select';

export { VoucherSelect };
export { ProgramVoucherSelect, CodeVoucherSelect };
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { HexString } from '@gear-js/api';
import { useAccountVouchers } from '@gear-js/react-hooks';
import { InputWrapper } from '@gear-js/ui';
import { HexString, IVoucherDetails } from '@gear-js/api';
import { getTypedEntries, useAccountVouchers } from '@gear-js/react-hooks';
import { InputWrapper, InputWrapperProps } from '@gear-js/ui';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';

import { Select } from '@/shared/ui';

import { VoucherOption } from './voucher-option';

type Props = {
programId: HexString | undefined;
type Props = Omit<InputWrapperProps, 'id' | 'label' | 'size' | 'children'> & {
entries: [HexString, IVoucherDetails][];
};

const VoucherSelect = ({ programId }: Props) => {
const { vouchers } = useAccountVouchers(programId);
const voucherEntries = Object.entries(vouchers || {});
const vouchersCount = voucherEntries.length;

const VoucherSelect = ({ entries, ...props }: Props) => {
const name = 'voucherId';
const vouchersCount = entries.length;

const renderVouchers = () =>
voucherEntries.map(([id, { expiry }]) => <VoucherOption key={id} id={id as HexString} expireBlock={expiry} />);
entries.map(([id, { expiry }]) => <VoucherOption key={id} id={id} expireBlock={expiry} />);

// TODO: should be done by react-hook-form's global shouldUnregister,
// however due to complications of current forms it's not possible yet.
Expand All @@ -32,7 +29,7 @@ const VoucherSelect = ({ programId }: Props) => {
}, [vouchersCount, resetField]);

return vouchersCount ? (
<InputWrapper id={name} label="Voucher funds:" size="normal" direction="x" gap="1/5">
<InputWrapper id={name} label="Voucher funds" size="normal" {...props}>
<Select name={name}>
<option value="" label="No voucher" />

Expand All @@ -42,4 +39,18 @@ const VoucherSelect = ({ programId }: Props) => {
) : null;
};

export { VoucherSelect };
const ProgramVoucherSelect = ({ programId }: { programId: HexString | undefined }) => {
const { vouchers } = useAccountVouchers(programId);
const entries = getTypedEntries(vouchers || {});

return <VoucherSelect entries={entries} direction="x" gap="1/5" />;
};

const CodeVoucherSelect = () => {
const { vouchers } = useAccountVouchers();
const entries = getTypedEntries(vouchers || {}).filter(([, { codeUploading }]) => codeUploading);

return <VoucherSelect entries={entries} direction="y" />;
};

export { ProgramVoucherSelect, CodeVoucherSelect };
4 changes: 4 additions & 0 deletions idea/frontend/src/hooks/useCodeUpload/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { HexString } from '@polkadot/util/types';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { ISubmittableResult } from '@polkadot/types/types';

import { ParamsToSignAndSend as CommonParamsToSignAndSend } from '@/entities/hooks';

type ParamsToUploadCode = {
optBuffer: Buffer;
name: string;
voucherId: string;
metaHex: HexString | undefined;
resolve: () => void;
};

type ParamsToSignAndSend = Omit<CommonParamsToSignAndSend, 'reject'> & {
extrinsic: SubmittableExtrinsic<'promise', ISubmittableResult>;
name: string;
codeId: HexString;
metaHex: HexString | undefined;
Expand Down
21 changes: 14 additions & 7 deletions idea/frontend/src/hooks/useCodeUpload/useCodeUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { EventRecord } from '@polkadot/types/interfaces';
import { HexString } from '@polkadot/util/types';
import { useApi, useAlert, useAccount, DEFAULT_ERROR_OPTIONS, DEFAULT_SUCCESS_OPTIONS } from '@gear-js/react-hooks';

import { useModal } from '@/hooks';
import { useChain, useModal } from '@/hooks';
import { Method } from '@/features/explorer';
import { checkWallet, getExtrinsicFailedMessage } from '@/shared/helpers';
import { PROGRAM_ERRORS, TransactionName, TransactionStatus, UPLOAD_METADATA_TIMEOUT } from '@/shared/config';
import { CopiedInfo } from '@/shared/ui/copiedInfo';

import { addMetadata, addCodeName } from '@/api';

import { ParamsToUploadCode, ParamsToSignAndSend } from './types';

const useCodeUpload = () => {
const { api, isApiReady } = useApi();
const alert = useAlert();
const { account } = useAccount();
const { showModal } = useModal();
const { isDevChain } = useChain();

const handleEventsStatus = (events: EventRecord[], codeHash: HexString, resolve?: () => void) => {
if (!isApiReady) throw new Error('API is not initialized');
Expand All @@ -36,13 +37,13 @@ const useCodeUpload = () => {
});
};

const signAndSend = async ({ signer, codeId, metaHex, name, resolve }: ParamsToSignAndSend) => {
const signAndSend = async ({ extrinsic, signer, codeId, metaHex, name, resolve }: ParamsToSignAndSend) => {
const alertId = alert.loading('SignIn', { title: TransactionName.SubmitCode });

try {
if (!isApiReady) throw new Error('API is not initialized');

await api.code.signAndSend(account!.address, { signer }, ({ events, status }) => {
await extrinsic.signAndSend(account!.address, { signer }, ({ events, status }) => {
if (status.isReady) {
alert.update(alertId, TransactionStatus.Ready);
} else if (status.isInBlock) {
Expand All @@ -51,6 +52,8 @@ const useCodeUpload = () => {
} else if (status.isFinalized) {
alert.update(alertId, TransactionStatus.Finalized, DEFAULT_SUCCESS_OPTIONS);

if (isDevChain) return;

// timeout cuz wanna be sure that block data is ready
setTimeout(() => {
const id = codeId;
Expand All @@ -71,18 +74,22 @@ const useCodeUpload = () => {
};

const uploadCode = useCallback(
async ({ optBuffer, name, metaHex, resolve }: ParamsToUploadCode) => {
async ({ optBuffer, name, voucherId, metaHex, resolve }: ParamsToUploadCode) => {
try {
if (!isApiReady) throw new Error('API is not initialized');
checkWallet(account);

const { address, meta } = account!;

const [{ codeHash }, { signer }] = await Promise.all([api.code.upload(optBuffer), web3FromSource(meta.source)]);
const [code, { signer }] = await Promise.all([api.code.upload(optBuffer), web3FromSource(meta.source)]);

const codeExtrinsic = code.extrinsic;
const codeId = code.codeHash;
const extrinsic = voucherId ? api.voucher.call(voucherId, { UploadCode: codeExtrinsic }) : codeExtrinsic;

const { partialFee } = await api.code.paymentInfo(address, { signer });

const handleConfirm = () => signAndSend({ signer, name, codeId: codeHash, metaHex, resolve });
const handleConfirm = () => signAndSend({ extrinsic, signer, name, codeId, metaHex, resolve });

showModal('transaction', {
fee: partialFee.toHuman(),
Expand Down
80 changes: 47 additions & 33 deletions idea/frontend/src/pages/uploadCode/ui/UploadCode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, FileInput, Input } from '@gear-js/ui';
import { useForm } from 'react-hook-form';
import { Button, FileInput } from '@gear-js/ui';
import { useApi } from '@gear-js/react-hooks';
import { FormProvider, useForm } from 'react-hook-form';
import { useEffect } from 'react';

import { FileTypes } from '@/shared/config';
Expand All @@ -9,12 +10,20 @@ import { Subheader } from '@/shared/ui/subheader';
import { BackButton } from '@/shared/ui/backButton';
import { UploadMetadata } from '@/features/uploadMetadata';
import PlusSVG from '@/shared/assets/images/actions/plus.svg?react';
import { CodeVoucherSelect } from '@/features/voucher';
import { Input } from '@/shared/ui';

import styles from './UploadCode.module.scss';

const defaultValues = { name: '' };
const defaultValues = {
name: '',
voucherId: '',
};

const UploadCode = () => {
const { isV110Runtime } = useApi();
const { isDevChain } = useChain();

const {
optFile,
setOptFile,
Expand All @@ -26,23 +35,20 @@ const UploadCode = () => {
isUploadedMetaReady,
} = useMetaOnUpload(true);

const form = useForm({ defaultValues });
const { register, getFieldState, reset, formState } = form;
const { error } = getFieldState('name', formState);
const required = metadata.hex ? 'Field is required' : false;
const methods = useForm({ defaultValues });
const { reset, handleSubmit } = methods;

const { isDevChain } = useChain();
const uploadCode = useCodeUpload();

const resetForm = () => {
reset();
resetOptFile();
};

const handleSubmit = ({ name }: typeof defaultValues) => {
const onSubmit = (data: typeof defaultValues) => {
if (!optBuffer) return;

uploadCode({ optBuffer, name, metaHex: metadata.hex, resolve: resetForm });
uploadCode({ optBuffer, metaHex: metadata.hex, resolve: resetForm, ...data });
};

useEffect(() => {
Expand All @@ -63,32 +69,40 @@ const UploadCode = () => {
<div>
<Subheader title="Enter code parameters" size="big" />
<Box>
<form className={styles.form} id="uploadCodeForm" onSubmit={form.handleSubmit(handleSubmit)}>
<FileInput
label="Code file"
direction="y"
value={optFile}
accept={FileTypes.Wasm}
onChange={setOptFile}
/>

<Input label="Code name" direction="y" error={error?.message} {...register('name', { required })} />
</form>
<FormProvider {...methods}>
<form className={styles.form} id="uploadCodeForm" onSubmit={handleSubmit(onSubmit)}>
<FileInput
label="Code file"
direction="y"
value={optFile}
accept={FileTypes.Wasm}
onChange={setOptFile}
/>

{optFile && (
<>
{/* since we're not storing codes in an indexeddb yet */}
{!isDevChain && <Input name="name" label="Code name" direction="y" />}

{/* not using withDeprecatedFallback since code voucher call wasn't exist */}
{isV110Runtime && <CodeVoucherSelect />}
</>
)}
</form>
</FormProvider>
</Box>
</div>

{!isDevChain && (
<div>
<Subheader size="big" title="Add metadata" />
<UploadMetadata
metadata={metadata.value}
isInputDisabled={!!metadata.isUploaded}
isLoading={!isUploadedMetaReady}
onUpload={setFileMetadata}
onReset={handleMetadataReset}
/>
</div>
)}
<div>
<Subheader size="big" title="Add metadata" />
<UploadMetadata
metadata={metadata.value}
isInputDisabled={!!metadata.isUploaded}
isLoading={!isUploadedMetaReady}
onUpload={setFileMetadata}
onReset={handleMetadataReset}
/>
</div>
</div>

<div className={styles.buttons}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@
.footer {
padding-right: 25%;

display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: 1fr 1.25fr 1.25fr 0.25fr;
gap: 16px;

.owner {
display: flex;
gap: 8px;

font-size: 10px;

button {
position: relative;
z-index: 4;

svg {
width: 16px;
height: 16px;
}
}
}
}
Loading

0 comments on commit d4cad2f

Please sign in to comment.