Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/send token confirm page #138

Merged
merged 4 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/wallet/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const LoginPage = lazy(() => import('./pages/login'));
const OidcLoginPage = lazy(() => import('./pages/oidc-login'));
const TokenDetailPage = lazy(() => import('./pages/token-detail'));
const SendTokenPage = lazy(() => import('./pages/send-token'));
const SendTokenConfirmPage = lazy(() => import('./pages/send-token/confirm-page'));
const ReceiveTokenPage = lazy(() => import('./pages/receive-token'));
const PasswordPage = lazy(() => import('./pages/password'));
const SettingsPage = lazy(() => import('./pages/settings'));
Expand Down Expand Up @@ -103,6 +104,7 @@ const App: FC = observer(() => {
<Route path="/account-manage" element={<AccountManagePage />} />
<Route path="/token/:addressOrSymbol" element={<TokenDetailPage />} />
<Route path="/send/:addressOrSymbol?" element={<SendTokenPage />} />
<Route path="/send/confirm" element={<SendTokenConfirmPage />} />
<Route path="/receive" element={<ReceiveTokenPage />} />
</>
)}
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet/src/assets/copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/wallet/src/assets/external.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions apps/wallet/src/assets/transfer-loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions apps/wallet/src/assets/transfer-success.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 212 additions & 0 deletions apps/wallet/src/pages/send-token/confirm-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { observer } from "mobx-react";
import { FC, useEffect, useMemo, useState } from "react";
import hibitIdSession from "../../stores/session";
import { useNavigate } from "react-router-dom";
import SvgGo from '../../assets/right-arrow.svg?react';
import SvgLoading from '../../assets/transfer-loading.svg?react';
import SvgSuccess from '../../assets/transfer-success.svg?react';
import SvgExternal from '../../assets/external.svg?react';
import { useTokenBalanceQuery, useTokenQuery } from "../../apis/react-query/token";
import BigNumber from "bignumber.js";
import toaster from "../../components/Toaster";
import { useMutation, useQuery } from "@tanstack/react-query";
import CopyButton from "../../components/CopyButton";
import { sendTokenStore } from "./store";
import { formatNumber } from "../../utils/formatter";
import { ChainAssetType } from "../../utils/basicTypes";
import { getChainTxLink } from "../../utils/link";

