Skip to content

Commit

Permalink
feat(dcellar-web-ui): add basic batch upload
Browse files Browse the repository at this point in the history
  • Loading branch information
devinxl committed Jul 25, 2023
1 parent f7c0efe commit 18d3301
Show file tree
Hide file tree
Showing 25 changed files with 1,054 additions and 417 deletions.
4 changes: 2 additions & 2 deletions apps/dcellar-web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
"ahooks": "3.7.7",
"hash-wasm": "4.9.0",
"@babel/core": "^7.20.12",
"@bnb-chain/greenfield-chain-sdk": "0.0.0-snapshot-20230713041344",
"@bnb-chain/greenfield-chain-sdk": "0.0.0-snapshot-20230725025153",
"@bnb-chain/greenfield-cosmos-types": "0.4.0-alpha.13",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@next/bundle-analyzer": "^13.1.6",
"@tanstack/react-table": "^8.7.9",
"@tanstack/react-virtual": "3.0.0-alpha.0",
"@totejs/icons": "^2.10.0",
"@totejs/uikit": "~2.44.5",
"@totejs/uikit": "~2.49.1",
"axios": "^1.3.2",
"axios-retry": "^3.4.0",
"bignumber.js": "^9.1.1",
Expand Down
10 changes: 9 additions & 1 deletion apps/dcellar-web-ui/src/components/common/DCDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ export const DCDrawer = (props: DCDrawerProps) => {

return (
<GAShow isShow={props.isOpen} name={gaShowName} data={gaShowData}>
<QDrawer closeOnOverlayClick={false} w={568} onClose={onBeforeClose} {...restProps}>
<QDrawer
closeOnOverlayClick={false}
w={568}
padding="16px 24px"
onClose={onBeforeClose}

rootProps={{ top: 68 }}
{...restProps}
>
{children}
</QDrawer>
</GAShow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useAppDispatch } from "@/store";
import { setupGasList } from "@/store/slices/global";
import { setupGasObjects } from "@/store/slices/global";
import { useAsyncEffect } from "ahooks";

export const GasList = () => {
export const GasObjects = () => {
const dispatch = useAppDispatch();
useAsyncEffect(async () => {
dispatch(setupGasList());
}, [dispatch, setupGasList]);
dispatch(setupGasObjects());
}, [dispatch, setupGasObjects]);

return <></>
}
111 changes: 88 additions & 23 deletions apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
progressFetchList,
selectHashTask,
selectUploadQueue,
updateHashChecksum,
updateHashStatus,
updateHashTaskMsg,
updateUploadChecksum,
updateUploadMsg,
updateUploadProgress,
updateUploadStatus,
updateUploadTaskMsg,
UploadFile,
uploadQueueAndRefresh,
} from '@/store/slices/global';
Expand All @@ -20,60 +22,123 @@ import { generatePutObjectOptions } from '@/modules/file/utils/generatePubObject
import axios from 'axios';
import { headObject, queryLockFee } from '@/facade/object';
import Long from 'long';
import { formatLockFee } from '@/utils/object';
import { TCreateObject } from '@bnb-chain/greenfield-chain-sdk';
import { reverseVisibilityType } from '@/utils/constant';
import { genCreateObjectTx } from '@/modules/file/utils/genCreateObjectTx';
import { resolve } from '@/facade/common';
import { broadcastFault, createTxFault, simulateFault } from '@/facade/error';
import { isEmpty } from 'lodash-es';

interface GlobalTasksProps {}

export const GlobalTasks = memo<GlobalTasksProps>(function GlobalTasks() {
const dispatch = useAppDispatch();
const { loginAccount } = useAppSelector((root) => root.persist);
const { spInfo } = useAppSelector((root) => root.sp);
const { primarySp } = useAppSelector((root) => root.object);
const hashTask = useAppSelector(selectHashTask);
const { spInfo: spInfos } = useAppSelector((root) => root.sp);
const { primarySp, bucketName} = useAppSelector((root) => root.object);
const { tmpAccount } = useAppSelector((root) => root.global);
const { sps: globalSps } = useAppSelector((root) => root.sp);
const hashTask = useAppSelector(selectHashTask(loginAccount));
console.log('hashTask', hashTask);
const checksumApi = useChecksumApi();
const [counter, setCounter] = useState(0);
const queue = useAppSelector(selectUploadQueue(loginAccount));
const upload = queue.filter((t) => t.status === 'UPLOAD');
const wait = queue.filter((t) => t.status === 'WAIT');
const ready = queue.filter((t) => t.status === 'READY');
const offset = 3 - upload.length;

const select3Task = useMemo(() => {
if (offset <= 0) return [];
return wait.slice(0, offset).map((p) => p.id);
}, [offset, wait]);
return ready.slice(0, offset).map((p) => p.id);
}, [offset, ready]);

const sealQueue = queue.filter((q) => q.status === 'SEAL').map((s) => s.id);

useAsyncEffect(async () => {
if (!hashTask) return;
dispatch(updateHashStatus({ id: hashTask.id, status: 'HASH' }));
const res = await checksumApi?.generateCheckSumV2(hashTask.file);
const params = {
primarySpAddress: primarySp.operatorAddress,
createAt: Long.fromInt(Math.floor(hashTask.id / 1000)),
payloadSize: Long.fromInt(hashTask.file.size),
};
const [data, error] = await queryLockFee(params);
dispatch(updateUploadStatus({ ids: [hashTask.id], status: 'HASH', account: loginAccount }));
const res = await checksumApi?.generateCheckSumV2(hashTask.file.file);
if (isEmpty(res)) {
dispatch(updateUploadMsg({ id: hashTask.id, msg: 'calculating hash error', account: loginAccount }));
return;
}
const { expectCheckSums } = res!;
dispatch(
updateHashChecksum({
updateUploadChecksum({
account: loginAccount,
id: hashTask.id,
checksum: expectCheckSums,
lockFee: formatLockFee(data?.amount),
}),
);
}, [hashTask, dispatch]);

// todo refactor
const runUploadTask = async (task: UploadFile) => {
// 1. get approval from sp
debugger;
const domain = getDomain();
const { seedString } = await dispatch(getSpOffChainData(loginAccount, task.sp));
const secondarySpAddresses = globalSps
.filter((item: any) => item.operator !== primarySp.operatorAddress)
.map((item: any) => item.operatorAddress);
const spInfo = {
endpoint: primarySp.endpoint,
primarySp: primarySp.operatorAddress,
sealAddress: primarySp.sealAddress,
secondarySpAddresses,
};
const finalName = [...task.prefixFolders, task.file.name].join('/');
const createObjectPayload: TCreateObject = {
bucketName,
objectName: finalName,
creator: tmpAccount.address,
visibility: reverseVisibilityType[task.visibility],
fileType: task.file.type || 'application/octet-stream',
contentLength: task.file.size,
expectCheckSums: task.checksum,
spInfo,
signType: 'authTypeV1',
privateKey: tmpAccount.privateKey,
};
const [createObjectTx, _createError] = await genCreateObjectTx(createObjectPayload).then(
resolve,
createTxFault,
);
if (_createError) {
return dispatch(updateUploadTaskMsg({
account: loginAccount,
id: task.id,
msg: _createError,
}))
}

const [simulateInfo, simulateError] = await createObjectTx!
.simulate({
denom: 'BNB',
})
.then(resolve, simulateFault);

const broadcastPayload = {
denom: 'BNB',
gasLimit: Number(simulateInfo?.gasLimit),
gasPrice: simulateInfo?.gasPrice || '5000000000',
payer: tmpAccount.address,
granter: loginAccount,
privateKey: tmpAccount.privateKey,
};
const [res, error] = await createObjectTx!
.broadcast(broadcastPayload)
.then(resolve, broadcastFault);

if (error) {
console.log('error', error)
}
const uploadOptions = await generatePutObjectOptions({
bucketName: task.bucketName,
objectName: [...task.folders, task.file.name].join('/'),
objectName: [...task.prefixFolders, task.file.name].join('/'),
body: task.file.file,
endpoint: spInfo[task.sp].endpoint,
txnHash: task.createHash,
endpoint: spInfos[task.sp].endpoint,
txnHash: res?.transactionHash || '',
userAddress: loginAccount,
domain,
seedString,
Expand Down Expand Up @@ -107,8 +172,8 @@ export const GlobalTasks = memo<GlobalTasksProps>(function GlobalTasks() {

const _tasks = await Promise.all(
tasks.map(async (task) => {
const { bucketName, folders, file } = task;
const objectName = [...folders, file.name].join('/');
const { bucketName, prefixFolders, file } = task;
const objectName = [...prefixFolders, file.name].join('/');
const objectInfo = await headObject(bucketName, objectName);
if (!objectInfo || ![0, 1].includes(objectInfo.objectStatus)) {
dispatch(
Expand Down
4 changes: 2 additions & 2 deletions apps/dcellar-web-ui/src/components/layout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useDebounceEffect, useMount } from 'ahooks';
import { setupBnbPrice, setupTmpAvailableBalance, setupTmpLockFee } from '@/store/slices/global';
import { useAppDispatch, useAppSelector } from '@/store';
import { useLogin } from '@/hooks/useLogin';
import { GasList } from './GasList';
import { GasObjects } from './GasObjects';
import { TaskManagement } from '@/modules/upload/TaskManagement';
import { GlobalTasks } from '@/components/layout/Header/GlobalTasks';

Expand Down Expand Up @@ -62,7 +62,7 @@ export const Header = ({ taskManagement = true }: { taskManagement?: boolean })
<>
<GlobalTasks />
<StreamBalance />
<GasList />
<GasObjects />
<Flex
w="340px"
ref={ref}
Expand Down
69 changes: 69 additions & 0 deletions apps/dcellar-web-ui/src/facade/account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { getClient } from '@/base/client';
import { GRNToString, MsgCreateObjectTypeUrl, MsgDeleteObjectTypeUrl, PermissionTypes, newBucketGRN } from '@bnb-chain/greenfield-chain-sdk';
import { Coin } from '@bnb-chain/greenfield-cosmos-types/cosmos/base/v1beta1/coin';
import { Wallet } from 'ethers';
import { parseEther } from 'ethers/lib/utils.js';
import { resolve } from './common';
import { ErrorResponse, broadcastFault, commonFault, simulateFault } from './error';
import { UNKNOWN_ERROR } from '@/modules/file/constant';
import { TTmpAccount } from '@/store/slices/global';

export type QueryBalanceRequest = { address: string; denom?: string };

Expand All @@ -13,3 +20,65 @@ export const getAccountBalance = async ({
.catch(() => ({ balance: { amount: '0', denom } }));
return balance!;
};

export const createTmpAccount = async ({address, bucketName, amount}: any): Promise<ErrorResponse | [TTmpAccount, null]> => {
// 1. create temporary account
const wallet = Wallet.createRandom();
console.log('wallet', wallet.address, wallet.privateKey);

// 2. allow temporary account to submit specified tx and amount
const client = await getClient();
const grantAllowanceTx = await client.feegrant.grantAllowance({
granter: address,
grantee: wallet.address,
allowedMessages: [MsgCreateObjectTypeUrl],
amount: parseEther(amount || '0.1').toString(),
denom: 'BNB',
});

// 3. Put bucket policy so that the temporary account can create objects within this bucket
const statement: PermissionTypes.Statement = {
effect: PermissionTypes.Effect.EFFECT_ALLOW,
actions: [PermissionTypes.ActionType.ACTION_CREATE_OBJECT],
resources: [GRNToString(newBucketGRN(bucketName))],
};
const [putPolicyTx, putPolicyError] = await client.bucket.putBucketPolicy(bucketName, {
operator: address,
statements: [statement],
principal: {
type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT,
value: wallet.address,
},
}).then(resolve, commonFault);
if (!putPolicyTx) {
return [null, putPolicyError];
}

// 4. broadcast txs include 2 msg
const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx]);
const [simulateInfo, simulateError] = await txs.simulate({
denom: 'BNB',
}).then(resolve, simulateFault);
if (simulateError) {
return [null, simulateError];
}

console.log('simuluateInfo', simulateInfo);

const [res, error] = await txs.broadcast({
denom: 'BNB',
gasLimit: Number(210000),
gasPrice: '5000000000',
payer: address,
granter: '',
}).then(resolve, broadcastFault);

if (res && res.code !== 0 || error) {
return [null, error || UNKNOWN_ERROR];
}

return [{
address: wallet.address,
privateKey: wallet.privateKey
}, null];
}
14 changes: 14 additions & 0 deletions apps/dcellar-web-ui/src/facade/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type ErrorMsg = string;

export const E_GET_GAS_FEE_LACK_BALANCE_ERROR = `Current available balance is not enough for gas simulation, please check.`;
export const E_UNKNOWN_ERROR = `Unknown error. Please try again later.`;
export const E_SP_PRICE_FAILED = `Get SP storage price failed.`;
export const E_USER_REJECT_STATUS_NUM = '4001';
export const E_NOT_FOUND = 'NOT_FOUND';
export const E_PERMISSION_DENIED = 'PERMISSION_DENIED';
Expand All @@ -24,6 +25,7 @@ export const E_CAL_OBJECT_HASH = 'CAL_OBJECT_HASH';
export const E_OBJECT_NAME_EXISTS = 'OBJECT_NAME_EXISTS';
export const E_ACCOUNT_BALANCE_NOT_ENOUGH = 'ACCOUNT_BALANCE_NOT_ENOUGH';
export const E_NO_PERMISSION = 'NO_PERMISSION';
export const E_SP_STORAGE_PRICE_FAILED = 'SP_STORAGE_PRICE_FAILED';
export declare class BroadcastTxError extends Error {
readonly code: number;
readonly codespace: string;
Expand Down Expand Up @@ -81,3 +83,15 @@ export const commonFault = (e: any): ErrorResponse => {
}
return [null, E_UNKNOWN_ERROR];
};

export const queryLockFeeFault = (e: any): ErrorResponse => {
console.log('e', e);
if (e?.message.includes('storage price')) {
return [null, E_SP_PRICE_FAILED];
}
if (e?.message) {
return [null, e?.message];
}

return [null, E_UNKNOWN_ERROR];
}
5 changes: 3 additions & 2 deletions apps/dcellar-web-ui/src/facade/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
E_UNKNOWN,
ErrorMsg,
ErrorResponse,
queryLockFeeFault,
simulateFault,
} from '@/facade/error';
import { getObjectInfoAndBucketQuota, resolve } from '@/facade/common';
Expand All @@ -33,6 +34,7 @@ import {
QueryLockFeeRequest,
} from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/query';
import { signTypedDataV4 } from '@/utils/signDataV4';
import BigNumber from 'bignumber.js';

export type DeliverResponse = Awaited<ReturnType<TxResponse['broadcast']>>;

Expand Down Expand Up @@ -259,8 +261,7 @@ export const cancelCreateObject = async (params: any, Connector: any): Promise<a

export const queryLockFee = async (params: QueryLockFeeRequest) => {
const client = await getClient();
const res = await client.storage.queryLockFee(params);
return await client.storage.queryLockFee(params).then(resolve, commonFault);
return await client.storage.queryLockFee(params).then(resolve, queryLockFeeFault);
};

export const headObject = async (bucketName: string, objectName: string) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/dcellar-web-ui/src/modules/file/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const UNKNOWN_ERROR_URL = `${assetPrefix}/images/files/unknown.svg`;
// status_TITLE
const FILE_TITLE_UPLOADING = 'Uploading File';
const OBJECT_TITLE_CREATING = 'Creating Object';
const OBJECT_AUTH_TEMP_ACCOUNT_CREATING = 'Uploading';
const FILE_TITLE_DOWNLOADING = 'Downloading File';
const FILE_TITLE_DELETING = 'Deleting File';
const FILE_TITLE_CANCELING = 'Canceling Uploading';
Expand Down Expand Up @@ -120,4 +121,5 @@ export {
UNKNOWN_ERROR_URL,
FILE_UPLOAD_STATIC_URL,
OBJECT_TITLE_CREATING,
OBJECT_AUTH_TEMP_ACCOUNT_CREATING,
};
Loading

0 comments on commit 18d3301

Please sign in to comment.