diff --git a/packages/neuron-ui/src/components/NervosDAO/hooks.ts b/packages/neuron-ui/src/components/NervosDAO/hooks.ts index cc306e03d5..58a66ea222 100644 --- a/packages/neuron-ui/src/components/NervosDAO/hooks.ts +++ b/packages/neuron-ui/src/components/NervosDAO/hooks.ts @@ -20,8 +20,9 @@ import { generateDaoDepositTx, generateDaoClaimTx, } from 'services/remote' -import { ckbCore, getHeaderByNumber, calculateDaoMaximumWithdraw } from 'services/chain' +import { ckbCore, getHeaderByNumber } from 'services/chain' import { isErrorWithI18n } from 'exceptions' +import { calculateMaximumWithdraw } from '@nervosnetwork/ckb-sdk-utils' const { MIN_AMOUNT, @@ -91,7 +92,7 @@ export const useInitData = ({ updateNervosDaoData({ walletID: wallet.id })(dispatch) const intervalId = setInterval(() => { updateNervosDaoData({ walletID: wallet.id })(dispatch) - }, 3000) + }, 10000) updateDepositValue( `${ BigInt(wallet.balance) > BigInt(CKBToShannonFormatter(`${MIN_DEPOSIT_AMOUNT}`)) @@ -436,35 +437,77 @@ export const useUpdateWithdrawList = ({ setWithdrawList: React.Dispatch>> }) => useEffect(() => { - Promise.all( - records.map(async ({ outPoint, depositOutPoint, blockHash }) => { - if (!tipBlockHash) { - return null - } - const withdrawBlockHash = depositOutPoint ? blockHash : tipBlockHash - const formattedDepositOutPoint = depositOutPoint - ? { - txHash: depositOutPoint.txHash, - index: `0x${BigInt(depositOutPoint.index).toString(16)}`, - } - : { - txHash: outPoint.txHash, - index: `0x${BigInt(outPoint.index).toString(16)}`, - } - return calculateDaoMaximumWithdraw(formattedDepositOutPoint, withdrawBlockHash).catch(() => null) - }) - ) - .then(res => { - const withdrawList = new Map() - if (tipBlockHash) { - records.forEach((record, idx) => { - const key = getRecordKey(record) - withdrawList.set(key, res[idx]) + if (!tipBlockHash) { + setWithdrawList(new Map()) + return + } + const depositOutPointHashes = records.map(v => v.depositOutPoint?.txHash ?? v.outPoint.txHash) + ckbCore.rpc + .createBatchRequest<'getTransaction', string[], CKBComponents.TransactionWithStatus[]>( + depositOutPointHashes.map(v => ['getTransaction', v]) + ) + .exec() + .then(txs => { + const committedTx = txs.filter(v => v.txStatus.status === 'committed') + const blockHashes = [ + ...(committedTx.map(v => v.txStatus.blockHash).filter(v => !!v) as string[]), + ...(records.map(v => (v.depositOutPoint ? v.blockHash : null)).filter(v => !!v) as string[]), + tipBlockHash, + ] + return ckbCore.rpc + .createBatchRequest<'getHeader', string[], CKBComponents.BlockHeader[]>( + blockHashes.map(v => ['getHeader', v]) + ) + .exec() + .then(blockHeaders => { + const hashHeaderMap = new Map() + blockHeaders.forEach((header, idx) => { + hashHeaderMap.set(blockHashes[idx], header.dao) + }) + const txMap = new Map() + txs.forEach((tx, idx) => { + if (tx.txStatus.status === 'committed') { + txMap.set(depositOutPointHashes[idx], tx) + } + }) + const withdrawList = new Map() + records.forEach(record => { + const key = getRecordKey(record) + const withdrawBlockHash = record.depositOutPoint ? record.blockHash : tipBlockHash + const formattedDepositOutPoint = record.depositOutPoint + ? { + txHash: record.depositOutPoint.txHash, + index: `0x${BigInt(record.depositOutPoint.index).toString(16)}`, + } + : { + txHash: record.outPoint.txHash, + index: `0x${BigInt(record.outPoint.index).toString(16)}`, + } + const tx = txMap.get(formattedDepositOutPoint.txHash) + if (!tx) { + return + } + const depositDAO = hashHeaderMap.get(tx.txStatus.blockHash!) + const withdrawDAO = hashHeaderMap.get(withdrawBlockHash) + if (!depositDAO || !withdrawDAO) { + return + } + withdrawList.set( + key, + calculateMaximumWithdraw( + tx.transaction.outputs[+formattedDepositOutPoint.index], + tx.transaction.outputsData[+formattedDepositOutPoint.index], + depositDAO, + withdrawDAO + ) + ) + }) + setWithdrawList(withdrawList) }) - } - setWithdrawList(withdrawList) }) - .catch(console.error) + .catch(() => { + setWithdrawList(new Map()) + }) }, [records, tipBlockHash, setWithdrawList]) export const useUpdateDepositEpochList = ({ @@ -478,24 +521,28 @@ export const useUpdateDepositEpochList = ({ }) => useEffect(() => { if (connectionStatus === 'online') { - Promise.all( - records.map(({ daoData, depositOutPoint, blockNumber }) => { - const depositBlockNumber = depositOutPoint ? ckbCore.utils.toUint64Le(daoData) : blockNumber - if (!depositBlockNumber) { - return null - } - return getHeaderByNumber(BigInt(depositBlockNumber)) - .then(header => header.epoch) - .catch(() => null) - }) - ).then(res => { - const epochList = new Map() - records.forEach((record, idx) => { - const key = getRecordKey(record) - epochList.set(key, res[idx]) - }) - setDepositEpochList(epochList) + const recordKeyIdxMap = new Map() + const batchParams: ['getHeaderByNumber', bigint][] = [] + records.forEach((record, idx) => { + const depositBlockNumber = record.depositOutPoint + ? ckbCore.utils.toUint64Le(record.daoData) + : record.blockNumber + if (depositBlockNumber) { + batchParams.push(['getHeaderByNumber', BigInt(depositBlockNumber)]) + recordKeyIdxMap.set(getRecordKey(record), idx) + } }) + ckbCore.rpc + .createBatchRequest<'getHeaderByNumber', any, CKBComponents.BlockHeader[]>(batchParams) + .exec() + .then(res => { + const epochList = new Map() + records.forEach(record => { + const key = getRecordKey(record) + epochList.set(key, recordKeyIdxMap.get(key) ? res[recordKeyIdxMap.get(key)!]?.epoch : null) + }) + setDepositEpochList(epochList) + }) } }, [records, setDepositEpochList, connectionStatus]) diff --git a/packages/neuron-wallet/src/services/cells.ts b/packages/neuron-wallet/src/services/cells.ts index 51468c3c9e..7b44e9ce5b 100644 --- a/packages/neuron-wallet/src/services/cells.ts +++ b/packages/neuron-wallet/src/services/cells.ts @@ -131,6 +131,65 @@ export default class CellsService { return uniqueLockArgs } + private static async addUnlockInfo(cells: Cell[]): Promise { + // find unlock info + const unlockTxHashes: string[] = cells + .filter(v => v.outPoint && (v.status === OutputStatus.Dead || v.status === OutputStatus.Pending)) + .map(o => o.outPoint!.txHash) + const inputs: InputEntity[] = await getConnection() + .getRepository(InputEntity) + .createQueryBuilder('input') + .leftJoinAndSelect('input.transaction', 'tx') + .where({ + outPointTxHash: In(unlockTxHashes) + }) + .getMany() + const unlockTxMap = new Map() + inputs.forEach(i => { + const key = i.outPointTxHash + ':' + i.outPointIndex + unlockTxMap.set(key, i.transaction!) + }) + cells.forEach(cell => { + // if unlocked, set unlockInfo + const key = cell.outPoint?.txHash + ':' + cell.outPoint?.index + const unlockTx = key ? unlockTxMap.get(key) : undefined + if (unlockTx && (cell.status === OutputStatus.Dead || cell.status === OutputStatus.Pending)) { + cell.setUnlockInfo({ + txHash: unlockTx.hash, + timestamp: unlockTx.timestamp! + }) + } + }) + return cells + } + + private static async addDepositInfo(cells: Cell[]): Promise { + // find deposit info + const depositTxHashes = cells.map(cells => cells.depositOutPoint?.txHash).filter(hash => !!hash) + const depositTxs = await getConnection() + .getRepository(TransactionEntity) + .createQueryBuilder('tx') + .where({ + hash: In(depositTxHashes) + }) + .getMany() + const depositTxMap = new Map() + depositTxs.forEach(tx => { + depositTxMap.set(tx.hash, tx) + }) + cells.forEach(cell => { + if (cell.depositOutPoint?.txHash && depositTxMap.has(cell.depositOutPoint.txHash)) { + const depositTx = depositTxMap.get(cell.depositOutPoint.txHash)! + cell.setDepositTimestamp(depositTx.timestamp!) + cell.setDepositInfo({ + txHash: depositTx.hash, + timestamp: depositTx.timestamp! + }) + } + }) + return cells + } + public static async getDaoCells(walletId: string): Promise { const outputs: OutputEntity[] = await getConnection() .getRepository(OutputEntity) @@ -169,36 +228,6 @@ export default class CellsService { .addOrderBy('tx.timestamp', 'ASC') .getMany() - // find deposit info - const depositTxHashes = outputs.map(output => output.depositTxHash).filter(hash => !!hash) - const depositTxs = await getConnection() - .getRepository(TransactionEntity) - .createQueryBuilder('tx') - .where({ - hash: In(depositTxHashes) - }) - .getMany() - const depositTxMap = new Map() - depositTxs.forEach(tx => { - depositTxMap.set(tx.hash, tx) - }) - - // find unlock info - const unlockTxKeys: string[] = outputs.map(o => o.outPointTxHash + ':' + o.outPointIndex) - const inputs: InputEntity[] = await getConnection() - .getRepository(InputEntity) - .createQueryBuilder('input') - .leftJoinAndSelect('input.transaction', 'tx') - .where(`input.outPointTxHash || ':' || input.outPointIndex IN (:...infos)`, { - infos: unlockTxKeys - }) - .getMany() - const unlockTxMap = new Map() - inputs.forEach(i => { - const key = i.outPointTxHash + ':' + i.outPointIndex - unlockTxMap.set(key, i.transaction!) - }) - const cells: Cell[] = outputs.map(output => { const cell = output.toModel() if (!output.depositTxHash) { @@ -208,35 +237,18 @@ export default class CellsService { timestamp: output.transaction!.timestamp! }) } else { - // if not deposit cell, set deposit timestamp info, depositInfo, withdrawInfo - const depositTx = depositTxMap.get(output.depositTxHash)! - cell.setDepositTimestamp(depositTx.timestamp!) - - cell.setDepositInfo({ - txHash: depositTx.hash, - timestamp: depositTx.timestamp! - }) - + // if not deposit cell, set withdrawInfo const withdrawTx = output.transaction cell.setWithdrawInfo({ txHash: withdrawTx!.hash, timestamp: withdrawTx!.timestamp! }) - - if (output.status === OutputStatus.Dead || output.status === OutputStatus.Pending) { - // if unlocked, set unlockInfo - const key = output.outPointTxHash + ':' + output.outPointIndex - const unlockTx = unlockTxMap.get(key)! - cell.setUnlockInfo({ - txHash: unlockTx.hash, - timestamp: unlockTx.timestamp! - }) - } } - return cell }) + await Promise.all([CellsService.addDepositInfo(cells), CellsService.addUnlockInfo(cells)]) + return cells } diff --git a/packages/neuron-wallet/src/services/tx/transaction-service.ts b/packages/neuron-wallet/src/services/tx/transaction-service.ts index de860e6639..45cf6298b0 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-service.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-service.ts @@ -484,16 +484,23 @@ export class TransactionsService { .createQueryBuilder('transaction') .where('transaction.hash is :hash', { hash }) .leftJoinAndSelect('transaction.inputs', 'input') - .leftJoinAndSelect('transaction.outputs', 'output') .orderBy({ 'input.id': 'ASC' }) .getOne() + const txOutputs = await getConnection() + .getRepository(OutputEntity) + .createQueryBuilder() + .where({ + outPointTxHash: hash + }) + .getMany() if (!tx) { return undefined } + tx.outputs = txOutputs return tx.toModel() } diff --git a/packages/neuron-wallet/tests/services/cells.test.ts b/packages/neuron-wallet/tests/services/cells.test.ts index 33a56bce48..96cf6bfb88 100644 --- a/packages/neuron-wallet/tests/services/cells.test.ts +++ b/packages/neuron-wallet/tests/services/cells.test.ts @@ -952,6 +952,155 @@ describe('CellsService', () => { }) }) + describe('#addUnlockInfo', () => { + const depositData = '0x0000000000000000' + const withdrawData = '0x000000000000000a' + const generateTx = (hash: string, timestamp: string) => { + const tx = new TransactionEntity() + tx.hash = hash + tx.version = '0x0' + tx.timestamp = timestamp + tx.status = TransactionStatus.Success + tx.witnesses = [] + tx.blockNumber = '1' + tx.blockHash = '0x' + '10'.repeat(32) + return tx + } + + const withdrawTxHash = '0x' + '2'.repeat(64) + + const unlockTxHash = '0x' + '4'.repeat(64) + const unlockTx = Transaction.fromObject({ + hash: unlockTxHash, + version: '0x0', + timestamp: '1572862777483', + status: TransactionStatus.Success, + witnesses: [], + blockNumber: '3', + blockHash: '0x' + '5'.repeat(64), + inputs: [ + Input.fromObject({ + previousOutput: new OutPoint(withdrawTxHash, '0'), + since: '0' + }) + ], + outputs: [ + Output.fromObject({ + capacity: '1000', + lock: bob.lockScript + }) + ] + }) + + const tx1 = generateTx('0x1234', '1572862777481') + + it('output cells is not cost', async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Live, false, null, bob, depositData, tx1).toModel() + ] + //@ts-ignore private property + await CellsService.addUnlockInfo(cells) + expect(cells[0].unlockInfo).toBeUndefined() + }) + + it('output cells no transaction', async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Dead, false, null, bob, depositData, tx1).toModel() + ] + //@ts-ignore private property + await CellsService.addUnlockInfo(cells) + expect(cells[0].unlockInfo).toBeUndefined() + }) + + it('output cells has cost', async () => { + await TransactionPersistor.saveFetchTx(unlockTx) + const outputs = Output.fromObject({ + capacity: '1000', + daoData: withdrawData, + lock: bob.lockScript, + type: SystemScriptInfo.generateDaoScript(), + outPoint: new OutPoint(withdrawTxHash, '0'), + status: OutputStatus.Dead + }) + //@ts-ignore private property + await CellsService.addUnlockInfo([outputs]) + expect(outputs.unlockInfo?.txHash).toEqual(unlockTxHash) + }) + }) + + describe('#addDepositInfo', () => { + const depositData = '0x0000000000000000' + const withdrawData = '0x000000000000000a' + const generateTx = (hash: string, timestamp: string) => { + const tx = new TransactionEntity() + tx.hash = hash + tx.version = '0x0' + tx.timestamp = timestamp + tx.status = TransactionStatus.Success + tx.witnesses = [] + tx.blockNumber = '1' + tx.blockHash = '0x' + '10'.repeat(32) + return tx + } + + const depositTxHash = '0x' + '0'.repeat(64) + const depositTx = Transaction.fromObject({ + hash: depositTxHash, + version: '0x0', + timestamp: '1572862777481', + status: TransactionStatus.Success, + witnesses: [], + blockNumber: '1', + blockHash: '0x' + '1'.repeat(64), + inputs: [], + outputs: [ + Output.fromObject({ + capacity: '1000', + daoData: depositData, + lock: bob.lockScript, + type: SystemScriptInfo.generateDaoScript() + }) + ] + }) + + const tx1 = generateTx('0x1234', '1572862777481') + + it('output cells is not deposit', async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Live, false, null, bob, depositData, tx1).toModel() + ] + //@ts-ignore private property + await CellsService.addDepositInfo(cells) + expect(cells[0].depositInfo).toBeUndefined() + }) + + it('output cells no transaction', async () => { + const cells = [ + generateCell(toShannon('1000'), OutputStatus.Dead, false, null, bob, depositData, tx1).toModel() + ] + cells[0].depositOutPoint = new OutPoint('0x' + '0'.repeat(64), '0x0') + //@ts-ignore private property + await CellsService.addDepositInfo(cells) + expect(cells[0].depositInfo).toBeUndefined() + }) + + it('output cells has cost', async () => { + await TransactionPersistor.saveFetchTx(depositTx) + const outputs = Output.fromObject({ + capacity: '1000', + daoData: withdrawData, + lock: bob.lockScript, + type: SystemScriptInfo.generateDaoScript(), + depositOutPoint: new OutPoint(depositTx.hash!, '0'), + status: OutputStatus.Dead + }) + //@ts-ignore + await CellsService.addDepositInfo([outputs]) + expect(outputs.depositInfo?.txHash).toEqual(depositTxHash) + }) + }) + + describe('#usedByAnyoneCanPayBlake160s', () => { const fakeArgs1 = '0x1' const fakeArgs2 = '0x2'