diff --git a/packages/neuron-wallet/src/services/tx/transaction-persistor.ts b/packages/neuron-wallet/src/services/tx/transaction-persistor.ts index 70978e17a0..db36988007 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-persistor.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-persistor.ts @@ -44,7 +44,10 @@ export class TransactionPersistor { // After the tx is fetched: // 1. If the tx is not persisted before fetching, output = live, input = dead // 2. If the tx is already persisted before fetching, output = live, input = dead - private static saveWithFetch = async (transaction: Transaction, lockArgsSet?: Set ): Promise => { + private static saveWithFetch = async ( + transaction: Transaction, + lockArgsSetNeedsDetail?: Set + ): Promise => { const connection = getConnection() const txEntity: TransactionEntity | undefined = await connection .getRepository(TransactionEntity) @@ -192,7 +195,7 @@ export class TransactionPersistor { return txEntity } - return TransactionPersistor.create(transaction, OutputStatus.Live, OutputStatus.Dead, lockArgsSet) + return TransactionPersistor.create(transaction, OutputStatus.Live, OutputStatus.Dead, lockArgsSetNeedsDetail) } // only create, check exist before this @@ -200,7 +203,7 @@ export class TransactionPersistor { transaction: Transaction, outputStatus: OutputStatus, inputStatus: OutputStatus, - lockArgsSet?: Set, + lockArgsSetNeedsDetail?: Set ): Promise => { const connection = getConnection() const tx = new TransactionEntity() @@ -300,31 +303,29 @@ export class TransactionPersistor { } return output }) - let currentWalletInputs: InputEntity[] = inputs - let currentWalletOutputs: OutputEntity[] = outputs + let willSaveDetailInputs: InputEntity[] = inputs + let willSaveDetailOutputs: OutputEntity[] = outputs let txLocks: TxLockEntity[] = [] - if (lockArgsSet?.size) { - currentWalletInputs = inputs.filter(v => this.isCurrentWalletCell(v, lockArgsSet)) - currentWalletOutputs = outputs.filter(v => this.isCurrentWalletCell(v, lockArgsSet)) - txLocks = [...new Set( - [ - ...inputs.filter(v => v.lockHash && !this.isCurrentWalletCell(v, lockArgsSet)) - .map(v => v.lockHash!), - ...outputs.filter(v => !this.isCurrentWalletCell(v, lockArgsSet)) - .map(v => v.lockHash), - ] - )].map(v => TxLockEntity.fromObject({ txHash: tx.hash, lockHash: v})) + if (lockArgsSetNeedsDetail?.size) { + willSaveDetailInputs = inputs.filter(v => this.shouldSaveDetail(v, lockArgsSetNeedsDetail)) + willSaveDetailOutputs = outputs.filter(v => this.shouldSaveDetail(v, lockArgsSetNeedsDetail)) + txLocks = [ + ...new Set([ + ...inputs.filter(v => v.lockHash && !this.shouldSaveDetail(v, lockArgsSetNeedsDetail)).map(v => v.lockHash!), + ...outputs.filter(v => !this.shouldSaveDetail(v, lockArgsSetNeedsDetail)).map(v => v.lockHash), + ]), + ].map(v => TxLockEntity.fromObject({ txHash: tx.hash, lockHash: v })) } - + const chunk = 100 const queryRunner = connection.createQueryRunner() await TransactionPersistor.waitUntilTransactionFinished(queryRunner) await queryRunner.startTransaction() try { await queryRunner.manager.save(tx) - await queryRunner.manager.save(currentWalletInputs, { chunk }) + await queryRunner.manager.save(willSaveDetailInputs, { chunk }) await queryRunner.manager.save(previousOutputs, { chunk }) - await queryRunner.manager.save(currentWalletOutputs, { chunk }) + await queryRunner.manager.save(willSaveDetailOutputs, { chunk }) await queryRunner.manager.save(txLocks, { chunk }) await queryRunner.commitTransaction() } catch (err) { @@ -356,7 +357,7 @@ export class TransactionPersistor { public static convertTransactionAndSave = async ( transaction: Transaction, saveType: TxSaveType, - lockArgsSet?: Set, + lockArgsSetNeedsDetail?: Set ): Promise => { const tx: Transaction = transaction @@ -364,18 +365,21 @@ export class TransactionPersistor { if (saveType === TxSaveType.Sent) { txEntity = await TransactionPersistor.saveWithSent(tx) } else if (saveType === TxSaveType.Fetch) { - txEntity = await TransactionPersistor.saveWithFetch(tx, lockArgsSet) + txEntity = await TransactionPersistor.saveWithFetch(tx, lockArgsSetNeedsDetail) } else { throw new Error('Error TxSaveType!') } return txEntity } - public static saveFetchTx = async (transaction: Transaction, lockArgsSet?: Set): Promise => { + public static saveFetchTx = async ( + transaction: Transaction, + lockArgsSetNeedsDetail?: Set + ): Promise => { const txEntity: TransactionEntity = await TransactionPersistor.convertTransactionAndSave( transaction, TxSaveType.Fetch, - lockArgsSet + lockArgsSetNeedsDetail ) return txEntity } @@ -389,14 +393,15 @@ export class TransactionPersistor { return txEntity } - private static isCurrentWalletCell(cell: InputEntity | OutputEntity, lockArgsSet: Set) { - return (cell.lockArgs && ( - lockArgsSet.has(cell.lockArgs) - || ( - cell.lockArgs.length === CHEQUE_ARGS_LENGTH - && [cell.lockArgs.slice(0, DEFAULT_ARGS_LENGTH), `0x${cell.lockArgs.slice(DEFAULT_ARGS_LENGTH)}`].some(v => lockArgsSet.has(v)) - ) - )) + private static shouldSaveDetail(cell: InputEntity | OutputEntity, lockArgsSetNeedsDetail: Set) { + return ( + cell.lockArgs && + (lockArgsSetNeedsDetail.has(cell.lockArgs) || + (cell.lockArgs.length === CHEQUE_ARGS_LENGTH && + [cell.lockArgs.slice(0, DEFAULT_ARGS_LENGTH), `0x${cell.lockArgs.slice(DEFAULT_ARGS_LENGTH)}`].some(v => + lockArgsSetNeedsDetail.has(v) + ))) + ) } } diff --git a/packages/neuron-wallet/src/services/tx/transaction-service.ts b/packages/neuron-wallet/src/services/tx/transaction-service.ts index 0ca45a77cf..c38d3a3e03 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-service.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-service.ts @@ -19,6 +19,7 @@ import exportTransactions from '../../utils/export-history' import RpcService from '../rpc-service' import NetworksService from '../networks' import Script from '../../models/chain/script' +import Input from '../../models/chain/input' export interface TransactionsByAddressesParam { pageNo: number @@ -96,7 +97,7 @@ export class TransactionsService { SELECT input.transactionHash FROM input WHERE input.lockArgs in (select publicKeyInBlake160 from hd_public_key_info where walletId = @1) ) `, - [lockHashToSearch, params.walletID ] + [lockHashToSearch, params.walletID] ) .then((txs: { transactionHash: string }[]) => txs.map(tx => tx.transactionHash)) } else if (type === SearchType.TxHash) { @@ -497,7 +498,15 @@ export class TransactionsService { return undefined } const tx = txInDB.toModel() - const inputTxHashes = txWithStatus.transaction.inputs.map(v => v.previousOutput?.txHash).filter((v): v is string => !!v) + tx.inputs = await this.fillInputFields(txWithStatus.transaction.inputs) + tx.outputs = txWithStatus.transaction.outputs + return tx + } + + private static async fillInputFields(inputs: Input[]) { + const inputTxHashes = inputs.map(v => v.previousOutput?.txHash).filter((v): v is string => !!v) + if (!inputTxHashes.length) return inputs + const url: string = NetworksService.getInstance().getCurrent().remote const ckb = new CKB(url) const inputTxs = await ckb.rpc .createBatchRequest<'getTransaction', string[], CKBComponents.TransactionWithStatus[]>( @@ -508,19 +517,17 @@ export class TransactionsService { inputTxs.forEach((v, idx) => { inputTxMap.set(inputTxHashes[idx], v.transaction) }) - txWithStatus.transaction.inputs.forEach(v => { - if (!v.previousOutput?.txHash) return + return inputs.map(v => { + if (!v.previousOutput?.txHash) return v const output = inputTxMap.get(v.previousOutput.txHash)?.outputs?.[+v.previousOutput.index] - if (!output) return + if (!output) return v v.setCapacity(output.capacity) v.setLock(Script.fromSDK(output.lock)) if (output.type) { v.setType(Script.fromSDK(output.type)) } + return v }) - tx.inputs = txWithStatus.transaction.inputs - tx.outputs = txWithStatus.transaction.outputs - return tx } public static blake160sOfTx(tx: Transaction) { diff --git a/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts index 47e616193a..3aa87e818c 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts @@ -12,6 +12,8 @@ import HdPublicKeyInfo from '../../../src/database/chain/entities/hd-public-key- import TransactionPersistor, { TxSaveType } from '../../../src/services/tx/transaction-persistor' import SystemScriptInfo from '../../../src/models/system-script-info' import { scriptToAddress } from '@nervosnetwork/ckb-sdk-utils' +import Input from '../../../src/models/chain/input' +import OutPoint from '../../../src/models/chain/out-point' jest.mock('../../../src/models/asset-account-info', () => { const originalModule = jest.requireActual('../../../src/models/asset-account-info').default @@ -561,4 +563,93 @@ describe('Test TransactionService', () => { }) }) }) + + describe('fillInputFields', () => { + it('inputs is empty', async () => { + const inputs: Input[] = [] + //@ts-ignore private-method + const actual = await TransactionService.fillInputFields(inputs) + expect(actual).toBe(inputs) + }) + it('inputs without txHash', async () => { + const inputs = [ + Input.fromObject({ + previousOutput: null + }) + ] + //@ts-ignore private-method + const actual = await TransactionService.fillInputFields(inputs) + expect(actual).toBe(inputs) + }) + it('can not get output', async () => { + const inputs = [ + Input.fromObject({ + previousOutput: new OutPoint(`0x${'0'.repeat(64)}`, '0x0') + }) + ] + ckbRpcExecMock.mockResolvedValueOnce([]) + //@ts-ignore private-method + const actual = await TransactionService.fillInputFields(inputs) + expect(actual).toStrictEqual(inputs) + }) + it('success fill input fields without type', async () => { + const inputs = [ + Input.fromObject({ + previousOutput: new OutPoint(`0x${'0'.repeat(64)}`, '0x0') + }) + ] + const transactionWithStatus = { + transaction: { + outputs: [ + { + capacity: '0x1000', + lock: { + codeHash: `0x${'0'.repeat(64)}`, + hashType: 'data', + args: '0x0' + }, + } + ] + } + } + ckbRpcExecMock.mockResolvedValueOnce([transactionWithStatus]) + //@ts-ignore private-method + const actual = await TransactionService.fillInputFields(inputs) + expect(actual[0].capacity).toBe('4096') + expect(actual[0].lock?.toSDK()).toStrictEqual(transactionWithStatus.transaction.outputs[0].lock) + expect(actual[0].type).toBeUndefined() + }) + it('success fill input fields with type', async () => { + const inputs = [ + Input.fromObject({ + previousOutput: new OutPoint(`0x${'0'.repeat(64)}`, '0x0') + }) + ] + const transactionWithStatus = { + transaction: { + outputs: [ + { + capacity: '0x1000', + lock: { + codeHash: `0x${'0'.repeat(64)}`, + hashType: 'data', + args: '0x0' + }, + type: { + codeHash: `0x${'1'.repeat(64)}`, + hashType: 'data', + args: '0x1' + } + } + ] + } + } + ckbRpcExecMock.mockResolvedValueOnce([transactionWithStatus]) + //@ts-ignore private-method + const actual = await TransactionService.fillInputFields(inputs) + expect(actual[0].capacity).toBe('4096') + expect(actual[0].lock?.toSDK()).toStrictEqual(transactionWithStatus.transaction.outputs[0].lock) + expect(actual[0].type?.toSDK()).toStrictEqual(transactionWithStatus.transaction.outputs[0].type) + }) + }) })