const SendTokenConfirmPage: FC = observer(() => {
const [errMsg, setErrMsg] = useState<string>('')
const [transferResult, setTransferResult] = useState<{
state: 'pending' | 'done',
txId: string
}>({
state: 'pending',
txId: ''
})
const navigate = useNavigate()

const { state } = sendTokenStore
const nativeTokenQuery = useTokenQuery(hibitIdSession.chainInfo.nativeAssetSymbol)
const nativeBalanceQuery = useTokenBalanceQuery(nativeTokenQuery.data || undefined)
const feeQuery = useQuery({
queryKey: ['estimatedFee', state],
queryFn: async () => {
if (!hibitIdSession.walletPool || !state.token) {
return null
}
return await hibitIdSession.walletPool.getEstimatedFee(
state.toAddress,
new BigNumber(state.amount),
state.token,
)
},
refetchInterval: 5000,
})

const minNativeBalance = useMemo(() => {
if (!feeQuery.data || !state.token) {
return null
}
if (state.token.chainAssetType.equals(ChainAssetType.Native)) {
return new BigNumber(state.amount).plus(feeQuery.data)
} else {
return feeQuery.data
}
}, [state, feeQuery.data])
Comment on lines +48 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize useMemo Calculation

The useMemo hook is used to calculate minNativeBalance. Ensure that the dependencies array is complete and that the calculation is necessary to optimize performance.

const minNativeBalance = useMemo(() => {
  if (!feeQuery.data || !state.token) {
    return null;
  }
  return state.token.chainAssetType.equals(ChainAssetType.Native)
    ? new BigNumber(state.amount).plus(feeQuery.data)
    : feeQuery.data;
}, [state.amount, state.token, feeQuery.data]);


useEffect(() => {
if (!nativeBalanceQuery.data || !minNativeBalance) {
return
}
if (nativeBalanceQuery.data.lt(minNativeBalance)) {
setErrMsg(`Insufficient gas in your wallet (at least ${formatNumber(minNativeBalance)} ${nativeTokenQuery.data?.assetSymbol})`)
} else {
setErrMsg('')
}
}, [nativeBalanceQuery.data, feeQuery.data, nativeTokenQuery.data])

const transferMutation = useMutation({
mutationFn: async ({ address, amount }: {
address: string
amount: string
}) => {
if (!hibitIdSession.walletPool || !state.token) {
throw new Error('Wallet or token not ready')
}
return await hibitIdSession.walletPool.transfer(
address,
new BigNumber(amount),
state.token
)
}
})
Comment on lines +70 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider Mutation Error Handling

The useMutation hook for transferMutation lacks specific error handling. Consider adding onError and onSuccess callbacks for better control over mutation outcomes.

const transferMutation = useMutation({
  mutationFn: async ({ address, amount }: { address: string; amount: string }) => {
    if (!hibitIdSession.walletPool || !state.token) {
      throw new Error('Wallet or token not ready');
    }
    return await hibitIdSession.walletPool.transfer(address, new BigNumber(amount), state.token);
  },
  onError: (error) => {
    console.error('Transfer failed:', error);
    toaster.error(error instanceof Error ? error.message : JSON.stringify(error));
  },
  onSuccess: (data) => {
    console.log('Transfer successful:', data);
  },
});


const handleSend = async () => {
if (!hibitIdSession.walletPool || !state.token || errMsg) {
return
}
try {
const txId = await transferMutation.mutateAsync({
address: state.toAddress,
amount: state.amount,
})
console.debug('[txId]', txId)
setTransferResult({ state: 'done', txId })
sendTokenStore.reset()
} catch (e) {
console.error(e)
setTransferResult({ state: 'pending', txId: '' })
toaster.error(e instanceof Error ? e.message : JSON.stringify(e))
}
Comment on lines +86 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve Error Handling in handleSend Function

The handleSend function has basic error handling. Consider enhancing it by categorizing errors and providing user-friendly messages.

const handleSend = async () => {
  if (!hibitIdSession.walletPool || !state.token || errMsg) {
    return;
  }
  try {
    const txId = await transferMutation.mutateAsync({
      address: state.toAddress,
      amount: state.amount,
    });
    console.debug('[txId]', txId);
    setTransferResult({ state: 'done', txId });
    sendTokenStore.reset();
  } catch (e: unknown) {
    console.error('Error during transfer:', e);
    setTransferResult({ state: 'pending', txId: '' });
    const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred';
    toaster.error(errorMessage);
  }
};

}

if (transferMutation.isPending) {
return (
<div className="h-full px-6 flex justify-center items-center">
<div className="flex flex-col items-center">
<SvgLoading />
<span>Await confirmation</span>
</div>
</div>
)
}

if (transferResult.state === 'done') {
const txLink = getChainTxLink(hibitIdSession.chainInfo.chainId, transferResult.txId)

return (
<div className="h-full px-6 flex flex-col overflow-auto">
<div className="flex-1 flex flex-col gap-8 justify-center items-center">
<SvgSuccess />
<span className="text-success">Transaction finished</span>
{txLink && (
<a className="flex items-center gap-2" href={txLink} target="_blank" rel="noreferrer">
<span>view in explorer</span>
<SvgExternal />
</a>
)}
</div>
<button className="btn btn-sm" onClick={() => {
navigate('/')
}}>
Close
</button>
</div>
)
}

return (
<div className="h-full px-6 flex flex-col gap-6 overflow-auto">
<div>
<button className="btn btn-ghost btn-sm gap-2 items-center pl-0" onClick={() => navigate(-1)}>
<SvgGo className="size-6 rotate-180" />
<span className="text-xs">Edit</span>
</button>
</div>

<div className="flex-1 flex flex-col gap-6">
<div>
<label className="form-control w-full">
<div className="label">
<span className="label-text text-neutral text-xs">To</span>
</div>
<div className="max-w-full p-2 pr-1 flex items-center gap-2 bg-base-100 rounded-xl text-primary">
<span className="text-xs break-all">{state.toAddress}</span>
<CopyButton copyText={state.toAddress} />
</div>
</label>
</div>
<div>
<label className="form-control w-full">
<div className="label">
<span className="label-text text-neutral text-xs">Amount</span>
</div>
<div className="flex items-center justify-between">
<span className="text-primary">{formatNumber(state.amount)}</span>
<span>{state.token?.assetSymbol}</span>
</div>
</label>
</div>
<div>
<label className="form-control w-full">
<div className="label">
<span className="label-text text-neutral text-xs">Network fee estimation</span>
</div>
<div className="flex items-center justify-between">
<span className="text-primary">
{!feeQuery.isFetching ? (
<span>~{formatNumber(feeQuery.data)}</span>
) : (
<span className="loading loading-spinner size-4" />
)}
</span>
<span>{nativeTokenQuery.data?.assetSymbol}</span>
</div>
{errMsg && (
<div className="label">
<span className="label-text-alt text-error">{errMsg}</span>
</div>
)}
</label>
</div>
</div>

<div className="flex items-center gap-2">
<button className="btn btn-sm flex-1">
Cancel
</button>
<button
className="btn btn-sm btn-primary flex-1 disabled:opacity-70"
onClick={handleSend}
disabled={!!errMsg || feeQuery.isFetching || !nativeBalanceQuery.data}
>
Confirm
</button>
</div>
</div>
)
})

