Skip to content

Commit

Permalink
fix: Fix generate transaction when deposit all without balance.
Browse files Browse the repository at this point in the history
  • Loading branch information
yanguoyu committed Jul 21, 2023
1 parent 0b8d7fb commit a15e52f
Show file tree
Hide file tree
Showing 21 changed files with 427 additions and 471 deletions.
43 changes: 20 additions & 23 deletions packages/neuron-ui/src/components/ApproveMultisigTxDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,25 @@ import getMultisigSignStatus from 'utils/getMultisigSignStatus'
import { useBroadcast, useExport, useSignAndBroadcast, useSignAndExport, useTabView } from './hooks'
import styles from './approveMultisigTx.module.scss'

interface CellProps {
lock: CKBComponents.Script
type?: CKBComponents.Script
data?: string
capacity: string
lockHash: string
}

const Cell = React.memo(({ cell, isMainnet }: { cell: CellProps; isMainnet: boolean }) => {
const address = useMemo(() => ckbCore.utils.scriptToAddress(cell.lock, isMainnet), [cell, isMainnet])
return (
<div className={styles.cellItem}>
<div>
{address.slice(0, 6)}...{address.slice(-6)}
<span className={`${cell.type ? styles.activity : ''} ${styles.tag}`}>Type</span>
<span className={`${cell.data && cell.data !== '0x' ? styles.activity : ''} ${styles.tag}`}>Data</span>
<ScriptTag script={cell.lock} isMainnet={isMainnet} />
const Cell = React.memo(
({ cell, isMainnet }: { cell: State.DetailedInput | State.DetailedOutput; isMainnet: boolean }) => {
const address = useMemo(
() => (cell.lock ? ckbCore.utils.scriptToAddress(cell.lock, isMainnet) : ''),
[cell, isMainnet]
)
return (
<div className={styles.cellItem}>
<div>
{address.slice(0, 6)}...{address.slice(-6)}
<span className={`${cell.type ? styles.activity : ''} ${styles.tag}`}>Type</span>
<span className={`${cell.data && cell.data !== '0x' ? styles.activity : ''} ${styles.tag}`}>Data</span>
<ScriptTag script={cell.lock} isMainnet={isMainnet} />
</div>
<div>{`${shannonToCKBFormatter(cell.capacity ?? '0')} CKB`}</div>
</div>
<div>{`${shannonToCKBFormatter(cell.capacity)} CKB`}</div>
</div>
)
})
)
}
)
const ApproveMultisigTxDialog = ({
multisigConfig,
closeDialog,
Expand Down Expand Up @@ -152,12 +149,12 @@ const ApproveMultisigTxDialog = ({
<div className={styles.conciseData}>
<div className={styles.inputWrap}>
<h2>Inputs</h2>
{offlineSignJson.transaction?.inputs?.map((input: CellProps) => (
{offlineSignJson.transaction?.inputs?.map(input => (
<Cell cell={input} isMainnet={isMainnet} key={input.lockHash} />
))}
</div>
<h2>Outputs</h2>
{offlineSignJson.transaction?.outputs?.map((output: CellProps) => (
{offlineSignJson.transaction?.outputs?.map(output => (
<Cell cell={output} isMainnet={isMainnet} key={output.lockHash} />
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@
font-size: 14px;
line-height: 20px;
color: var(--main-text-color);

& > div {
&:nth-last-child(1) {
width: 230px;
text-align: right;
}
}
}

.notice {
$lineHeight: 24px;
display: flex;
box-sizing: border-box;
max-width: 648px;
margin-top: 22px;
border: 1px solid rgba(252, 136, 0, 0.2);
padding: 7px 54px;
Expand All @@ -65,7 +71,7 @@
& > svg {
$size: 14px;
flex-shrink: 0;
margin-top: ($lineHeight - $size) / 2;
margin: calc(($lineHeight - $size) / 2) 4px 0 0;
width: $size;
height: $size;

Expand Down
270 changes: 270 additions & 0 deletions packages/neuron-ui/src/components/DepositDialog/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { isErrorWithI18n } from 'exceptions'
import { TFunction } from 'i18next'
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
generateDaoDepositAllTx as generateDaoDepositAllTxAPI,
generateDaoDepositTx as generateDaoDepositTxAPI,
} from 'services/remote'
import { AppActions, useDispatch } from 'states'
import {
CKBToShannonFormatter,
ErrorCode,
isSuccessResponse,
padFractionDigitsIfDecimal,
shannonToCKBFormatter,
useClearGeneratedTx,
validateAmount,
} from 'utils'
import { MAX_DECIMAL_DIGITS, MIN_DEPOSIT_AMOUNT, SHANNON_CKB_RATIO } from 'utils/const'

const PERCENT_100 = 100

function checkDepositValue(depositValue: string, t: TFunction): string | undefined {
try {
validateAmount(depositValue)
} catch (err) {
if (isErrorWithI18n(err)) {
return t(`messages.codes.${err.code}`, {
fieldName: 'deposit',
fieldValue: depositValue,
length: MAX_DECIMAL_DIGITS,
})
}
return undefined
}
if (BigInt(CKBToShannonFormatter(depositValue)) < BigInt(MIN_DEPOSIT_AMOUNT * SHANNON_CKB_RATIO)) {
return t('nervos-dao.minimal-fee-required', { minimal: MIN_DEPOSIT_AMOUNT })
}
return undefined
}

function generateDaoDepositTx({
walletID,
capacity,
suggestFeeRate,
t,
}: {
walletID: string
capacity: string
suggestFeeRate: number
t: TFunction
}): Promise<State.GeneratedTx | null> {
return generateDaoDepositTxAPI({
feeRate: `${suggestFeeRate}`,
capacity,
walletID,
}).then(res => {
if (isSuccessResponse(res)) {
return res.result
}
if (res.status === 0) {
throw new Error(`${typeof res.message === 'string' ? res.message : res.message.content}`)
} else if (res.status === ErrorCode.CapacityNotEnoughForChange) {
throw new Error(t(`messages.codes.106`))
} else {
throw new Error(t(`messages.codes.${res.status}`))
}
})
}

function generateDaoDepositAllTx({
suggestFeeRate,
isBalanceReserved,
walletID,
}: {
suggestFeeRate: number
isBalanceReserved: boolean
walletID: string
}): Promise<State.GeneratedTx | null> {
return generateDaoDepositAllTxAPI({
walletID,
feeRate: `${suggestFeeRate}`,
isBalanceReserved,
}).then(res => {
if (isSuccessResponse(res)) {
return res.result
}
throw new Error(`${typeof res.message === 'string' ? res.message : res.message.content}`)
})
}

export const useGenerateDaoDepositTx = ({
walletID,
isBalanceReserved,
depositValue,
suggestFeeRate,
showDepositDialog,
slidePercent,
}: {
walletID: string
isBalanceReserved: boolean
depositValue: string
suggestFeeRate: number
showDepositDialog: boolean
slidePercent: number
}) => {
const timer = useRef<ReturnType<typeof setTimeout>>()
const [errorMessage, setErrorMessage] = useState('')
const [maxDepositValue, setMaxDepositValue] = useState<string | undefined>()
const [t] = useTranslation()
const dispatch = useDispatch()
const clearGeneratedTx = useClearGeneratedTx()
const isDepositAll = useMemo(() => slidePercent === PERCENT_100, [slidePercent])
useEffect(() => {
clearTimeout(timer.current)
if (!showDepositDialog) {
return
}
timer.current = setTimeout(() => {
setErrorMessage('')
const errorDepositValue = checkDepositValue(depositValue, t)
if (errorDepositValue) {
clearGeneratedTx()
setErrorMessage(errorDepositValue)
return
}

const generateDaoDepositResult: Promise<State.GeneratedTx | null> = isDepositAll
? generateDaoDepositAllTx({ walletID, isBalanceReserved, suggestFeeRate })
: generateDaoDepositTx({ walletID, capacity: CKBToShannonFormatter(depositValue), suggestFeeRate, t })
generateDaoDepositResult
.then(res => {
dispatch({
type: AppActions.UpdateGeneratedTx,
payload: res,
})
if (isDepositAll) {
setMaxDepositValue(shannonToCKBFormatter(res?.outputs[0]?.capacity ?? '0', false, ''))
if (!isBalanceReserved) {
setErrorMessage(t('messages.remain-ckb-for-withdraw'))
}
}
})
.catch((err: unknown) => {
clearGeneratedTx()
setErrorMessage(err instanceof Error ? err.message : '')
})
})
}, [
clearGeneratedTx,
dispatch,
walletID,
t,
setErrorMessage,
isBalanceReserved,
depositValue,
suggestFeeRate,
showDepositDialog,
isDepositAll,
])
return {
errorMessage,
maxDepositValue: isDepositAll ? maxDepositValue ?? depositValue : null,
}
}

function calculatePercent(amount: string, total: string) {
if (!total || total === '0') return 0
return +((BigInt(PERCENT_100) * BigInt(amount)) / BigInt(total)).toString()
}

export const useDepositValue = (balance: string) => {
const [depositValue, setDepositValue] = useState(`${MIN_DEPOSIT_AMOUNT}`)
const [slidePercent, setSlidePercent] = useState(
calculatePercent(CKBToShannonFormatter(`${MIN_DEPOSIT_AMOUNT}`), balance)
)
const onSliderChange = useCallback(
(percent: number) => {
setSlidePercent(percent)
const amount = shannonToCKBFormatter(
((BigInt(percent) * BigInt(balance)) / BigInt(PERCENT_100)).toString(),
false,
''
)
setDepositValue(padFractionDigitsIfDecimal(amount, 8))
},
[balance]
)
const onChangeDepositValue = useCallback(
(e: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = e.currentTarget
const amount = value.replace(/,/g, '')
if (Number.isNaN(+amount) || /[^\d.]/.test(amount) || +amount < 0) {
return
}
setDepositValue(amount)
try {
validateAmount(amount)
const percent = calculatePercent(CKBToShannonFormatter(amount), balance)
setSlidePercent(percent >= PERCENT_100 ? 100 : percent)
} catch (error) {
// here we can ignore the error, it used to verify amount and set slide percent
}
},
[setDepositValue, balance]
)
const resetDepositValue = useCallback(() => {
setDepositValue(`${MIN_DEPOSIT_AMOUNT}`)
setSlidePercent(calculatePercent(CKBToShannonFormatter(`${MIN_DEPOSIT_AMOUNT}`), balance))
}, [balance])
return {
onChangeDepositValue,
setDepositValue,
depositValue,
slidePercent,
onSliderChange,
resetDepositValue,
}
}

export const useBalanceReserved = () => {
const [isBalanceReserved, setIsBalanceReserved] = useState(true)
const onIsBalanceReservedChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setIsBalanceReserved(!e.currentTarget.checked)
}
return {
isBalanceReserved,
onIsBalanceReservedChange,
setIsBalanceReserved,
}
}

export const useOnDepositDialogSubmit = ({
onCloseDepositDialog,
walletID,
}: {
onCloseDepositDialog: () => void
walletID: string
}) => {
const dispatch = useDispatch()
return useCallback(() => {
dispatch({
type: AppActions.RequestPassword,
payload: {
walletID,
actionType: 'send',
},
})
onCloseDepositDialog()
}, [dispatch, walletID, onCloseDepositDialog])
}

export const useOnDepositDialogCancel = ({
onCloseDepositDialog,
resetDepositValue,
setIsBalanceReserved,
}: {
onCloseDepositDialog: () => void
resetDepositValue: () => void
setIsBalanceReserved: Dispatch<SetStateAction<boolean>>
}) => {
const dispatch = useDispatch()
const clearGeneratedTx = useClearGeneratedTx()
return useCallback(() => {
onCloseDepositDialog()
resetDepositValue()
setIsBalanceReserved(true)
clearGeneratedTx()
}, [dispatch, onCloseDepositDialog, resetDepositValue, clearGeneratedTx])
}
Loading

0 comments on commit a15e52f

Please sign in to comment.