From 2fcc43dc80e2126538dce715d1f77cca9bfd1b1f Mon Sep 17 00:00:00 2001 From: devchenyan Date: Mon, 20 May 2024 15:42:01 +0800 Subject: [PATCH] feat: Supports amend transactions (#3134) * feat: Supports amend transactions * feat: Nervos DAO * fix: comments * fix: comments * fix: comments * fix: scriptToAddress * fix: tx.hash --------- Co-authored-by: Chen Yu --- .../amendPendingTransactionDialog.module.scss | 5 + .../AmendPendingTransactionDialog/hooks.ts | 236 ++++++++++++++++++ .../AmendPendingTransactionDialog/index.tsx | 157 ++++++++++++ .../src/components/AmendSUDTSend/hooks.ts | 31 ++- .../src/components/AmendSUDTSend/index.tsx | 95 ++----- .../src/components/AmendSend/hooks.ts | 33 ++- .../src/components/AmendSend/index.tsx | 19 +- .../src/components/History/RowExtend.tsx | 179 +++++++------ .../src/components/History/index.tsx | 3 +- packages/neuron-ui/src/locales/en.json | 3 +- packages/neuron-ui/src/locales/es.json | 3 +- packages/neuron-ui/src/locales/fr.json | 3 +- packages/neuron-ui/src/locales/zh-tw.json | 3 +- packages/neuron-ui/src/locales/zh.json | 3 +- .../neuron-ui/src/services/remote/offline.ts | 1 - .../src/services/remote/remoteApiWrapper.ts | 1 - packages/neuron-ui/src/types/App/index.d.ts | 1 + .../neuron-ui/src/types/Controller/index.d.ts | 1 + packages/neuron-ui/src/utils/const.ts | 4 + packages/neuron-wallet/src/controllers/api.ts | 5 +- .../src/controllers/transactions.ts | 9 - .../neuron-wallet/src/controllers/wallets.ts | 3 +- .../src/services/tx/transaction-generator.ts | 1 + 23 files changed, 567 insertions(+), 232 deletions(-) create mode 100644 packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss create mode 100644 packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts create mode 100644 packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss new file mode 100644 index 0000000000..15c6aeef62 --- /dev/null +++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss @@ -0,0 +1,5 @@ +@import '../../styles/mixin.scss'; + +.content { + width: 680px; +} diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts new file mode 100644 index 0000000000..cef45ef8e9 --- /dev/null +++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts @@ -0,0 +1,236 @@ +import React, { useState, useCallback, useEffect, useMemo } from 'react' +import { PasswordIncorrectException } from 'exceptions' +import { TFunction } from 'i18next' +import { getTransaction as getOnChainTransaction } from 'services/chain' +import { getTransaction as getSentTransaction, sendTx, invokeShowErrorMessage } from 'services/remote' +import { isSuccessResponse, ErrorCode, shannonToCKBFormatter, scriptToAddress } from 'utils' +import { FEE_RATIO } from 'utils/const' + +export const useInitialize = ({ + tx, + walletID, + t, + onClose, +}: { + tx: State.Transaction + walletID: string + t: TFunction + onClose: () => void +}) => { + const [transaction, setTransaction] = useState(null) + const [generatedTx, setGeneratedTx] = useState(null) + const [size, setSize] = useState(0) + const [minPrice, setMinPrice] = useState('0') + const [price, setPrice] = useState('0') + const [password, setPassword] = useState('') + const [pwdError, setPwdError] = useState('') + const [isSending, setIsSending] = useState(false) + + const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false) + + const fee = useMemo(() => { + const ratio = BigInt(FEE_RATIO) + const base = BigInt(size) * BigInt(price) + const curFee = base / ratio + if (curFee * ratio < base) { + return curFee + BigInt(1) + } + return curFee + }, [price, size]) + + const fetchInitData = useCallback(async () => { + const res = await getOnChainTransaction(tx.hash) + const { + // @ts-expect-error Replace-By-Fee (RBF) + min_replace_fee: minFee, + transaction: { outputsData }, + } = res + + if (!minFee) { + setIsConfirmedAlertShown(true) + } + + const txRes = await getSentTransaction({ hash: tx.hash, walletID }) + + if (isSuccessResponse(txRes)) { + const txResult = txRes.result + setTransaction({ + ...txResult, + outputsData, + }) + + setSize(txResult.size) + if (minFee) { + const mPrice = ((BigInt(minFee) * BigInt(FEE_RATIO)) / BigInt(txResult.size)).toString() + setMinPrice(mPrice) + setPrice(mPrice) + } + } + }, [tx, setIsConfirmedAlertShown, setPrice, setTransaction, setSize, setMinPrice]) + + useEffect(() => { + fetchInitData() + }, []) + + const onPwdChange = useCallback( + (e: React.SyntheticEvent) => { + const { value } = e.target as HTMLInputElement + setPassword(value) + setPwdError('') + }, + [setPassword, setPwdError] + ) + + const onSubmit = useCallback(async () => { + try { + // @ts-expect-error Replace-By-Fee (RBF) + const { min_replace_fee: minFee } = await getOnChainTransaction(tx.hash) + if (!minFee) { + setIsConfirmedAlertShown(true) + return + } + + if (!generatedTx) { + return + } + setIsSending(true) + + try { + const skipLastInputs = generatedTx.inputs.length > generatedTx.witnesses.length + + const res = await sendTx({ walletID, tx: generatedTx, password, skipLastInputs, amendHash: tx.hash }) + + if (isSuccessResponse(res)) { + onClose() + } else if (res.status === ErrorCode.PasswordIncorrect) { + setPwdError(t(new PasswordIncorrectException().message)) + } else { + invokeShowErrorMessage({ + title: t('messages.error'), + content: typeof res.message === 'string' ? res.message : res.message.content!, + }) + } + } catch (err) { + console.warn(err) + } finally { + setIsSending(false) + } + } catch { + // ignore + } + }, [walletID, tx, setIsConfirmedAlertShown, setPwdError, password, generatedTx, setIsSending]) + + return { + fee, + price, + setPrice, + generatedTx, + setGeneratedTx, + transaction, + setTransaction, + minPrice, + isConfirmedAlertShown, + onSubmit, + password, + onPwdChange, + pwdError, + isSending, + setIsSending, + } +} + +export const useOutputs = ({ + transaction, + isMainnet, + addresses, + sUDTAccounts, + fee, +}: { + transaction: State.GeneratedTx | null + isMainnet: boolean + addresses: State.Address[] + sUDTAccounts: State.SUDTAccount[] + fee: bigint +}) => { + const getLastOutputAddress = (outputs: State.DetailedOutput[]) => { + if (outputs.length === 1) { + return scriptToAddress(outputs[0].lock, { isMainnet }) + } + + const change = outputs.find(output => { + const address = scriptToAddress(output.lock, { isMainnet }) + return !!addresses.find(item => item.address === address && item.type === 1) + }) + + if (change) { + return scriptToAddress(change.lock, { isMainnet }) + } + + const receive = outputs.find(output => { + const address = scriptToAddress(output.lock, { isMainnet }) + return !!addresses.find(item => item.address === address && item.type === 0) + }) + if (receive) { + return scriptToAddress(receive.lock, { isMainnet }) + } + + const sudt = outputs.find(output => { + const address = scriptToAddress(output.lock, { isMainnet }) + return !!sUDTAccounts.find(item => item.address === address) + }) + if (sudt) { + return scriptToAddress(sudt.lock, { isMainnet }) + } + return '' + } + + const items: { + address: string + amount: string + capacity: string + isLastOutput: boolean + output: State.DetailedOutput + }[] = useMemo(() => { + if (transaction && transaction.outputs.length) { + const lastOutputAddress = getLastOutputAddress(transaction.outputs) + return transaction.outputs.map(output => { + const address = scriptToAddress(output.lock, { isMainnet }) + return { + capacity: output.capacity, + address, + output, + amount: shannonToCKBFormatter(output.capacity || '0'), + isLastOutput: address === lastOutputAddress, + } + }) + } + return [] + }, [transaction?.outputs]) + + const outputsCapacity = useMemo(() => { + const outputList = items.filter(item => !item.isLastOutput) + return outputList.reduce((total, cur) => { + return total + BigInt(cur.capacity || '0') + }, BigInt(0)) + }, [items]) + + const lastOutputsCapacity = useMemo(() => { + if (transaction) { + const inputsCapacity = transaction.inputs.reduce((total, cur) => { + return total + BigInt(cur.capacity || '0') + }, BigInt(0)) + + return inputsCapacity - outputsCapacity - fee + } + return undefined + }, [transaction, fee, outputsCapacity]) + + return { + items, + lastOutputsCapacity, + } +} + +export default { + useInitialize, +} diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx new file mode 100644 index 0000000000..49a52289b7 --- /dev/null +++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useMemo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useState as useGlobalState } from 'states' +import TextField from 'widgets/TextField' +import Dialog from 'widgets/Dialog' +import { MIN_AMOUNT, DAO_DATA } from 'utils/const' +import { isMainnet as isMainnetUtil, localNumberFormatter, shannonToCKBFormatter } from 'utils' +import AlertDialog from 'widgets/AlertDialog' +import styles from './amendPendingTransactionDialog.module.scss' +import { useInitialize, useOutputs } from './hooks' + +const AmendPendingTransactionDialog = ({ tx, onClose }: { tx: State.Transaction; onClose: () => void }) => { + const { + wallet: { id: walletID = '', addresses }, + chain: { networkID }, + settings: { networks = [] }, + sUDTAccounts, + } = useGlobalState() + const { t } = useTranslation() + + const isMainnet = isMainnetUtil(networks, networkID) + + const { + fee, + price, + setPrice, + transaction, + onSubmit, + minPrice, + isConfirmedAlertShown, + password, + onPwdChange, + pwdError, + generatedTx, + setGeneratedTx, + isSending, + } = useInitialize({ + tx, + walletID, + t, + onClose, + }) + + const { items, lastOutputsCapacity } = useOutputs({ + transaction, + isMainnet, + addresses, + sUDTAccounts, + fee, + }) + + const priceError = useMemo(() => { + return Number(price) < Number(minPrice) ? t('price-switch.errorTip', { minPrice }) : null + }, [price, minPrice]) + + const inputHint = t('price-switch.hintTip', { suggestFeeRate: minPrice }) + + const handlePriceChange = useCallback( + (e: React.SyntheticEvent) => { + const { value: inputValue } = e.currentTarget + + const value = inputValue.split('.')[0].replace(/[^\d]/g, '') + setPrice(value) + }, + [setPrice] + ) + + useEffect(() => { + if (transaction && lastOutputsCapacity !== undefined) { + const outputs = items.map(item => { + const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity + if (item.output.data === DAO_DATA) { + // eslint-disable-next-line no-param-reassign + item.output.daoData = DAO_DATA + } + return { + ...item.output, + capacity, + } + }) + + setGeneratedTx({ + ...transaction, + outputs, + }) + } + }, [lastOutputsCapacity, transaction, items, setGeneratedTx]) + + const disabled = !!( + isSending || + !generatedTx || + priceError || + lastOutputsCapacity === undefined || + lastOutputsCapacity < MIN_AMOUNT + ) + + return ( + <> + +
+ + + + +
+
+ + + ) +} + +AmendPendingTransactionDialog.displayName = 'AmendPendingTransactionDialog' + +export default AmendPendingTransactionDialog diff --git a/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts b/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts index 3b17fe22a6..199ec5f37c 100644 --- a/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts +++ b/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts @@ -2,8 +2,9 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react' import { TFunction } from 'i18next' import { AppActions } from 'states/stateProvider/reducer' import { getTransaction as getOnChainTransaction } from 'services/chain' -import { getTransaction as getSentTransaction, getTransactionSize, getTransactionList } from 'services/remote' +import { getTransaction as getSentTransaction, getTransactionList } from 'services/remote' import { isSuccessResponse } from 'utils' +import { FEE_RATIO } from 'utils/const' export const useInitialize = ({ hash, @@ -21,7 +22,7 @@ export const useInitialize = ({ const [minPrice, setMinPrice] = useState('0') const [price, setPrice] = useState('0') const [description, setDescription] = useState('') - const [showConfirmedAlert, setShowConfirmedAlert] = useState(false) + const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false) const [sudtInfo, setSudtInfo] = useState(null) const [txValue, setTxValue] = useState('0') @@ -34,7 +35,7 @@ export const useInitialize = ({ ) const fee = useMemo(() => { - const ratio = BigInt(1000) + const ratio = BigInt(FEE_RATIO) const base = BigInt(size) * BigInt(price) const curFee = base / ratio if (curFee * ratio < base) { @@ -50,7 +51,7 @@ export const useInitialize = ({ transaction: { outputsData }, } = await getOnChainTransaction(hash) if (!minFee) { - setShowConfirmedAlert(true) + setIsConfirmedAlertShown(true) } const listRes = await getTransactionList({ @@ -74,18 +75,14 @@ export const useInitialize = ({ setTransaction({ ...tx, outputsData }) - const sizeRes = await getTransactionSize(tx) - - if (isSuccessResponse(sizeRes) && typeof sizeRes.result === 'number') { - setSize(sizeRes.result) - if (minFee) { - const mPrice = ((BigInt(minFee) * BigInt(1000)) / BigInt(sizeRes.result)).toString() - setMinPrice(mPrice) - setPrice(mPrice) - } + setSize(tx.size) + if (minFee) { + const mPrice = ((BigInt(minFee) * BigInt(FEE_RATIO)) / BigInt(tx.size)).toString() + setMinPrice(mPrice) + setPrice(mPrice) } } - }, [hash, setShowConfirmedAlert, setPrice, setTransaction, setSize, setMinPrice]) + }, [hash, setIsConfirmedAlertShown, setPrice, setTransaction, setSize, setMinPrice]) useEffect(() => { fetchInitData() @@ -104,7 +101,7 @@ export const useInitialize = ({ // @ts-expect-error Replace-By-Fee (RBF) const { min_replace_fee: minFee } = await getOnChainTransaction(hash) if (!minFee) { - setShowConfirmedAlert(true) + setIsConfirmedAlertShown(true) return } @@ -123,7 +120,7 @@ export const useInitialize = ({ // ignore } }, - [dispatch, walletID, hash, setShowConfirmedAlert, transaction] + [dispatch, walletID, hash, setIsConfirmedAlertShown, transaction] ) return { @@ -135,7 +132,7 @@ export const useInitialize = ({ transaction, setTransaction, minPrice, - showConfirmedAlert, + isConfirmedAlertShown, onSubmit, sudtInfo, txValue, diff --git a/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx b/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx index 56734e28f7..5cdbc79812 100644 --- a/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx +++ b/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx @@ -18,6 +18,7 @@ import { } from 'utils' import { DEFAULT_SUDT_FIELDS } from 'utils/const' import AlertDialog from 'widgets/AlertDialog' +import { useOutputs } from 'components/AmendPendingTransactionDialog/hooks' import styles from './amendSUDTSend.module.scss' import { useInitialize } from './hooks' @@ -48,7 +49,7 @@ const AmendSUDTSend = () => { transaction, onSubmit, minPrice, - showConfirmedAlert, + isConfirmedAlertShown, sudtInfo, description, onDescriptionChange, @@ -61,6 +62,14 @@ const AmendSUDTSend = () => { t, }) + const { items, lastOutputsCapacity } = useOutputs({ + transaction, + isMainnet, + addresses, + sUDTAccounts, + fee, + }) + const priceError = useMemo(() => { return Number(price || '0') < Number(minPrice) ? t('price-switch.errorTip', { minPrice }) : null }, [price, minPrice]) @@ -71,7 +80,7 @@ const AmendSUDTSend = () => { (e: React.SyntheticEvent) => { const { value: inputValue } = e.currentTarget - const value = inputValue.split('.')[0].replace(/[^\d]/, '') + const value = inputValue.split('.')[0].replace(/[^\d]/g, '') setPrice(value) }, [setPrice] @@ -95,83 +104,8 @@ const AmendSUDTSend = () => { return scriptToAddress(transaction?.outputs[0].lock, { isMainnet }) }, [transaction?.outputs]) - const getLastOutputAddress = (outputs: State.DetailedOutput[]) => { - const change = outputs.find(output => { - const address = scriptToAddress(output.lock, { isMainnet }) - - return !!addresses.find(item => item.address === address && item.type === 1) - }) - if (change) { - return scriptToAddress(change.lock, { isMainnet }) - } - - const receive = outputs.find(output => { - const address = scriptToAddress(output.lock, { isMainnet }) - return !!addresses.find(item => item.address === address && item.type === 0) - }) - if (receive) { - return scriptToAddress(receive.lock, { isMainnet }) - } - - const sudt = outputs.find(output => { - const address = scriptToAddress(output.lock, { isMainnet }) - return !!sUDTAccounts.find(item => item.address === address) - }) - if (sudt) { - return scriptToAddress(sudt.lock, { isMainnet }) - } - return '' - } - - const items: { - address: string - amount: string - capacity: string - isLastOutput: boolean - output: State.DetailedOutput - }[] = useMemo(() => { - if (transaction && transaction.outputs.length) { - const lastOutputAddress = getLastOutputAddress(transaction.outputs) - return transaction.outputs.map(output => { - const address = scriptToAddress(output.lock, { isMainnet }) - return { - capacity: output.capacity, - address, - output, - amount: shannonToCKBFormatter(output.capacity || '0'), - isLastOutput: address === lastOutputAddress, - } - }) - } - return [] - }, [transaction?.outputs]) - - const outputsCapacity = useMemo(() => { - const outputList = items.filter(item => !item.isLastOutput) - return outputList.reduce((total, cur) => { - if (Number.isNaN(+(cur.capacity || ''))) { - return total - } - return total + BigInt(cur.capacity || '0') - }, BigInt(0)) - }, [items]) - - const lastOutputsCapacity = useMemo(() => { - if (transaction) { - const inputsCapacity = transaction.inputs.reduce((total, cur) => { - if (Number.isNaN(+(cur.capacity || ''))) { - return total - } - return total + BigInt(cur.capacity || '0') - }, BigInt(0)) - - return inputsCapacity - outputsCapacity - fee - } - return -1 - }, [transaction, fee, outputsCapacity]) - useEffect(() => { - if (transaction) { + if (transaction && lastOutputsCapacity !== undefined) { const outputs = items.map(item => { const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity return { @@ -192,7 +126,8 @@ const AmendSUDTSend = () => { } }, [lastOutputsCapacity, transaction, items, dispatch, experimental?.params?.description, description]) - const disabled = sending || !experimental?.tx || priceError || lastOutputsCapacity < 0 + const disabled = + sending || !experimental?.tx || priceError || lastOutputsCapacity === undefined || lastOutputsCapacity < 0 return ( { { dispatch({ @@ -14,7 +15,7 @@ const clear = (dispatch: StateDispatch) => { const useUpdateTransactionPrice = (dispatch: StateDispatch) => useCallback( (value: string) => { - const price = value.split('.')[0].replace(/[^\d]/, '') + const price = value.split('.')[0].replace(/[^\d]/g, '') dispatch({ type: AppActions.UpdateSendPrice, payload: price, @@ -51,13 +52,13 @@ export const useInitialize = ({ const [transaction, setTransaction] = useState(null) const [size, setSize] = useState(0) const [minPrice, setMinPrice] = useState('0') - const [showConfirmedAlert, setShowConfirmedAlert] = useState(false) + const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false) const updateTransactionPrice = useUpdateTransactionPrice(dispatch) const onDescriptionChange = useSendDescriptionChange(dispatch) const fee = useMemo(() => { - const ratio = BigInt(1000) + const ratio = BigInt(FEE_RATIO) const base = BigInt(size) * BigInt(price) const curFee = base / ratio if (curFee * ratio < base) { @@ -74,7 +75,7 @@ export const useInitialize = ({ transaction: { outputsData }, } = res if (!minFee) { - setShowConfirmedAlert(true) + setIsConfirmedAlertShown(true) } const txRes = await getSentTransaction({ hash, walletID }) @@ -85,18 +86,14 @@ export const useInitialize = ({ outputsData, }) - const sizeRes = await getTransactionSize(tx) - - if (isSuccessResponse(sizeRes) && typeof sizeRes.result === 'number') { - setSize(sizeRes.result) - if (minFee) { - const mPrice = ((BigInt(minFee) * BigInt(1000)) / BigInt(sizeRes.result)).toString() - setMinPrice(mPrice) - updateTransactionPrice(mPrice) - } + setSize(tx.size) + if (minFee) { + const mPrice = ((BigInt(minFee) * BigInt(FEE_RATIO)) / BigInt(tx.size)).toString() + setMinPrice(mPrice) + updateTransactionPrice(mPrice) } } - }, [hash, setShowConfirmedAlert, updateTransactionPrice, setTransaction, setSize, setMinPrice]) + }, [hash, setIsConfirmedAlertShown, updateTransactionPrice, setTransaction, setSize, setMinPrice]) useEffect(() => { fetchInitData() @@ -119,7 +116,7 @@ export const useInitialize = ({ // @ts-expect-error Replace-By-Fee (RBF) const { min_replace_fee: minFee } = await getOnChainTransaction(hash) if (!minFee) { - setShowConfirmedAlert(true) + setIsConfirmedAlertShown(true) return } dispatch({ @@ -134,7 +131,7 @@ export const useInitialize = ({ // ignore } }, - [dispatch, walletID, hash, setShowConfirmedAlert] + [dispatch, walletID, hash, setIsConfirmedAlertShown] ) return { @@ -144,7 +141,7 @@ export const useInitialize = ({ transaction, setTransaction, minPrice, - showConfirmedAlert, + isConfirmedAlertShown, onSubmit, } } diff --git a/packages/neuron-ui/src/components/AmendSend/index.tsx b/packages/neuron-ui/src/components/AmendSend/index.tsx index 7f83c0d9cf..92eb75675d 100644 --- a/packages/neuron-ui/src/components/AmendSend/index.tsx +++ b/packages/neuron-ui/src/components/AmendSend/index.tsx @@ -41,7 +41,7 @@ const AmendSend = () => { const isMainnet = isMainnetUtil(networks, networkID) - const { fee, updateTransactionPrice, onDescriptionChange, transaction, onSubmit, minPrice, showConfirmedAlert } = + const { fee, updateTransactionPrice, onDescriptionChange, transaction, onSubmit, minPrice, isConfirmedAlertShown } = useInitialize({ hash, walletID, @@ -120,11 +120,8 @@ const AmendSend = () => { }, [transaction?.outputs]) const outputsCapacity = useMemo(() => { - const outputList = items.filter(item => !item.isLastOutput) + const outputList = items.length === 1 ? items : items.filter(item => !item.isLastOutput) return outputList.reduce((total, cur) => { - if (Number.isNaN(+(cur.capacity || ''))) { - return total - } return total + BigInt(cur.capacity || '0') }, BigInt(0)) }, [items]) @@ -134,19 +131,16 @@ const AmendSend = () => { const lastOutputsCapacity = useMemo(() => { if (transaction) { const inputsCapacity = transaction.inputs.reduce((total, cur) => { - if (Number.isNaN(+(cur.capacity || ''))) { - return total - } return total + BigInt(cur.capacity || '0') }, BigInt(0)) return inputsCapacity - outputsCapacity - fee } - return -1 + return undefined }, [transaction, fee, outputsCapacity]) useEffect(() => { - if (transaction) { + if (transaction && lastOutputsCapacity !== undefined) { const outputs = items.map(item => { const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity return { @@ -164,7 +158,8 @@ const AmendSend = () => { } }, [lastOutputsCapacity, transaction, items, dispatch]) - const disabled = sending || !send.generatedTx || priceError || lastOutputsCapacity < MIN_AMOUNT + const disabled = + sending || !send.generatedTx || priceError || lastOutputsCapacity === undefined || lastOutputsCapacity < MIN_AMOUNT return ( { { +const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber, isWatchOnly }: RowExtendProps) => { const dispatch = useDispatch() const navigate = useNavigate() const [t] = useTranslation() const [amendabled, setAmendabled] = useState(false) + const [amendPendingTx, setAmendPendingTx] = useState() const { onChangeEditStatus, onSubmitDescription } = useLocalDescription('transaction', id, dispatch) @@ -44,11 +47,16 @@ const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber }: RowExten break } case 'amend': { - if (column?.sudtInfo) { - navigate(`${RoutePath.History}/amendSUDTSend/${btn.dataset.hash}`) - return + if (column.type === 'send' && !column.nftInfo && !column.nervosDao) { + if (column?.sudtInfo) { + navigate(`${RoutePath.History}/amendSUDTSend/${btn.dataset.hash}`) + } else { + navigate(`${RoutePath.History}/amend/${btn.dataset.hash}`) + } + } else { + setAmendPendingTx(column) } - navigate(`${RoutePath.History}/amend/${btn.dataset.hash}`) + break } default: { @@ -75,93 +83,98 @@ const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber }: RowExten }, [hash, dispatch]) useEffect(() => { - if (status !== 'success') { - if (column.type === 'send' && !column.nftInfo && !column.nervosDao) { - getOnChainTransaction(hash).then(tx => { - // @ts-expect-error Replace-By-Fee (RBF) - const { min_replace_fee: minReplaceFee } = tx - if (minReplaceFee) { - setAmendabled(true) - } - }) - } - } setAmendabled(false) + if (status !== 'success' && column.type !== 'receive' && !isWatchOnly) { + getOnChainTransaction(hash).then(tx => { + // @ts-expect-error Replace-By-Fee (RBF) + const { min_replace_fee: minReplaceFee } = tx + if (minReplaceFee) { + setAmendabled(true) + } + }) + } }, [status, hash, setAmendabled]) + const onCloseAmendDialog = useCallback(() => { + setAmendPendingTx(undefined) + }, [setAmendPendingTx]) + return ( - - -
-
-
-
{t('history.confirmationTimes')}
-
{confirmationsLabel}
+ <> + + +
+
+
+
{t('history.confirmationTimes')}
+
{confirmationsLabel}
+
+
+
{t('history.description')}
+ + } + showTriangle + isTriggerNextToChild + > +
{description || t('addresses.default-description')}
+
+
-
{t('history.description')}
- - } - showTriangle - isTriggerNextToChild - > -
{description || t('addresses.default-description')}
-
-
-
-
-
{t('history.transaction-hash')}
-
- {hash} - -
-
-
-
- - +
{t('history.transaction-hash')}
+
+ {hash} + +
+
+
+ + +
- {amendabled ? ( - - ) : null} + {amendabled ? ( + + ) : null} +
-
- - + + + {amendPendingTx ? : null} + ) } export default RowExtend diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 73a9bdbb55..6e1f19c2d3 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -30,7 +30,7 @@ import styles from './history.module.scss' const History = () => { const { app: { pageNotice }, - wallet: { id, name: walletName }, + wallet: { id, name: walletName, isWatchOnly }, chain: { networkID, syncState: { cacheTipBlockNumber, bestKnownBlockNumber }, @@ -228,6 +228,7 @@ const History = () => { isMainnet={isMainnet} id={id} bestBlockNumber={bestBlockNumber} + isWatchOnly={isWatchOnly} /> )} expandedRow={expandedRow} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 037af66417..cb311cc300 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -255,7 +255,8 @@ "allow-use-sent-cell": "Unconfirmed Outputs are allowed in this transaction.", "submit-transaction": "Submit the transaction", "transaction-confirmed": "Transaction Confirmed", - "transaction-cannot-amend": "The current transaction is confirmed and cannot be amended." + "transaction-cannot-amend": "The current transaction is confirmed and cannot be amended.", + "amend-pending-transaction": "Amend Pending Transaction" }, "receive": { "title": "Receive", diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index 0a62c02f52..bb7093c4e9 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -247,7 +247,8 @@ "allow-use-sent-cell": "Se permite el uso de salidas no confirmadas en esta transacción.", "submit-transaction": "Enviar la transacción", "transaction-confirmed": "Transacción confirmada", - "transaction-cannot-amend": "La transacción actual está confirmada y no puede modificarse." + "transaction-cannot-amend": "La transacción actual está confirmada y no puede modificarse.", + "amend-pending-transaction": "Modificar transacción pendiente" }, "receive": { "title": "Recibir", diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index c355aabd83..534e1fd1d2 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -254,7 +254,8 @@ "allow-use-sent-cell": "Les sorties non confirmées sont autorisées dans cette transaction.", "submit-transaction": "Soumettre la transaction", "transaction-confirmed": "Transaction confirmée", - "transaction-cannot-amend": "La transaction en cours est confirmée et ne peut être modifiée." + "transaction-cannot-amend": "La transaction en cours est confirmée et ne peut être modifiée.", + "amend-pending-transaction": "Modifier une transaction en cours" }, "receive": { "title": "Recevoir", diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index e1de094b06..4350e2981a 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -249,7 +249,8 @@ "allow-use-sent-cell": "允許使用待確認的 Outputs 構建交易", "submit-transaction": "發起交易", "transaction-confirmed": "交易已確認", - "transaction-cannot-amend": "目前交易已確認,無法修改。" + "transaction-cannot-amend": "目前交易已確認,無法修改。", + "amend-pending-transaction": "修改待處理交易" }, "receive": { "title": "收款", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 3355e80542..86b3a03864 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -248,7 +248,8 @@ "allow-use-sent-cell": "允许使用待确认的 Outputs 构建交易", "submit-transaction": "发起交易", "transaction-confirmed": "交易已确认", - "transaction-cannot-amend": "当前交易已确认,无法修改。" + "transaction-cannot-amend": "当前交易已确认,无法修改。", + "amend-pending-transaction": "修改待处理交易" }, "receive": { "title": "收款", diff --git a/packages/neuron-ui/src/services/remote/offline.ts b/packages/neuron-ui/src/services/remote/offline.ts index 6824bd9c0a..0d2a4d917f 100644 --- a/packages/neuron-ui/src/services/remote/offline.ts +++ b/packages/neuron-ui/src/services/remote/offline.ts @@ -45,7 +45,6 @@ export const exportTransactionAsJSON = remoteApi('export- export const signTransactionOnly = remoteApi('sign-transaction-only') export const broadcastTransaction = remoteApi('broadcast-transaction-only') export const broadcastSignedTransaction = remoteApi('broadcast-signed-transaction') -export const getTransactionSize = remoteApi('get-transaction-size') export const signAndExportTransaction = remoteApi( 'sign-and-export-transaction' ) diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index d36a400417..8640a2c384 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -142,7 +142,6 @@ type Action = | 'sign-transaction-only' | 'broadcast-transaction-only' | 'broadcast-signed-transaction' - | 'get-transaction-size' | 'sign-and-export-transaction' | 'sign-and-broadcast-transaction' // nft diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 4bf65bf429..76ce82827b 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -41,6 +41,7 @@ declare namespace State { outPoint: CKBComponents.OutPoint type?: CKBComponents.Script data?: string + daoData?: string isChangeCell?: boolean } interface DetailedTransaction extends Transaction { diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index c308f2b88b..3f3d1a1940 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -70,6 +70,7 @@ declare namespace Controller { password?: string description?: string amendHash?: string + skipLastInputs?: boolean multisigConfig?: { id: number walletId: string diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 7442b63012..2dda7ff4d3 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -83,3 +83,7 @@ export const ADDRESS_MIN_LENGTH = 86 export const ADDRESS_HEAD_TAIL_LENGTH = 34 export const PlaceHolderArgs = `0x${'00'.repeat(21)}` + +export const DAO_DATA = '0x0000000000000000' + +export const FEE_RATIO = 1000 diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 0d9c093f8a..fa209b0887 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -449,6 +449,7 @@ export default class ApiController { description?: string multisigConfig?: MultisigConfigModel amendHash?: string + skipLastInputs?: boolean } ) => { return this.#walletsController.sendTx({ @@ -868,10 +869,6 @@ export default class ApiController { return this.#offlineSignController.broadcastTransaction({ ...params, walletID: '' }) }) - handle('get-transaction-size', async (_, params) => { - return this.#transactionsController.getTransactionSize(params) - }) - handle('sign-and-export-transaction', async (_, params) => { return this.#offlineSignController.signAndExportTransaction({ ...params, diff --git a/packages/neuron-wallet/src/controllers/transactions.ts b/packages/neuron-wallet/src/controllers/transactions.ts index 193d7c7565..eb3a4fabfc 100644 --- a/packages/neuron-wallet/src/controllers/transactions.ts +++ b/packages/neuron-wallet/src/controllers/transactions.ts @@ -10,7 +10,6 @@ import Transaction from '../models/chain/transaction' import { set as setDescription, get as getDescription } from '../services/tx/transaction-description' import ShowGlobalDialogSubject from '../models/subjects/show-global-dialog' -import TransactionSize from '../models/transaction-size' export default class TransactionsController { public async getAll( @@ -147,12 +146,4 @@ export default class TransactionsController { throw err } } - - public async getTransactionSize(tx: Transaction) { - const size = TransactionSize.tx(Transaction.fromObject(tx)) - return { - status: ResponseCode.Success, - result: size, - } - } } diff --git a/packages/neuron-wallet/src/controllers/wallets.ts b/packages/neuron-wallet/src/controllers/wallets.ts index 29acc6fea2..29bff5030e 100644 --- a/packages/neuron-wallet/src/controllers/wallets.ts +++ b/packages/neuron-wallet/src/controllers/wallets.ts @@ -417,6 +417,7 @@ export default class WalletsController { description?: string multisigConfig?: MultisigConfigModel amendHash?: string + skipLastInputs?: boolean }, skipSign = false ) { @@ -438,7 +439,7 @@ export default class WalletsController { params.walletID, Transaction.fromObject(params.tx), params.password, - false, + params.skipLastInputs || false, skipSign, params.amendHash ) diff --git a/packages/neuron-wallet/src/services/tx/transaction-generator.ts b/packages/neuron-wallet/src/services/tx/transaction-generator.ts index 8ebfbc9df5..3164d353cc 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-generator.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-generator.ts @@ -378,6 +378,7 @@ export class TransactionGenerator { tx.outputs[outputs.length - 1].setCapacity((totalCapacity - capacitiesExceptLast - finalFee).toString()) tx.fee = finalFee.toString() tx.size = txSize + tx.hash = tx.computeHash() // check if (