export default SendTokenConfirmPage
42 changes: 10 additions & 32 deletions apps/wallet/src/pages/send-token/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ import TokenSelect from "../../components/TokenSelect";
import { RootAssetInfo } from "../../apis/models";
import BigNumber from "bignumber.js";
import { formatNumber } from "../../utils/formatter";
import toaster from "../../components/Toaster";
import { useMutation } from "@tanstack/react-query";
import LoaderButton from "../../components/LoaderButton";
import { object, string } from "yup";
import { walletAddressValidate } from "../../utils/validator";
import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, useForm } from "react-hook-form";
import { SYSTEM_MAX_DECIMALS } from "../../utils/formatter/numberFormatter";
import { sendTokenStore } from "./store";

const SendTokenPage: FC = observer(() => {
const { addressOrSymbol } = useParams()
const [token, setToken] = useState<RootAssetInfo | null>(null)
const { state, setState } = sendTokenStore
const navigate = useNavigate()

const tokenQuery = useTokenQuery(addressOrSymbol ?? '')
Expand Down Expand Up @@ -60,29 +60,13 @@ const SendTokenPage: FC = observer(() => {
formState: { errors },
} = useForm({
defaultValues: {
toAddress: '',
amount: ''
toAddress: state.toAddress || '',
amount: state.amount || '',
},
resolver: yupResolver(formSchema),
mode: 'onChange'
})

const transferMutation = useMutation({
mutationFn: async ({ address, amount }: {
address: string
amount: string
}) => {
if (!hibitIdSession.walletPool || !token) {
throw new Error('Wallet or token not ready')
}
return await hibitIdSession.walletPool.transfer(
address,
new BigNumber(amount),
token
)
}
})

useEffect(() => {
if (tokenQuery.data) {
setToken(tokenQuery.data)
Expand All @@ -93,17 +77,12 @@ const SendTokenPage: FC = observer(() => {
if (!hibitIdSession.walletPool || !token) {
return
}
try {
const txId = await transferMutation.mutateAsync({
address: toAddress,
amount
})
console.debug('[txId]', txId)
toaster.success('Transfer success')
} catch (e) {
console.error(e)
toaster.error(e instanceof Error ? e.message : JSON.stringify(e))
}
setState({
toAddress,
token,
amount
})
navigate('/send/confirm')
})

return (
Expand Down Expand Up @@ -200,7 +179,6 @@ const SendTokenPage: FC = observer(() => {
<LoaderButton
className="btn btn-block btn-sm disabled:opacity-70"
onClick={handleSend}
loading={transferMutation.isPending}
>
Send
</LoaderButton>
Expand Down
34 changes: 34 additions & 0 deletions apps/wallet/src/pages/send-token/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RootAssetInfo } from "../../apis/models";
import { makeAutoObservable } from "mobx";

export interface SendTokenState {
token: RootAssetInfo | null
toAddress: string
amount: string
}

export class SendTokenStore {
state: SendTokenState = {
token: null,
toAddress: '',
amount: '',
}

constructor() {
makeAutoObservable(this)
}

setState = (state: SendTokenState) => {
this.state = { ...state }
}

reset = () => {
this.state = {
token: null,
toAddress: '',
amount: '',
}
}
}

export const sendTokenStore = new SendTokenStore()
Loading
Loading