diff --git a/.travis.yml b/.travis.yml index 764186dfc..786dae07c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ node_js: - "12" services: - docker + - mongodb env: global: - RELEASE_BRANCH=main @@ -19,6 +20,8 @@ before_script: - . ./travis/docker-functions.sh - log_env_variables - echo '$SUBPROJECT' +- mongo mydb_test --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});' +- if [[ ! -z "$DOCKER_USERNAME" ]] ; then echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin; fi - sh yarn_setup.sh script: cd ${SUBPROJECT} && yarn run lint && yarn run test:travis && cd .. diff --git a/CHANGELOG.md b/CHANGELOG.md index 691b14fe6..927e7fb04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [v2.3.2] - 02-Feb-2021 + +### Added + +- `FromHeight` and `ToHeight` to receipt search endpoint. + +### Fixed + +- Fixed issues on only multisig and aggregate initiator can query partial transactions. + ## [v2.3.1] - 19-Jan-2021 ### Fixed diff --git a/rest/bootstrap-preset.yml b/rest/bootstrap-preset.yml index 3778c583e..e382c1629 100644 --- a/rest/bootstrap-preset.yml +++ b/rest/bootstrap-preset.yml @@ -10,6 +10,7 @@ nodes: - trustedHosts: '127.0.0.1, 172.20.0.25, 172.20.0.1' localNetworks: '127.0.0.1, 172.20.0.25, 172.20.0.1' brokerOpenPort: 7902 + openPort: '{{add $index 7900}}' gateways: - excludeDockerService: true name: rest diff --git a/rest/package.json b/rest/package.json index 21c91ac91..aa15dc180 100644 --- a/rest/package.json +++ b/rest/package.json @@ -13,14 +13,14 @@ "test": "mocha --full-trace --recursive", "test:coverage": "nyc npm test && nyc report --reporter=text-lcov", "test:jenkins": "cross-env JUNIT_REPORT_PATH=test-results.xml mocha --reporter mocha-jenkins-reporter --mongoHost db --forbid-only --full-trace --recursive test || exit 0", - "test:travis": "npm run bootstrap-start-detached && nyc npm test && nyc report --reporter=text-lcov | coveralls && npm run bootstrap-stop ", + "test:travis": "nyc npm test && nyc report --reporter=text-lcov | coveralls", "lint": "eslint src test", "lint:fix": "eslint src test --fix", "lint:jenkins": "eslint -o tests.catapult.lint.xml -f junit src test || exit 0", "bootstrap-clean": "symbol-bootstrap clean", "bootstrap-start": "symbol-bootstrap start -a light -c bootstrap-preset.yml --healthCheck", "bootstrap-start-testnet": "symbol-bootstrap start -p testnet -a dual -c bootstrap-preset-testnet.yml --healthCheck", - "bootstrap-start-detached": "symbol-bootstrap start -a light -c bootstrap-preset.yml --detached --healthCheck", + "bootstrap-start-detached": "symbol-bootstrap start -a light -c bootstrap-preset.yml --detached --healthCheck --pullImages", "bootstrap-stop": "symbol-bootstrap stop", "bootstrap-run": "symbol-bootstrap run", "bootstrap-run-detached": "symbol-bootstrap run --detached --healthCheck", @@ -47,7 +47,7 @@ "nodemon": "^2.0.6", "rimraf": "^2.6.3", "sinon": "^7.3.2", - "symbol-bootstrap": "0.3.0-alpha-202012081631" + "symbol-bootstrap": "0.4.1" }, "dependencies": { "catapult-sdk": "link:../catapult-sdk", diff --git a/rest/src/db/CatapultDb.js b/rest/src/db/CatapultDb.js index ca7635247..e9621aa0d 100644 --- a/rest/src/db/CatapultDb.js +++ b/rest/src/db/CatapultDb.js @@ -23,6 +23,7 @@ const connector = require('./connector'); const { convertToLong, buildOffsetCondition } = require('./dbUtils'); +const MultisigDb = require('../plugins/multisig/MultisigDb'); const catapult = require('catapult-sdk'); const MongoDb = require('mongodb'); @@ -304,24 +305,33 @@ class CatapultDb { * `pageSize` and `pageNumber`. 'sortField' must be within allowed 'sortingOptions'. * @returns {Promise.} Transactions page. */ - transactions(group, filters, options) { + async transactions(group, filters, options) { const sortingOptions = { id: '_id' }; - const buildAccountConditions = () => { - if (undefined !== filters.address) + const buildAccountConditions = async () => { + // Check multisig graph if address is used in search criteria for cosigning, + // Then, show transactions for other cosigers. + if (undefined !== filters.address) { + if ('partial' === group) { + const multisigEntries = await new MultisigDb(this).multisigsByAddresses([filters.address]); + + if (multisigEntries.length && multisigEntries[0].multisig.multisigAddresses.length) { + const buffers = multisigEntries[0].multisig.multisigAddresses.map(address => address.buffer); + buffers.push(Buffer.from(filters.address)); + return { 'meta.addresses': { $in: buffers } }; + } + } return { 'meta.addresses': Buffer.from(filters.address) }; - + } const accountConditions = {}; if (undefined !== filters.signerPublicKey) accountConditions['transaction.signerPublicKey'] = Buffer.from(filters.signerPublicKey); - if (undefined !== filters.recipientAddress) accountConditions['transaction.recipientAddress'] = Buffer.from(filters.recipientAddress); - return accountConditions; }; - const buildConditions = () => { + const buildConditions = async () => { let conditions = {}; const offsetCondition = buildOffsetCondition(options, sortingOptions); @@ -363,7 +373,7 @@ class CatapultDb { conditions[amountPath].$lte = convertToLong(filters.toTransferAmount); } - const accountConditions = buildAccountConditions(); + const accountConditions = await buildAccountConditions(); if (accountConditions) conditions = Object.assign(conditions, accountConditions); @@ -372,7 +382,7 @@ class CatapultDb { const removedFields = ['meta.addresses']; const sortConditions = { [sortingOptions[options.sortField]]: options.sortDirection }; - const conditions = buildConditions(); + const conditions = await buildConditions(); return this.queryPagedDocuments(conditions, removedFields, sortConditions, TransactionGroup[group], options); } @@ -470,17 +480,18 @@ class CatapultDb { // fetch result sorted by specific mosaic amount, this unwinds mosaics and only returns matching mosaics (incomplete response) queryPromise = this.database.collection('accounts') .aggregate([], { promoteLongs: false }) - .skip(pageSize * pageIndex) - .limit(pageSize) .unwind('$account.mosaics') .match(conditions) .sort(sortConditions) + .skip(pageSize * pageIndex) + .limit(pageSize) .toArray() .then(accounts => { const accountIds = accounts.map(account => account._id); const newConditions = { _id: { $in: accountIds } }; - // repeat the response with the found and sorted account ids, so that the result can be complete with all the mosaics + // Second query set pageIndex to 0; + options.pageNumber = 1; return this.queryPagedDocuments(newConditions, [], {}, 'accounts', options) .then(fullAccountsPage => { // $in results do not preserve query order diff --git a/rest/src/plugins/multisig/multisigRoutes.js b/rest/src/plugins/multisig/multisigRoutes.js index 77a1878ae..ca291ac6c 100644 --- a/rest/src/plugins/multisig/multisigRoutes.js +++ b/rest/src/plugins/multisig/multisigRoutes.js @@ -19,6 +19,7 @@ * along with Catapult. If not, see . */ +const multisigUtils = require('./multisigUtils'); const merkleUtils = require('../../routes/merkleUtils'); const routeUtils = require('../../routes/routeUtils'); const catapult = require('catapult-sdk'); @@ -44,58 +45,9 @@ module.exports = { }); }); - const getMultisigEntries = (multisigEntries, fieldName) => { - const addresses = new Set(); - multisigEntries.forEach(multisigEntry => multisigEntry.multisig[fieldName].forEach(address => { - addresses.add(address.buffer); - })); - - return db.multisigsByAddresses(Array.from(addresses)); - }; - server.get('/account/:address/multisig/graph', (req, res, next) => { const accountAddress = routeUtils.parseArgument(req.params, 'address', 'address'); - - const multisigLevels = []; - return db.multisigsByAddresses([accountAddress]) - .then(multisigEntries => { - if (0 === multisigEntries.length) - return Promise.resolve(undefined); - - multisigLevels.push({ - level: 0, - multisigEntries: [multisigEntries[0]] - }); - - return Promise.resolve(multisigEntries[0]); - }) - .then(multisigEntry => { - if (undefined === multisigEntry) - return Promise.resolve(undefined); - - const handleUpstream = (level, multisigEntries) => getMultisigEntries(multisigEntries, 'multisigAddresses') - .then(entries => { - if (0 === entries.length) - return Promise.resolve(); - - multisigLevels.unshift({ level, multisigEntries: entries }); - return handleUpstream(level - 1, entries); - }); - - const handleDownstream = (level, multisigEntries) => getMultisigEntries(multisigEntries, 'cosignatoryAddresses') - .then(entries => { - if (0 === entries.length) - return Promise.resolve(); - - multisigLevels.push({ level, multisigEntries: entries }); - return handleDownstream(level + 1, entries); - }); - - const upstreamPromise = handleUpstream(-1, [multisigEntry]); - const downstreamPromise = handleDownstream(1, [multisigEntry]); - return Promise.all([upstreamPromise, downstreamPromise]) - .then(() => multisigLevels); - }) + return multisigUtils.getMultisigGraph(db, accountAddress) .then(response => { const sender = routeUtils.createSender('multisigGraph'); return undefined === response diff --git a/rest/src/plugins/multisig/multisigUtils.js b/rest/src/plugins/multisig/multisigUtils.js new file mode 100644 index 000000000..56f5c7433 --- /dev/null +++ b/rest/src/plugins/multisig/multisigUtils.js @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-2019, Jaguar0625, gimre, BloodyRookie, Tech Bureau, Corp. + * Copyright (c) 2020-present, Jaguar0625, gimre, BloodyRookie. + * All rights reserved. + * + * This file is part of Catapult. + * + * Catapult is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Catapult is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Catapult. If not, see . + */ + +const multisigUtils = { + getMultisigGraph: (db, address) => { + const getMultisigEntries = (multisigEntries, fieldName) => { + const addresses = new Set(); + multisigEntries.forEach(multisigEntry => multisigEntry.multisig[fieldName].forEach(multisigAddress => { + addresses.add(multisigAddress.buffer); + })); + + return db.multisigsByAddresses(Array.from(addresses)); + }; + + const multisigLevels = []; + return db.multisigsByAddresses([address]) + .then(multisigEntries => { + if (0 === multisigEntries.length) + return Promise.resolve(undefined); + + multisigLevels.push({ + level: 0, + multisigEntries: [multisigEntries[0]] + }); + + return Promise.resolve(multisigEntries[0]); + }) + .then(multisigEntry => { + if (undefined === multisigEntry) + return Promise.resolve(undefined); + + const handleUpstream = (level, multisigEntries) => getMultisigEntries(multisigEntries, 'multisigAddresses') + .then(entries => { + if (0 === entries.length) + return Promise.resolve(); + + multisigLevels.unshift({ level, multisigEntries: entries }); + return handleUpstream(level - 1, entries); + }); + + const handleDownstream = (level, multisigEntries) => getMultisigEntries(multisigEntries, 'cosignatoryAddresses') + .then(entries => { + if (0 === entries.length) + return Promise.resolve(); + + multisigLevels.push({ level, multisigEntries: entries }); + return handleDownstream(level + 1, entries); + }); + + const upstreamPromise = handleUpstream(-1, [multisigEntry]); + const downstreamPromise = handleDownstream(1, [multisigEntry]); + return Promise.all([upstreamPromise, downstreamPromise]) + .then(() => multisigLevels); + }); + } + +}; + +module.exports = multisigUtils; diff --git a/rest/src/plugins/receipts/ReceiptsDb.js b/rest/src/plugins/receipts/ReceiptsDb.js index de014fa40..941c2f900 100644 --- a/rest/src/plugins/receipts/ReceiptsDb.js +++ b/rest/src/plugins/receipts/ReceiptsDb.js @@ -72,6 +72,17 @@ class ReceiptsDb { conditions[[`statement.receipts.${artifactIdType}`]] = convertToLong(filters.artifactId); } + if (undefined !== filters.fromHeight || undefined !== filters.toHeight) { + const heightPath = 'statement.height'; + conditions[heightPath] = {}; + + if (undefined !== filters.fromHeight) + conditions[heightPath].$gte = convertToLong(filters.fromHeight); + + if (undefined !== filters.toHeight) + conditions[heightPath].$lte = convertToLong(filters.toHeight); + } + const sortConditions = { [sortingOptions[options.sortField]]: options.sortDirection }; return this.catapultDb.queryPagedDocuments(conditions, [], sortConditions, 'transactionStatements', options); } diff --git a/rest/src/plugins/receipts/receiptsRoutes.js b/rest/src/plugins/receipts/receiptsRoutes.js index 953b2ba3e..3180439f7 100644 --- a/rest/src/plugins/receipts/receiptsRoutes.js +++ b/rest/src/plugins/receipts/receiptsRoutes.js @@ -29,6 +29,8 @@ module.exports = { const { params } = req; const filters = { height: params.height ? routeUtils.parseArgument(params, 'height', 'uint64') : undefined, + fromHeight: params.fromHeight ? routeUtils.parseArgument(params, 'fromHeight', 'uint64') : undefined, + toHeight: params.toHeight ? routeUtils.parseArgument(params, 'toHeight', 'uint64') : undefined, receiptType: params.receiptType ? routeUtils.parseArgumentAsArray(params, 'receiptType', 'uint') : undefined, recipientAddress: params.recipientAddress ? routeUtils.parseArgument(params, 'recipientAddress', 'address') : undefined, senderAddress: params.senderAddress ? routeUtils.parseArgument(params, 'senderAddress', 'address') : undefined, diff --git a/rest/src/routes/transactionRoutes.js b/rest/src/routes/transactionRoutes.js index 0d64b1d7b..b2951bca7 100644 --- a/rest/src/routes/transactionRoutes.js +++ b/rest/src/routes/transactionRoutes.js @@ -72,7 +72,6 @@ module.exports = { recipientAddress: params.recipientAddress ? routeUtils.parseArgument(params, 'recipientAddress', 'address') : undefined, transactionTypes: params.type ? routeUtils.parseArgumentAsArray(params, 'type', 'uint') : undefined, embedded: params.embedded ? routeUtils.parseArgument(params, 'embedded', 'boolean') : undefined, - /** transfer transaction specific filters */ transferMosaicId: params.transferMosaicId ? routeUtils.parseArgument(params, 'transferMosaicId', 'uint64hex') : undefined, fromTransferAmount: params.fromTransferAmount @@ -80,7 +79,6 @@ module.exports = { toTransferAmount: params.toTransferAmount ? routeUtils.parseArgument(params, 'toTransferAmount', 'uint64') : undefined }; const options = routeUtils.parsePaginationArguments(params, services.config.pageSize, { id: 'objectId' }); - return db.transactions(params.group, filters, options) .then(result => routeUtils.createSender(routeResultTypes.transaction).sendPage(res, next)(result)); }); diff --git a/rest/test/plugins/receipts/receiptsRoutes_spec.js b/rest/test/plugins/receipts/receiptsRoutes_spec.js index db3624003..666cc1988 100644 --- a/rest/test/plugins/receipts/receiptsRoutes_spec.js +++ b/rest/test/plugins/receipts/receiptsRoutes_spec.js @@ -206,6 +206,34 @@ describe('receipts routes', () => { }); }); + it('forwards fromHeight', () => { + // Arrange: + const req = { params: { fromHeight: '123' } }; + + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionStatementsFake.calledOnce).to.equal(true); + expect(dbTransactionStatementsFake.firstCall.args[0].fromHeight).to.deep.equal([123, 0]); + + expect(mockServer.next.calledOnce).to.equal(true); + }); + }); + + it('forwards toHeight', () => { + // Arrange: + const req = { params: { toHeight: '123' } }; + + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionStatementsFake.calledOnce).to.equal(true); + expect(dbTransactionStatementsFake.firstCall.args[0].toHeight).to.deep.equal([123, 0]); + + expect(mockServer.next.calledOnce).to.equal(true); + }); + }); + describe('forwards receiptType', () => { it('one element', () => { // Arrange: diff --git a/rest/yarn.lock b/rest/yarn.lock index 7a368a0a0..1eba03780 100644 --- a/rest/yarn.lock +++ b/rest/yarn.lock @@ -802,16 +802,12 @@ caseless@~0.12.0: "catapult-sdk@link:../catapult-sdk": version "0.0.0" - dependencies: - js-sha3 "^0.8.0" - long "^4.0.0" - ripemd160 "^2.0.2" - tweetnacl "^1.0.1" + uid "" -catbuffer-typescript@0.0.24-alpha-202011210642: - version "0.0.24-alpha-202011210642" - resolved "https://registry.yarnpkg.com/catbuffer-typescript/-/catbuffer-typescript-0.0.24-alpha-202011210642.tgz#d4837354b55f7eee5d994bd71642da813e012916" - integrity sha512-rxbr92UcwTgCrpEbapaLROyMAE9RAssU7OXmWfodvYmi3VADZAo3OzqF3pxCSALvCJNRgWB2bXKT0IvE/012CA== +catbuffer-typescript@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/catbuffer-typescript/-/catbuffer-typescript-0.1.1.tgz#152746d7ec07af28da31c2f4ad9e00cc27133d5a" + integrity sha512-r/z3UKG3YCCdsTEHRXGe3IQxA8OaBRBE31e9du2LOaLStGxYCmxUjfRtJ/DyKfgpS55fJPl3w/VFMnsfwIHmkA== chai@^4.2.0: version "4.2.0" @@ -5904,10 +5900,10 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -symbol-bootstrap@0.3.0-alpha-202012081631: - version "0.3.0-alpha-202012081631" - resolved "https://registry.yarnpkg.com/symbol-bootstrap/-/symbol-bootstrap-0.3.0-alpha-202012081631.tgz#ce2a50165bb513fa0c8a8423f8576e9e8b19c25c" - integrity sha512-2N/amDgmm5foqOw6EGaKyfRC6xqrQh9T6fx/fkMefdrf54caJ9Qc/RXdcD/pF4aM4DhSzWRsCIveA3i+vIHj+w== +symbol-bootstrap@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/symbol-bootstrap/-/symbol-bootstrap-0.4.1.tgz#7495c9e096ad0f4015852ec8bd329b3e48fc1756" + integrity sha512-Cxm8wnR+atUKvyFRRG9jV1zE03vw7MYc5gIALsAsR6Tdu1X9IGU6BiG4e9CgCPjrv4HhA6wcuW+309eMaZBw7w== dependencies: "@oclif/command" "^1.7.0" "@oclif/config" "^1.16.0" @@ -5921,24 +5917,24 @@ symbol-bootstrap@0.3.0-alpha-202012081631: rxjs "^6.6.3" shx "^0.3.2" sshpk "^1.16.1" - symbol-sdk "0.21.1-alpha-202011211848" + symbol-sdk "0.23.0" tslib "^1.13.0" utf8 "^2.1.2" winston "^3.3.3" -symbol-openapi-typescript-fetch-client@0.10.1-SNAPSHOT.202011191848: - version "0.10.1-SNAPSHOT.202011191848" - resolved "https://registry.yarnpkg.com/symbol-openapi-typescript-fetch-client/-/symbol-openapi-typescript-fetch-client-0.10.1-SNAPSHOT.202011191848.tgz#44445d2344aeebacbed29b70afc6247ac2d61cc8" - integrity sha512-5V7FBtdzz8nX3U06a6yqudLOMvk0/sw+cD5uIubYOmOrleIWFp2nb1IE7nrCSr1t4Qr/Rlg9UU6T/Xuyfp4YFA== +symbol-openapi-typescript-fetch-client@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/symbol-openapi-typescript-fetch-client/-/symbol-openapi-typescript-fetch-client-0.11.1.tgz#e4e67a8cdd47032d2a4ed5619c7e882182edf6d5" + integrity sha512-4YVS4RzKOPv3c2+g0N989mglw53Kd91jEUpjxEltjZBzfoCNPc2cM12Ct1DWZVlnypo23iVOkGCM/mkR7Qflsw== -symbol-sdk@0.21.1-alpha-202011211848: - version "0.21.1-alpha-202011211848" - resolved "https://registry.yarnpkg.com/symbol-sdk/-/symbol-sdk-0.21.1-alpha-202011211848.tgz#0a9a411039152e31ad8f08a273b0de22859d8711" - integrity sha512-qPOFSYHwVvMi2gg0WE/4PndBhPgghtC4hlvwS6x0Q9juZNz2/D63z6wZyBdXonlMmW8LNLWgWHx9QbMhb9ojWg== +symbol-sdk@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/symbol-sdk/-/symbol-sdk-0.23.0.tgz#c3577e48255a1093e4474e281fd38812863e14a8" + integrity sha512-rYqy2f+omwZC7JUXznR2timoEnUkC/OZIEh8mBW/zyrKrMpseZdL/YTIq6Ht11TKolBPib2J5f5H1n33a+sYng== dependencies: "@js-joda/core" "^3.2.0" bluebird "^3.7.2" - catbuffer-typescript "0.0.24-alpha-202011210642" + catbuffer-typescript "0.1.1" crypto-js "^4.0.0" diff "^4.0.2" futoin-hkdf "^1.3.2" @@ -5952,7 +5948,7 @@ symbol-sdk@0.21.1-alpha-202011211848: ripemd160 "^2.0.2" rxjs "^6.6.3" rxjs-compat "^6.6.3" - symbol-openapi-typescript-fetch-client "0.10.1-SNAPSHOT.202011191848" + symbol-openapi-typescript-fetch-client "0.11.1" tweetnacl "^1.0.3" ws "^7.3.1" diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +