Skip to content

Commit

Permalink
refactor(idea/frontend): metadata fetch on program/code upload and pr…
Browse files Browse the repository at this point in the history
…ogram init (#1579)
  • Loading branch information
nikitayutanov authored Jun 12, 2024
1 parent 1426dd0 commit f571ad5
Show file tree
Hide file tree
Showing 29 changed files with 328 additions and 414 deletions.
6 changes: 6 additions & 0 deletions idea/frontend/src/features/code/consts/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const WASM_FILE_TYPE = {
PROGRAM: 'program',
CODE: 'code',
} as const;

export { WASM_FILE_TYPE };
3 changes: 3 additions & 0 deletions idea/frontend/src/features/code/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { WASM_FILE_TYPE } from './file';

export { WASM_FILE_TYPE };
4 changes: 4 additions & 0 deletions idea/frontend/src/features/code/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useWasmFileHandler } from './use-wasm-file-handler';
import { useWasmFile } from './use-wasm-file';

export { useWasmFileHandler, useWasmFile };
47 changes: 47 additions & 0 deletions idea/frontend/src/features/code/hooks/use-wasm-file-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { HexString, generateCodeHash } from '@gear-js/api';
import { useApi, useAlert } from '@gear-js/react-hooks';
import { generatePath } from 'react-router-dom';

import { FileTypes, routes } from '@/shared/config';
import { CustomLink } from '@/shared/ui/customLink';

import { WasmFileType } from '../types';
import { WASM_FILE_TYPE } from '../consts';

type OnChange = (value: File | undefined, buffer: Buffer | undefined) => void;

// upload-code feature?
function useWasmFileHandler(type: WasmFileType, onChange: OnChange = () => {}) {
const { api, isApiReady } = useApi();
const alert = useAlert();

const renderCodeExistsAlert = (codeId: HexString) => (
<>
<p>Code already exists</p>
<p>
ID: <CustomLink to={generatePath(routes.code, { codeId })} text={codeId} />
</p>
</>
);

return async (value: File | undefined) => {
if (!value) return onChange(undefined, undefined);
if (value.type !== FileTypes.Wasm) return alert.error('Invalid file type');

const arrayBuffer = await value.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

if (type === WASM_FILE_TYPE.CODE) {
if (!isApiReady) throw new Error('API is not initialized');

const codeId = generateCodeHash(buffer);
const isCodeExists = await api.code.exists(codeId);

if (isCodeExists) return alert.error(renderCodeExistsAlert(codeId));
}

onChange(value, buffer);
};
}

export { useWasmFileHandler };
35 changes: 35 additions & 0 deletions idea/frontend/src/features/code/hooks/use-wasm-file.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';

import { WASM_FILE_TYPE } from '../consts';
import { WasmFileType } from '../types';
import { useWasmFileHandler } from './use-wasm-file-handler';

type Location = {
state: {
file: File | undefined;
buffer: Buffer | undefined;
} | null;
};

// upload-code/program feature?
function useWasmFile(type: WasmFileType = WASM_FILE_TYPE.PROGRAM) {
const { state } = useLocation() as Location;

const [file, setFile] = useState(state?.file);
const [buffer, setBuffer] = useState(state?.buffer);

const handleFileChange = useWasmFileHandler(type, (value, bufferValue) => {
setFile(value);
setBuffer(bufferValue);
});

const resetFile = () => {
setFile(undefined);
setBuffer(undefined);
};

return { value: file, buffer, reset: resetFile, handleChange: handleFileChange };
}

export { useWasmFile };
3 changes: 2 additions & 1 deletion idea/frontend/src/features/code/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CodeTable } from './ui';
import { useWasmFileHandler, useWasmFile } from './hooks';

export { CodeTable };
export { CodeTable, useWasmFileHandler, useWasmFile };
5 changes: 5 additions & 0 deletions idea/frontend/src/features/code/types/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WASM_FILE_TYPE } from '../consts';

type WasmFileType = typeof WASM_FILE_TYPE[keyof typeof WASM_FILE_TYPE];

export type { WasmFileType };
3 changes: 3 additions & 0 deletions idea/frontend/src/features/code/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { WasmFileType } from './file';

export type { WasmFileType };
4 changes: 3 additions & 1 deletion idea/frontend/src/features/metadata/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMetadata } from './use-metadata';
import { useMetadataHash } from './use-metadata-hash';
import { useMetadataWithFile } from './use-metadata-with-file';

export { useMetadata };
export { useMetadata, useMetadataHash, useMetadataWithFile };
39 changes: 39 additions & 0 deletions idea/frontend/src/features/metadata/hooks/use-metadata-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HexString } from '@gear-js/api';
import { useApi } from '@gear-js/react-hooks';
import { isHex } from '@polkadot/util';
import { useQuery } from '@tanstack/react-query';

import { isString } from '@/shared/helpers';

const NO_METAHASH_ERROR = 'metahash function not found in exports';

function useMetadataHash(codeIdOrBuffer: HexString | Buffer | undefined) {
const { api, isApiReady } = useApi();
const queryKey = ['metadataHash', codeIdOrBuffer];

const getMetadataHash = async () => {
if (!isApiReady) throw new Error('API is not initialized');
if (!codeIdOrBuffer) throw new Error('Code ID or buffer is not provided');

try {
return await (isHex(codeIdOrBuffer)
? api.code.metaHash(codeIdOrBuffer)
: api.code.metaHashFromWasm(codeIdOrBuffer));
} catch (error) {
// mock the behavior of the meta-storage api. useMetadata hook is relying on this, maybe worth to refactor
if (isString(error) && error === NO_METAHASH_ERROR) return null;

throw error;
}
};

const { data } = useQuery({
queryKey,
queryFn: getMetadataHash,
enabled: isApiReady && Boolean(codeIdOrBuffer),
});

return data;
}

export { useMetadataHash };
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HexString, ProgramMetadata } from '@gear-js/api';
import { useState, useMemo } from 'react';

import { useMetadata } from './use-metadata';

// upload-metadata feature?
function useMetadataWithFile(hash: HexString | null | undefined) {
const { metadataHex: storageMetadataHex, isMetadataReady: isStorageMetadataReady } = useMetadata(hash);

const [fileMetadataHex, setFileMetadataHex] = useState<HexString>();
const resetFileMetadataHex = () => setFileMetadataHex(undefined);

const metadataHex = fileMetadataHex || storageMetadataHex;
const metadata = useMemo(() => (metadataHex ? ProgramMetadata.from(metadataHex) : undefined), [metadataHex]);
const isMetadataReady = isStorageMetadataReady || Boolean(fileMetadataHex);
const isStorageMetadata = Boolean(storageMetadataHex);

return {
value: metadata,
hex: metadataHex,
isReady: isMetadataReady,
isFromStorage: isStorageMetadata,
set: setFileMetadataHex,
reset: resetFileMetadataHex,
};
}

export { useMetadataWithFile };
18 changes: 11 additions & 7 deletions idea/frontend/src/features/metadata/hooks/use-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import { HexString, ProgramMetadata } from '@gear-js/api';
import { useAlert } from '@gear-js/react-hooks';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';

import { fetchMetadata, getLocalMetadata } from '@/api';
import { RPCError, RPCErrorCode } from '@/shared/services/rpcService';
import { useChain } from '@/hooks';

function useMetadata(hash?: HexString | null | undefined) {
const alert = useAlert();

const { isDevChain } = useChain();

const [metadata, setMetadata] = useState<ProgramMetadata>();
const [isMetadataReady, setIsMetadataReady] = useState(false);
const [metadataHex, setMetadataHex] = useState<HexString>();
const metadata = useMemo(() => (metadataHex ? ProgramMetadata.from(metadataHex) : undefined), [metadataHex]);

const defaultIsReady = hash === null;
const [isMetadataReady, setIsMetadataReady] = useState(defaultIsReady);

const getMetadata = (_hash: HexString) =>
isDevChain ? getLocalMetadata(_hash).catch(() => fetchMetadata(_hash)) : fetchMetadata(_hash);

useEffect(() => {
if (hash === null) return setIsMetadataReady(true);
setMetadataHex(undefined);
setIsMetadataReady(defaultIsReady);

if (!hash) return;

getMetadata(hash)
.then(({ result }) => result.hex && setMetadata(ProgramMetadata.from(result.hex)))
.then(({ result }) => result.hex && setMetadataHex(result.hex))
.catch(({ message, code }: RPCError) => code !== RPCErrorCode.MetadataNotFound && alert.error(message))
.finally(() => setIsMetadataReady(true));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hash]);

return { metadata, isMetadataReady, setMetadata, getMetadata };
return { metadata, metadataHex, isMetadataReady, setMetadataHex, getMetadata };
}

export { useMetadata };
4 changes: 2 additions & 2 deletions idea/frontend/src/features/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMetadata } from './hooks';
import { useMetadata, useMetadataHash, useMetadataWithFile } from './hooks';
import { MetadataTable } from './ui';
import { isHumanTypesRepr, isState } from './utils';

export { useMetadata, MetadataTable, isHumanTypesRepr, isState };
export { useMetadata, useMetadataHash, useMetadataWithFile, MetadataTable, isHumanTypesRepr, isState };

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit f571ad5

Please sign in to comment.