From 94302cb652ff821d3dab75ae89f180d3b3e201ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Thu, 21 Dec 2023 09:06:31 +0000 Subject: [PATCH] feat: Support use sent cell when send CKB (#2963) --- .../neuron-ui/src/components/Send/hooks.ts | 116 +++++++-- .../neuron-ui/src/components/Send/index.tsx | 46 ++-- .../src/components/Send/send.module.scss | 34 ++- packages/neuron-ui/src/locales/en.json | 4 +- packages/neuron-ui/src/locales/zh-tw.json | 4 +- packages/neuron-ui/src/locales/zh.json | 4 +- packages/neuron-ui/src/states/init/chain.ts | 1 + packages/neuron-ui/src/types/App/index.d.ts | 1 + .../neuron-ui/src/types/Controller/index.d.ts | 1 + packages/neuron-wallet/src/controllers/api.ts | 2 + .../neuron-wallet/src/controllers/wallets.ts | 18 +- .../src/models/chain/transaction.ts | 1 + packages/neuron-wallet/src/services/cells.ts | 27 +- .../src/services/transaction-sender.ts | 85 ++++--- .../src/services/tx/transaction-generator.ts | 87 ++++--- .../tests/services/cells.test.ts | 1 + .../services/tx/transaction-generator.test.ts | 235 +++++++++--------- .../services/tx/transaction-sender.test.ts | 60 +++-- 18 files changed, 454 insertions(+), 273 deletions(-) diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index e5411df0b9..86fff5f3d4 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -50,6 +50,7 @@ const updateTransactionWith = setErrorMessage, updateTransactionOutput, isMainnet, + enableUseSentCell, dispatch, t, consumeOutPoints, @@ -61,6 +62,7 @@ const updateTransactionWith = setErrorMessage: (val: string) => void updateTransactionOutput?: ReturnType isMainnet: boolean + enableUseSentCell: boolean dispatch: StateDispatch t: TFunction consumeOutPoints?: CKBComponents.OutPoint[] @@ -87,6 +89,7 @@ const updateTransactionWith = })), feeRate: price, consumeOutPoints, + enableUseSentCell, } return generator(realParams) .then((res: any) => { @@ -155,18 +158,31 @@ const useRemoveTransactionOutput = (dispatch: StateDispatch) => [dispatch] ) -const useOnTransactionChange = ( - walletID: string, - items: State.Output[], - price: string, - isMainnet: boolean, - dispatch: StateDispatch, - isSendMax: boolean, - setTotalAmount: (val: string) => void, - setErrorMessage: (val: string) => void, - t: TFunction, +const useOnTransactionChange = ({ + walletID, + items, + price, + isMainnet, + dispatch, + isSendMax, + setTotalAmount, + setErrorMessage, + t, + consumeOutPoints, + enableUseSentCell, +}: { + walletID: string + items: State.Output[] + price: string + isMainnet: boolean + dispatch: StateDispatch + isSendMax: boolean + setTotalAmount: (val: string) => void + setErrorMessage: (val: string) => void + t: TFunction consumeOutPoints?: CKBComponents.OutPoint[] -) => { + enableUseSentCell: boolean +}) => { useEffect(() => { clearTimeout(generateTxTimer) setErrorMessage('') @@ -189,9 +205,22 @@ const useOnTransactionChange = ( dispatch, t, consumeOutPoints, + enableUseSentCell, }) }, 300) - }, [walletID, items, price, isSendMax, dispatch, setTotalAmount, setErrorMessage, t, isMainnet, consumeOutPoints]) + }, [ + walletID, + items, + price, + isSendMax, + dispatch, + setTotalAmount, + setErrorMessage, + t, + isMainnet, + consumeOutPoints, + enableUseSentCell, + ]) } const useOnSubmit = (items: Readonly, isMainnet: boolean, dispatch: StateDispatch) => @@ -286,11 +315,13 @@ export const useGetBatchGeneratedTx = async ({ priceArray = [], items, isSendMax, + enableUseSentCell, }: { walletID: string priceArray?: string[] items: Readonly isSendMax: boolean + enableUseSentCell?: boolean }) => { const getUpdateGeneratedTx = (params: Controller.GenerateTransactionParams) => (isSendMax ? generateSendingAllTx : generateTx)(params).then((res: ControllerResponse) => { @@ -309,7 +340,9 @@ export const useGetBatchGeneratedTx = async ({ })), } - const requestArray = priceArray.map(itemPrice => getUpdateGeneratedTx({ ...realParams, feeRate: itemPrice })) + const requestArray = priceArray.map(itemPrice => + getUpdateGeneratedTx({ ...realParams, feeRate: itemPrice, enableUseSentCell }) + ) const allPromiseResult = await Promise.allSettled(requestArray) const feeRateValueArray: FeeRateValueArrayItemType[] = allPromiseResult?.map( @@ -322,17 +355,29 @@ export const useGetBatchGeneratedTx = async ({ return feeRateValueArray } -export const useInitialize = ( - walletID: string, - items: Readonly, - generatedTx: any | null, - price: string, - sending: boolean, - isMainnet: boolean, - dispatch: React.Dispatch, - t: TFunction, +export const useInitialize = ({ + walletID, + items, + generatedTx, + price, + sending, + isMainnet, + consumeOutPoints, + enableUseSentCell, + dispatch, + t, +}: { + walletID: string + items: Readonly + generatedTx: any | null + price: string + sending: boolean + isMainnet: boolean consumeOutPoints?: CKBComponents.OutPoint[] -) => { + enableUseSentCell: boolean + dispatch: React.Dispatch + t: TFunction +}) => { const fee = useMemo(() => calculateFee(generatedTx), [generatedTx]) const [totalAmount, setTotalAmount] = useState('0') @@ -382,6 +427,7 @@ export const useInitialize = ( setErrorMessage, updateTransactionOutput, isMainnet, + enableUseSentCell, dispatch, t, consumeOutPoints, @@ -390,7 +436,18 @@ export const useInitialize = ( updateIsSendMax(false) } }) - }, [walletID, updateTransactionOutput, price, items, dispatch, t, isMainnet, updateIsSendMax, consumeOutPoints]) + }, [ + walletID, + updateTransactionOutput, + price, + items, + dispatch, + t, + isMainnet, + updateIsSendMax, + consumeOutPoints, + enableUseSentCell, + ]) const onSendMaxClick = useCallback(() => { if (!isSendMax) { @@ -436,6 +493,17 @@ export const useInitialize = ( } } +export const useSendWithSentCell = () => { + const [enableUseSentCell, setEnableUseSentCell] = useState(false) + const onChangeEnableUseSentCell = useCallback(() => { + setEnableUseSentCell(v => !v) + }, []) + return { + enableUseSentCell, + onChangeEnableUseSentCell, + } +} + export default { useInitialize, } diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index 05b2cb61ca..c6c7a5563a 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -22,7 +22,7 @@ import { HIDE_BALANCE } from 'utils/const' import { isErrorWithI18n } from 'exceptions' import { useSearchParams } from 'react-router-dom' -import { useInitialize } from './hooks' +import { useInitialize, useSendWithSentCell } from './hooks' import styles from './send.module.scss' const SendHeader = ({ balance, useConsumeCell }: { balance: string; useConsumeCell: boolean }) => { @@ -69,6 +69,7 @@ const Send = () => { const isMainnet = isMainnetUtil(networks, networkID) const consumeOutPoints = useMemo(() => consumeCells?.map(v => v.outPoint), [useMemo]) + const { enableUseSentCell, onChangeEnableUseSentCell } = useSendWithSentCell() const { outputs, @@ -89,17 +90,18 @@ const Send = () => { isSendMax, onSendMaxClick: handleSendMaxClick, updateIsSendMax, - } = useInitialize( + } = useInitialize({ walletID, - send.outputs, - send.generatedTx, - send.price, + items: send.outputs, + generatedTx: send.generatedTx, + price: send.price, sending, isMainnet, + enableUseSentCell, + consumeOutPoints, dispatch, t, - consumeOutPoints - ) + }) const [searchParams] = useSearchParams() @@ -134,18 +136,19 @@ const Send = () => { [setLocktimeIndex, updateTransactionOutput] ) - useOnTransactionChange( + useOnTransactionChange({ walletID, - outputs, - send.price, + items: outputs, + price: send.price, isMainnet, dispatch, isSendMax, setTotalAmount, setErrorMessage, t, - consumeOutPoints - ) + consumeOutPoints, + enableUseSentCell, + }) let errorMessageUnderTotal = errorMessage try { @@ -265,10 +268,21 @@ const Send = () => { />
- + +
+ +
diff --git a/packages/neuron-ui/src/components/Send/send.module.scss b/packages/neuron-ui/src/components/Send/send.module.scss index 44e52f6d56..32f5f430b5 100644 --- a/packages/neuron-ui/src/components/Send/send.module.scss +++ b/packages/neuron-ui/src/components/Send/send.module.scss @@ -57,6 +57,7 @@ $headerHeight: 104px; position: relative; border-radius: 16px; background: var(--secondary-background-color); + min-height: 436px; .content { padding: 16px; @@ -76,13 +77,34 @@ $headerHeight: 104px; width: 100%; bottom: 16px; left: 0; - display: flex; - justify-content: center; + text-align: center; + .actions { + display: flex; + justify-content: center; - button { - width: 216px; - &:last-child { - margin-left: 12px; + button { + width: 216px; + &:last-child { + margin-left: 12px; + } + } + } + .allowUseSent { + display: inline-block; + margin-bottom: 16px; + color: var(--secondary-text-color); + input[type='checkbox'] { + display: none; + margin-right: 8px; + } + input[type='checkbox'] + span { + display: inline-block; + padding-left: 26px; + background: url('../../widgets/Icons/Radio.svg') no-repeat left top; + user-select: none; + } + input[type='checkbox']:checked + span { + background: url('../../widgets/Icons/RadioSelected.svg') no-repeat left top; } } } diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 08c1ba3add..032be96f9f 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -239,7 +239,9 @@ "remove-receiving-address-msg": "Are you sure you want to remove the selected receiving address?", "locktime-notice-content": "According to the actual running block height, there may be some time variances in locktime.", "release-on": "Release on", - "locktime-warning": "Please ensure that receiver's wallet can support expiration unlocking. (Note: 1.Exchanges generally do not support expiration unlocking {{extraNote}})" + "locktime-warning": "Please ensure that receiver's wallet can support expiration unlocking. (Note: 1.Exchanges generally do not support expiration unlocking {{extraNote}})", + "allow-use-sent-cell": "Unconfirmed Outputs are allowed in this transaction.", + "submit-transaction": "Submit the transaction" }, "receive": { "title": "Receive", diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 66ecd3fc35..675eaf99f3 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -233,7 +233,9 @@ "remove-receiving-address-msg": "確定要刪除所選收款地址?", "locktime-notice-content": "鎖定時間根據區塊鏈實際運行情況會有一定的誤差。", "release-on": "鎖定至", - "locktime-warning": "請確保對方錢包支持到期解鎖功能。(註:1.交易所壹般不支持到期解鎖功能 {{extraNote}})" + "locktime-warning": "請確保對方錢包支持到期解鎖功能。(註:1.交易所壹般不支持到期解鎖功能 {{extraNote}})", + "allow-use-sent-cell": "允許使用待確認的 Outputs 構建交易", + "submit-transaction": "發起交易" }, "receive": { "title": "收款", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 7d24c9dc24..339302dfe7 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -232,7 +232,9 @@ "remove-receiving-address-msg": "确定要删除所选收款地址?", "locktime-notice-content": "锁定时间根据区块链实际运行情况会有一定的误差。", "release-on": "锁定至", - "locktime-warning": "请确保对方钱包支持到期解锁功能。(注:1.交易所一般不支持到期解锁功能 {{extraNote}})" + "locktime-warning": "请确保对方钱包支持到期解锁功能。(注:1.交易所一般不支持到期解锁功能 {{extraNote}})", + "allow-use-sent-cell": "允许使用待确认的 Outputs 构建交易", + "submit-transaction": "发起交易" }, "receive": { "title": "收款", diff --git a/packages/neuron-ui/src/states/init/chain.ts b/packages/neuron-ui/src/states/init/chain.ts index e0da056662..2fc84a4314 100644 --- a/packages/neuron-ui/src/states/init/chain.ts +++ b/packages/neuron-ui/src/states/init/chain.ts @@ -19,6 +19,7 @@ export const transactionState: State.DetailedTransaction = { blockHash: '', witnesses: [], nervosDao: false, + size: 0, } export const chainState: Readonly = { diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 3916fb842d..243fd982de 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -50,6 +50,7 @@ declare namespace State { outputs: DetailedOutput[] outputsCount: string witnesses: string[] + size: number } interface Output { address: string | undefined diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 012dd89cac..33b3aab3b8 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -79,6 +79,7 @@ declare namespace Controller { }[] feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] + enableUseSentCell?: boolean } type GenerateSendingAllTransactionParams = GenerateTransactionParams diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 45f5f4c8f2..239e9b36e1 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -404,6 +404,7 @@ export default class ApiController { fee: string feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] + enableUseSentCell?: boolean } ) => { return this.#walletsController.generateTx(params) @@ -420,6 +421,7 @@ export default class ApiController { fee: string feeRate: string consumeOutPoints: CKBComponents.OutPoint[] + enableUseSentCell?: boolean } ) => { return this.#walletsController.generateSendingAllTx(params) diff --git a/packages/neuron-wallet/src/controllers/wallets.ts b/packages/neuron-wallet/src/controllers/wallets.ts index 1870a1e25c..2865217439 100644 --- a/packages/neuron-wallet/src/controllers/wallets.ts +++ b/packages/neuron-wallet/src/controllers/wallets.ts @@ -458,6 +458,7 @@ export default class WalletsController { fee: string feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] + enableUseSentCell?: boolean }) { if (!params) { throw new IsRequired('Parameters') @@ -465,13 +466,7 @@ export default class WalletsController { const addresses: string[] = params.items.map(i => i.address) this.checkAddresses(addresses) - const tx: Transaction = await new TransactionSender().generateTx( - params.walletID, - params.items, - params.fee, - params.feeRate, - params.consumeOutPoints - ) + const tx: Transaction = await new TransactionSender().generateTx(params) return { status: ResponseCode.Success, result: tx, @@ -484,6 +479,7 @@ export default class WalletsController { fee: string feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] + enableUseSentCell?: boolean }) { if (!params) { throw new IsRequired('Parameters') @@ -491,13 +487,7 @@ export default class WalletsController { const addresses: string[] = params.items.map(i => i.address) this.checkAddresses(addresses) - const tx: Transaction = await new TransactionSender().generateSendingAllTx( - params.walletID, - params.items, - params.fee, - params.feeRate, - params.consumeOutPoints - ) + const tx: Transaction = await new TransactionSender().generateSendingAllTx(params) return { status: ResponseCode.Success, result: tx, diff --git a/packages/neuron-wallet/src/models/chain/transaction.ts b/packages/neuron-wallet/src/models/chain/transaction.ts index 885c3b182a..2bd405c8bb 100644 --- a/packages/neuron-wallet/src/models/chain/transaction.ts +++ b/packages/neuron-wallet/src/models/chain/transaction.ts @@ -60,6 +60,7 @@ export default class Transaction { public value?: string public fee?: string + public size?: number public interest?: string public type?: string diff --git a/packages/neuron-wallet/src/services/cells.ts b/packages/neuron-wallet/src/services/cells.ts index c3dc43dd85..3959c4137a 100644 --- a/packages/neuron-wallet/src/services/cells.ts +++ b/packages/neuron-wallet/src/services/cells.ts @@ -657,12 +657,14 @@ export default class CellsService { hashType: ScriptHashType } = { codeHash: SystemScriptInfo.SECP_CODE_HASH, hashType: ScriptHashType.Type }, multisigConfigs: MultisigConfigModel[] = [], - consumeOutPoints?: CKBComponents.OutPoint[] + consumeOutPoints?: CKBComponents.OutPoint[], + enableUseSentCell?: boolean ): Promise<{ inputs: Input[] capacities: string finalFee: string hasChangeOutput: boolean + totalSize: number }> => { if (!walletId && !lockClass.lockArgs) { throw new TransactionInputParameterMiss() @@ -693,20 +695,20 @@ export default class CellsService { }) : CellsService.getLiveOrSentCellByLockArgsMultisigOutput(lockClass)) - const liveCells = cellEntities.filter(c => c.status === OutputStatus.Live) + const useCells = enableUseSentCell ? cellEntities : cellEntities.filter(c => c.status === OutputStatus.Live) const sentBalance: bigint = cellEntities .filter(c => c.status === OutputStatus.Sent) .map(c => BigInt(c.capacity)) .reduce((result, c) => result + c, BigInt(0)) if ( - liveCells.length === 0 && + useCells.length === 0 && sentBalance === BigInt(0) && ((mode.isFeeRateMode() && feeRateInt !== BigInt(0)) || (mode.isFeeMode() && feeInt !== BigInt(0))) ) { throw new CapacityNotEnough() } - liveCells.sort((a, b) => { + useCells.sort((a, b) => { const result = BigInt(a.capacity) - BigInt(b.capacity) if (result > BigInt(0)) { return 1 @@ -744,7 +746,7 @@ export default class CellsService { }), {} ) - liveCells.every(cell => { + useCells.every(cell => { const input: Input = new Input(cell.outPoint(), '0', cell.capacity, cell.lockScript(), cell.lockHash) if (inputs.find(el => el.lockHash === cell.lockHash!)) { totalSize += TransactionSize.emptyWitness() @@ -771,6 +773,7 @@ export default class CellsService { return false } else if (diff - changeOutputFee >= minChangeCapacity) { needFee += changeOutputFee + totalSize += changeOutputSize + changeOutputDataSize hasChangeOutput = true return false } @@ -813,6 +816,7 @@ export default class CellsService { capacities: inputCapacities.toString(), finalFee: finalFee.toString(), hasChangeOutput, + totalSize, } } @@ -823,7 +827,8 @@ export default class CellsService { hashType: ScriptHashType args?: string } = { codeHash: SystemScriptInfo.SECP_CODE_HASH, hashType: ScriptHashType.Type }, - consumeOutPoints?: CKBComponents.OutPoint[] + consumeOutPoints?: CKBComponents.OutPoint[], + enableUseSentCell?: boolean ): Promise => { let cellEntities: (OutputEntity | MultisigOutput)[] = [] if (consumeOutPoints?.length) { @@ -846,13 +851,11 @@ export default class CellsService { }) } - const inputs: Input[] = cellEntities - .filter(v => v.status === OutputStatus.Live) - .map(cell => { - return new Input(cell.outPoint(), '0', cell.capacity, cell.lockScript(), cell.lockHash) - }) + const useCells = enableUseSentCell ? cellEntities : cellEntities.filter(v => v.status === OutputStatus.Live) - return inputs + return useCells.map(cell => { + return new Input(cell.outPoint(), '0', cell.capacity, cell.lockScript(), cell.lockHash) + }) } public static async gatherAnyoneCanPayCKBInputs( diff --git a/packages/neuron-wallet/src/services/transaction-sender.ts b/packages/neuron-wallet/src/services/transaction-sender.ts index 208739a2d0..9820c9c0e7 100644 --- a/packages/neuron-wallet/src/services/transaction-sender.ts +++ b/packages/neuron-wallet/src/services/transaction-sender.ts @@ -428,13 +428,21 @@ export default class TransactionSender { return [emptyWitness, ...restWitnesses] } - public generateTx = async ( - walletID: string = '', - items: TargetOutput[] = [], - fee: string = '0', - feeRate: string = '0', + public generateTx = async ({ + walletID = '', + items = [], + fee = '0', + feeRate = '0', + consumeOutPoints, + enableUseSentCell, + }: { + walletID: string + items: TargetOutput[] + fee: string + feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] - ): Promise => { + enableUseSentCell?: boolean + }): Promise => { const targetOutputs = items.map(item => ({ ...item, capacity: BigInt(item.capacity).toString(), @@ -443,16 +451,15 @@ export default class TransactionSender { const changeAddress: string = await this.getChangeAddress() try { - const tx: Transaction = await TransactionGenerator.generateTx( + const tx: Transaction = await TransactionGenerator.generateTx({ walletID, targetOutputs, changeAddress, fee, feeRate, - undefined, - undefined, - consumeOutPoints - ) + consumeOutPoints, + enableUseSentCell, + }) return tx } catch (error) { @@ -463,26 +470,34 @@ export default class TransactionSender { } } - public generateSendingAllTx = async ( - walletID: string = '', - items: TargetOutput[] = [], - fee: string = '0', - feeRate: string = '0', + public generateSendingAllTx = async ({ + walletID = '', + items = [], + fee = '0', + feeRate = '0', + consumeOutPoints, + enableUseSentCell, + }: { + walletID: string + items: TargetOutput[] + fee: string + feeRate: string consumeOutPoints?: CKBComponents.OutPoint[] - ): Promise => { + enableUseSentCell?: boolean + }): Promise => { const targetOutputs = items.map(item => ({ ...item, capacity: BigInt(item.capacity).toString(), })) - const tx: Transaction = await TransactionGenerator.generateSendingAllTx( + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ walletID, targetOutputs, fee, feeRate, - undefined, - consumeOutPoints - ) + consumeOutPoints, + enableUseSentCell, + }) return tx } @@ -496,13 +511,13 @@ export default class TransactionSender { capacity: BigInt(item.capacity).toString(), })) - const tx: Transaction = await TransactionGenerator.generateSendingAllTx( - '', + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: '', targetOutputs, - '0', - '1000', - multisigConfig - ) + fee: '0', + feeRate: '1000', + multisigConfig, + }) return tx } @@ -524,19 +539,19 @@ export default class TransactionSender { multisigConfig.n ) const multisigAddresses = scriptToAddress(lockScript, NetworksService.getInstance().isMainnet()) - const tx: Transaction = await TransactionGenerator.generateTx( - '', + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: '', targetOutputs, - multisigAddresses, - '0', - '1000', - { + changeAddress: multisigAddresses, + fee: '0', + feeRate: '1000', + lockClass: { lockArgs: [lockScript.args], codeHash: SystemScriptInfo.MULTI_SIGN_CODE_HASH, hashType: SystemScriptInfo.MULTI_SIGN_HASH_TYPE, }, - multisigConfig - ) + multisigConfig, + }) return tx } catch (error) { if (error instanceof CapacityNotEnoughForChange) { diff --git a/packages/neuron-wallet/src/services/tx/transaction-generator.ts b/packages/neuron-wallet/src/services/tx/transaction-generator.ts index 657a2fe89e..d0a833f13b 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-generator.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-generator.ts @@ -161,20 +161,34 @@ export class TransactionGenerator { return tx } - public static generateTx = async ( - walletId: string, - targetOutputs: TargetOutput[], - changeAddress: string, - fee: string = '0', - feeRate: string = '0', - lockClass: { + public static generateTx = async ({ + walletID, + targetOutputs, + changeAddress, + fee = '0', + feeRate = '0', + lockClass = { + codeHash: SystemScriptInfo.SECP_CODE_HASH, + hashType: ScriptHashType.Type, + }, + multisigConfig, + consumeOutPoints, + enableUseSentCell, + }: { + walletID: string + targetOutputs: TargetOutput[] + changeAddress: string + fee: string + feeRate?: string + lockClass?: { lockArgs?: string[] codeHash: string hashType: ScriptHashType - } = { codeHash: SystemScriptInfo.SECP_CODE_HASH, hashType: ScriptHashType.Type }, - multisigConfig?: MultisigConfigModel, + } + multisigConfig?: MultisigConfigModel consumeOutPoints?: CKBComponents.OutPoint[] - ): Promise => { + enableUseSentCell?: boolean + }): Promise => { let cellDep: CellDep if (lockClass.codeHash === SystemScriptInfo.MULTI_SIGN_CODE_HASH) { cellDep = await SystemScriptInfo.getInstance().getMultiSignCellDep() @@ -222,9 +236,9 @@ export class TransactionGenerator { }) const baseSize: number = TransactionSize.tx(tx) - const { inputs, capacities, finalFee, hasChangeOutput } = await CellsService.gatherInputs( + const { inputs, capacities, finalFee, hasChangeOutput, totalSize } = await CellsService.gatherInputs( needCapacities.toString(), - walletId, + walletID, fee, feeRate, baseSize, @@ -233,11 +247,13 @@ export class TransactionGenerator { undefined, lockClass, multisigConfig ? [multisigConfig] : [], - consumeOutPoints + consumeOutPoints, + enableUseSentCell ) const finalFeeInt = BigInt(finalFee) tx.inputs = inputs tx.fee = finalFee + tx.size = totalSize // change if (hasChangeOutput) { @@ -255,14 +271,23 @@ export class TransactionGenerator { } // rest of capacity all send to last target output. - public static generateSendingAllTx = async ( - walletId: string, - targetOutputs: TargetOutput[], - fee: string = '0', - feeRate: string = '0', - multisigConfig?: MultisigConfigModel, + public static generateSendingAllTx = async ({ + walletID, + targetOutputs, + fee = '0', + feeRate = '0', + multisigConfig, + consumeOutPoints, + enableUseSentCell, + }: { + walletID: string + targetOutputs: TargetOutput[] + fee: string + feeRate?: string + multisigConfig?: MultisigConfigModel consumeOutPoints?: CKBComponents.OutPoint[] - ): Promise => { + enableUseSentCell?: boolean + }): Promise => { let cellDep: CellDep if (multisigConfig) { cellDep = await SystemScriptInfo.getInstance().getMultiSignCellDep() @@ -278,13 +303,14 @@ export class TransactionGenerator { const mode = new FeeMode(feeRateInt) const allInputs: Input[] = await CellsService.gatherAllInputs( - walletId, + walletID, multisigConfig ? Script.fromSDK( Multisig.getMultisigScript(multisigConfig.blake160s, multisigConfig.r, multisigConfig.m, multisigConfig.n) ) : undefined, - consumeOutPoints + consumeOutPoints, + enableUseSentCell ) if (allInputs.length === 0) { @@ -331,15 +357,15 @@ export class TransactionGenerator { // change let finalFee: bigint = feeInt + const lockHashes = new Set(allInputs.map(i => i.lockHash!)) + const keyCount: number = lockHashes.size + const txSize: number = + TransactionSize.tx(tx) + + (multisigConfig + ? TransactionSize.multiSignWitness(multisigConfig.r, multisigConfig.m, multisigConfig.n) + : TransactionSize.secpLockWitness() * keyCount) + + TransactionSize.emptyWitness() * (allInputs.length - keyCount) if (mode.isFeeRateMode()) { - const lockHashes = new Set(allInputs.map(i => i.lockHash!)) - const keyCount: number = lockHashes.size - const txSize: number = - TransactionSize.tx(tx) + - (multisigConfig - ? TransactionSize.multiSignWitness(multisigConfig.r, multisigConfig.m, multisigConfig.n) - : TransactionSize.secpLockWitness() * keyCount) + - TransactionSize.emptyWitness() * (allInputs.length - keyCount) finalFee = TransactionFee.fee(txSize, feeRateInt) } @@ -349,6 +375,7 @@ export class TransactionGenerator { .reduce((result, c) => result + c, BigInt(0)) tx.outputs[outputs.length - 1].setCapacity((totalCapacity - capacitiesExceptLast - finalFee).toString()) tx.fee = finalFee.toString() + tx.size = txSize // check if ( diff --git a/packages/neuron-wallet/tests/services/cells.test.ts b/packages/neuron-wallet/tests/services/cells.test.ts index 2706a20c47..e8afb1bf48 100644 --- a/packages/neuron-wallet/tests/services/cells.test.ts +++ b/packages/neuron-wallet/tests/services/cells.test.ts @@ -712,6 +712,7 @@ describe('CellsService', () => { BigInt('1000') ).toString(), hasChangeOutput: true, + totalSize: 161, }) }) }) diff --git a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts index 3839a56a0f..46551c7a02 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts @@ -239,18 +239,18 @@ describe('TransactionGenerator', () => { const feeRate = '1000' it('capacity 500', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('500'), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -269,18 +269,18 @@ describe('TransactionGenerator', () => { it('capacity 1000', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('1000'), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -298,18 +298,18 @@ describe('TransactionGenerator', () => { it('capacity 1000 - fee, no change output', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: BigInt(1000 * 10 ** 8 - 355).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -327,18 +327,18 @@ describe('TransactionGenerator', () => { it('capacity 1000 - fee + 1 shannon', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: (BigInt(1000 * 10 ** 8) - BigInt(464) + BigInt(1)).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -358,9 +358,9 @@ describe('TransactionGenerator', () => { await getConnection().manager.save(aliceCell) const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: BigInt(1000 * 10 ** 8).toString(), @@ -370,10 +370,10 @@ describe('TransactionGenerator', () => { capacity: BigInt(2500 * 10 ** 8).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const expectedSize: number = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() * 2 + TransactionSize.emptyWitness() @@ -384,18 +384,18 @@ describe('TransactionGenerator', () => { describe('with full address', () => { it(`only full address, 43 capacity`, async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: fullAddressInfo.address, capacity: BigInt(43 * 10 ** 8).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const expectedSize: number = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() const expectedFee: bigint = TransactionFee.fee(expectedSize, BigInt(feeRate)) @@ -405,25 +405,25 @@ describe('TransactionGenerator', () => { it('only full address, 42 capacity', async () => { expect( - TransactionGenerator.generateTx( - walletId1, - [ + TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: fullAddressInfo.address, capacity: BigInt(42 * 10 ** 8).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) ).rejects.toThrowError() }) it(`full address and bob's output`, async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: fullAddressInfo.address, capacity: BigInt(1000 * 10 ** 8).toString(), @@ -433,10 +433,10 @@ describe('TransactionGenerator', () => { capacity: BigInt(1000 * 10 ** 8).toString(), }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const expectedSize: number = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() + TransactionSize.emptyWitness() @@ -448,19 +448,19 @@ describe('TransactionGenerator', () => { describe('with date', () => { it('capacity 500', async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('500'), date, }, ], - bob.address, - '0', - feeRate - ) + changeAddress: bob.address, + fee: '0', + feeRate, + }) const expectedSize: number = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() @@ -485,17 +485,17 @@ describe('TransactionGenerator', () => { describe('with fee 1000', () => { const fee = '1000' it('capacity 500', async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('500'), }, ], - bob.address, - fee - ) + changeAddress: bob.address, + fee, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -508,17 +508,17 @@ describe('TransactionGenerator', () => { }) it('capacity 1000', async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('1000'), }, ], - bob.address, - fee - ) + changeAddress: bob.address, + fee, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -531,17 +531,17 @@ describe('TransactionGenerator', () => { }) it('capacity 1000 - fee', async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: (BigInt(1000 * 10 ** 8) - BigInt(fee)).toString(), }, ], - bob.address, - fee - ) + changeAddress: bob.address, + fee, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -554,17 +554,17 @@ describe('TransactionGenerator', () => { }) it('capacity 1000 - fee + 1 shannon', async () => { - const tx: Transaction = await TransactionGenerator.generateTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: (BigInt(1000 * 10 ** 8) - BigInt(fee) + BigInt(1)).toString(), }, ], - bob.address, - fee - ) + changeAddress: bob.address, + fee, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -608,7 +608,11 @@ describe('TransactionGenerator', () => { it('with fee 800', async () => { const fee = '800' const feeInt = BigInt(fee) - const tx: Transaction = await TransactionGenerator.generateSendingAllTx(walletId1, targetOutputs, fee) + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, + targetOutputs, + fee, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -629,7 +633,12 @@ describe('TransactionGenerator', () => { it('with feeRate 1000', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateSendingAllTx(walletId1, targetOutputs, '0', feeRate) + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, + targetOutputs, + fee: '0', + feeRate, + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) @@ -656,9 +665,9 @@ describe('TransactionGenerator', () => { it('full address with feeRate 1000, 43 capacity', async () => { const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateSendingAllTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, + targetOutputs: [ { address: fullAddressInfo.address, capacity: toShannon('43'), @@ -668,9 +677,9 @@ describe('TransactionGenerator', () => { capacity: toShannon('0'), }, ], - '0', - feeRate - ) + fee: '0', + feeRate, + }) const outputCapacities = tx .outputs!.map(output => BigInt(output.capacity)) @@ -689,9 +698,9 @@ describe('TransactionGenerator', () => { const feeRate = '1000' expect( - TransactionGenerator.generateSendingAllTx( - walletId1, - [ + TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, + targetOutputs: [ { address: fullAddressInfo.address, capacity: toShannon('42'), @@ -701,27 +710,27 @@ describe('TransactionGenerator', () => { capacity: toShannon('0'), }, ], - '0', - feeRate - ) + fee: '0', + feeRate, + }) ).rejects.toThrowError() }) describe('feeRate = 1000, with date', () => { const feeRate = '1000' it('capacity 500', async () => { - const tx: Transaction = await TransactionGenerator.generateSendingAllTx( - walletId1, - [ + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, + targetOutputs: [ { address: bob.address, capacity: toShannon('500'), date, }, ], - '0', - feeRate - ) + fee: '0', + feeRate, + }) expect(tx.outputs[0].lock.codeHash).toEqual(SystemScriptInfo.MULTI_SIGN_CODE_HASH) @@ -744,12 +753,12 @@ describe('TransactionGenerator', () => { }), }) const feeRate = '1000' - const tx: Transaction = await TransactionGenerator.generateSendingAllTx( - walletId1, + const tx: Transaction = await TransactionGenerator.generateSendingAllTx({ + walletID: walletId1, targetOutputs, - '0', + fee: '0', feeRate, - MultisigConfigModel.fromObject({ + multisigConfig: MultisigConfigModel.fromObject({ walletId: walletId1, r: 1, m: 2, @@ -759,8 +768,8 @@ describe('TransactionGenerator', () => { 'ckt1qyqdpymnu202x3p4cnrrgek5czcdsg95xznswjr98y', 'ckt1qyqwqcknusdreymrhhme00hg9af3pr5hcmwqzfxvda', ].map(v => addressToScript(v).args), - }) - ) + }), + }) const inputCapacities = tx .inputs!.map(input => BigInt(input.capacity ?? 0)) diff --git a/packages/neuron-wallet/tests/services/tx/transaction-sender.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-sender.test.ts index d52372dc3a..f4ca99d4e1 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-sender.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-sender.test.ts @@ -604,22 +604,26 @@ describe('TransactionSender Test', () => { }) describe('success', () => { beforeEach(async () => { - await transactionSender.generateTx(fakeWallet.id, targetOutputs, fee, feeRate) + await transactionSender.generateTx({ + walletID: fakeWallet.id, + items: targetOutputs, + fee, + feeRate, + }) }) it('generates transaction', () => { - expect(stubbedGenerateTx).toHaveBeenCalledWith( - fakeWallet.id, - [ + expect(stubbedGenerateTx).toHaveBeenCalledWith({ + walletID: fakeWallet.id, + targetOutputs: [ { address: '1', capacity: '1' }, { address: '1', capacity: '1' }, ], - fakeAddress1, + changeAddress: fakeAddress1, fee, feeRate, - undefined, - undefined, - undefined - ) + consumeOutPoints: undefined, + enableUseSentCell: undefined, + }) }) }) describe('fail', () => { @@ -627,9 +631,14 @@ describe('TransactionSender Test', () => { stubbedGenerateTx.mockRejectedValue(new CapacityNotEnoughForChange()) }) it('generates transaction', async () => { - expect(transactionSender.generateTx(fakeWallet.id, targetOutputs, fee, feeRate)).rejects.toThrowError( - CapacityNotEnoughForChangeByTransfer - ) + expect( + transactionSender.generateTx({ + walletID: fakeWallet.id, + items: targetOutputs, + fee, + feeRate, + }) + ).rejects.toThrowError(CapacityNotEnoughForChangeByTransfer) }) }) }) @@ -642,20 +651,25 @@ describe('TransactionSender Test', () => { { address: '1', capacity: '1' }, { address: '1', capacity: '1' }, ] - await transactionSender.generateSendingAllTx(fakeWallet.id, targetOutputs, fee, feeRate) + await transactionSender.generateSendingAllTx({ + walletID: fakeWallet.id, + items: targetOutputs, + fee, + feeRate, + }) }) it('generates transaction', () => { - expect(stubbedGenerateSendingAllTx).toHaveBeenCalledWith( - fakeWallet.id, - [ + expect(stubbedGenerateSendingAllTx).toHaveBeenCalledWith({ + walletID: fakeWallet.id, + targetOutputs: [ { address: '1', capacity: '1' }, { address: '1', capacity: '1' }, ], fee, feeRate, - undefined, - undefined - ) + consumeOutPoints: undefined, + enableUseSentCell: undefined, + }) }) }) @@ -673,7 +687,13 @@ describe('TransactionSender Test', () => { blake160s: ['blake160s'], }) await transactionSender.generateMultisigSendAllTx(targetOutputs, multisigConfig) - expect(stubbedGenerateSendingAllTx).toHaveBeenCalledWith('', targetOutputs, '0', '1000', multisigConfig) + expect(stubbedGenerateSendingAllTx).toHaveBeenCalledWith({ + walletID: '', + targetOutputs, + fee: '0', + feeRate: '1000', + multisigConfig, + }) }) })