Skip to content
This repository has been archived by the owner on Jun 10, 2022. It is now read-only.

Commit

Permalink
Revisited transaction endpoints to support groups (confirmed/unconfir…
Browse files Browse the repository at this point in the history
…med/partial)
  • Loading branch information
Vektrat authored Jun 17, 2020
1 parent b8535f5 commit 2241e8b
Show file tree
Hide file tree
Showing 7 changed files with 561 additions and 312 deletions.
39 changes: 14 additions & 25 deletions rest/src/db/CatapultDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ const buildBlocksFromOptions = (height, numBlocks, chainHeight) => {
const getBoundedPageSize = (pageSize, pagingOptions) =>
Math.max(pagingOptions.pageSizeMin, Math.min(pagingOptions.pageSizeMax, pageSize || pagingOptions.pageSizeDefault));

const TransactionGroup = Object.freeze({
confirmed: 'transactions',
unconfirmed: 'unconfirmedTransactions',
partial: 'partialTransactions'
});

class CatapultDb {
// region construction / connect / disconnect

Expand Down Expand Up @@ -367,25 +373,16 @@ class CatapultDb {

/**
* Retrieves filtered and paginated transactions.
* @param {string} group Transactions group on which the query is made.
* @param {object} filters Filters to be applied: `address` for an involved address in the query, `signerPublicKey`, `recipientAddress`,
* `group`, `height`, `embedded`, `transactionTypes` array of uint. If `address` is provided, other account related filters are omitted.
* `height`, `embedded`, `transactionTypes` array of uint. If `address` is provided, other account related filters are omitted.
* @param {object} options Options for ordering and pagination. Can have an `offset`, and must contain the `sortField`, `sortDirection`,
* `pageSize` and `pageNumber`. 'sortField' must be within allowed 'sortingOptions'.
* @returns {Promise.<object>} Transactions page.
*/
transactions(filters, options) {
transactions(group, filters, options) {
const sortingOptions = { id: '_id' };

const getCollectionName = (transactionStatus = 'confirmed') => {
const collectionNames = {
confirmed: 'transactions',
unconfirmed: 'unconfirmedTransactions',
partial: 'partialTransactions'
};
return collectionNames[transactionStatus];
};
const collectionName = getCollectionName(filters.group);

const buildAccountConditions = () => {
if (filters.address)
return { 'meta.addresses': Buffer.from(filters.address) };
Expand Down Expand Up @@ -433,7 +430,7 @@ class CatapultDb {
const sortConditions = { $sort: { [sortingOptions[options.sortField]]: options.sortDirection } };
const conditions = buildConditions();

return this.queryPagedDocuments_2(conditions, removedFields, sortConditions, collectionName, options);
return this.queryPagedDocuments_2(conditions, removedFields, sortConditions, TransactionGroup[group], options);
}

transactionsByIdsImpl(collectionName, conditions) {
Expand All @@ -455,20 +452,12 @@ class CatapultDb {
})));
}

transactionsByIds(ids) {
return this.transactionsByIdsImpl('transactions', { _id: { $in: ids.map(id => new ObjectId(id)) } });
}

transactionsByHashes(hashes) {
return this.transactionsByIdsImpl('transactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } });
}

transactionsByHashesUnconfirmed(hashes) {
return this.transactionsByIdsImpl('unconfirmedTransactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } });
transactionsByIds(group, ids) {
return this.transactionsByIdsImpl(TransactionGroup[group], { _id: { $in: ids.map(id => new ObjectId(id)) } });
}

transactionsByHashesPartial(hashes) {
return this.transactionsByIdsImpl('partialTransactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } });
transactionsByHashes(group, hashes) {
return this.transactionsByIdsImpl(TransactionGroup[group], { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } });
}

/**
Expand Down
80 changes: 45 additions & 35 deletions rest/src/routes/transactionRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const routeResultTypes = require('./routeResultTypes');
const routeUtils = require('./routeUtils');
const errors = require('../server/errors');
const catapult = require('catapult-sdk');
const { NotFoundError } = require('restify-errors');


const { convert } = catapult.utils;
const { PacketType } = catapult.packet;
Expand All @@ -33,14 +35,7 @@ const constants = {
}
};

const parseHeight = params => routeUtils.parseArgument(params, 'height', 'uint');

const parseObjectId = str => {
if (!convert.isHexString(str))
throw Error('must be 12-byte hex string');

return str;
};
const isValidTransactionGroup = group => ['confirmed', 'unconfirmed', 'partial'].includes(group);

module.exports = {
register: (server, db, services) => {
Expand All @@ -53,51 +48,66 @@ module.exports = {
params => routeUtils.parseArgument(params, 'payload', convert.hexToUint8)
);

routeUtils.addGetPostDocumentRoutes(
server,
sender,
{ base: '/transactions', singular: 'transactionId', plural: 'transactionIds' },
// params has already been converted by a parser below, so it is: string - in case of objectId, Uint8Array - in case of hash
params => (('string' === typeof params[0]) ? db.transactionsByIds(params) : db.transactionsByHashes(params)),
(transactionId, index, array) => {
if (0 < index && array[0].length !== transactionId.length)
throw Error(`all ids must be homogeneous, element ${index}`);

if (constants.sizes.objectId === transactionId.length)
return parseObjectId(transactionId);
if (constants.sizes.hash === transactionId.length)
return convert.hexToUint8(transactionId);

throw Error(`invalid length of transaction id '${transactionId}'`);
}
);

server.get('/transactions', (req, res, next) => {
server.get('/transactions/:group', (req, res, next) => {
const { params } = req;

if (!isValidTransactionGroup(params.group))
return next(new NotFoundError());

if (params.address && (params.signerPublicKey || params.recipientAddress)) {
throw errors.createInvalidArgumentError(
'can\'t filter by address if signerPublicKey or recipientAddress are already provided'
);
}

if (params.group && !['confirmed', 'unconfirmed', 'partial'].includes(params.group))
throw errors.createInvalidArgumentError('invalid transaction group provided');

const filters = {
height: params.height ? parseHeight(params) : undefined,
height: params.height ? routeUtils.parseArgument(params, 'height', 'uint') : undefined,
address: params.address ? routeUtils.parseArgument(params, 'address', 'address') : undefined,
signerPublicKey: params.signerPublicKey ? routeUtils.parseArgument(params, 'signerPublicKey', 'publicKey') : undefined,
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,
group: params.group
embedded: params.embedded ? routeUtils.parseArgument(params, 'embedded', 'boolean') : undefined
};

const options = routeUtils.parsePaginationArguments(params, services.config.pageSize, { id: 'objectId' });

return db.transactions(filters, options)
return db.transactions(params.group, filters, options)
.then(result => routeUtils.createSender(routeResultTypes.transaction).sendPage(res, next)(result));
});

server.get('/transactions/:group/:transactionId', (req, res, next) => {
const { params } = req;

if (!isValidTransactionGroup(params.group))
return next(new NotFoundError());

let paramType = constants.sizes.objectId === params.transactionId.length ? 'id' : undefined;
paramType = constants.sizes.hash === params.transactionId.length ? 'hash' : paramType;
if (!paramType)
throw Error(`invalid length of transaction id '${params.transactionId}'`);

const transactionId = routeUtils.parseArgument(params, 'transactionId', 'id' === paramType ? 'objectId' : 'hash256');

const dbTransactionsRetriever = 'id' === paramType ? db.transactionsByIds : db.transactionsByHashes;
return dbTransactionsRetriever(params.group, [transactionId]).then(sender.sendOne(params.transactionId, res, next));
});

server.post('/transactions/:group', (req, res, next) => {
const { params } = req;

if (!isValidTransactionGroup(params.group))
return next(new NotFoundError());

if ((req.params.transactionIds && req.params.hashes) || (!params.transactionIds && !params.hashes))
throw errors.createInvalidArgumentError('either ids or hashes must be provided');

const transactionIds = params.transactionIds
? routeUtils.parseArgumentAsArray(params, 'transactionIds', 'objectId')
: routeUtils.parseArgumentAsArray(params, 'hashes', 'hash256');

const dbTransactionsRetriever = params.transactionIds ? db.transactionsByIds : db.transactionsByHashes;
return dbTransactionsRetriever(params.group, transactionIds)
.then(sender.sendArray(params.transactionIds || params.hashes, res, next));
});
}
};
4 changes: 1 addition & 3 deletions rest/src/routes/transactionStatusRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ module.exports = {
routeUtils.addGetPostDocumentRoutes(
server,
routeUtils.createSender(routeResultTypes.transactionStatus),
{
base: '/transactions', singular: 'hash', plural: 'hashes', postfixes: { singular: 'status', plural: 'statuses' }
},
{ base: '/transactionStatus', singular: 'hash', plural: 'hashes' },
params => dbFacade.transactionStatusesByHashes(db, params, services.config.transactionStates),
hash => {
if (2 * constants.sizes.hash256 === hash.length)
Expand Down
76 changes: 60 additions & 16 deletions rest/test/db/CatapultDb_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const DefaultPagingOptions = {
pageSizeDefault: 20
};

const TransactionGroups = {
confirmed: 'confirmed',
unconfirmed: 'unconfirmed',
partial: 'partial'
};

describe('catapult db', () => {
const deleteIds = dbEntities => {
test.collection.names.forEach(collectionName => {
Expand Down Expand Up @@ -823,28 +829,28 @@ describe('catapult db', () => {
addTestsWithId(traits, {
convertToId: test.db.createObjectId,
collectionName: 'transactions',
transactionsByIds: (db, ids) => db.transactionsByIds(ids)
transactionsByIds: (db, ids) => db.transactionsByIds(TransactionGroups.confirmed, ids)
}));

describe('by transaction hash', () =>
addTestsWithId(traits, {
convertToId: createTransactionHash,
collectionName: 'transactions',
transactionsByIds: (db, ids) => db.transactionsByHashes(ids)
transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.confirmed, ids)
}));

describe('by transaction hash (unconfirmed)', () =>
addTestsWithId(traits, {
convertToId: createTransactionHash,
collectionName: 'unconfirmedTransactions',
transactionsByIds: (db, ids) => db.transactionsByHashesUnconfirmed(ids)
transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.unconfirmed, ids)
}));

describe('by transaction hash (partial)', () =>
addTestsWithId(traits, {
convertToId: createTransactionHash,
collectionName: 'partialTransactions',
transactionsByIds: (db, ids) => db.transactionsByHashesPartial(ids)
transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.partial, ids)
}));
};

Expand Down Expand Up @@ -891,11 +897,49 @@ describe('catapult db', () => {
// Act + Assert:
return runTransactionsDbTest(
{ transactions: seedTransactions },
db => db.transactionsByIds([documentId]),
db => db.transactionsByIds(TransactionGroups.confirmed, [documentId]),
transactions => assertEqualDocuments([renameId(seedTransactions[4])], transactions)
);
});
});

describe('translates group to collection name', () => {
const validObjectId = test.db.createObjectId(10);
const validHash = '112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00';

const runTransactionsByIdTest = (dbCall, param, group, collection) => {
it(group, () => {
// Arrange:
const transactionsByIdsImplStub = sinon.stub(CatapultDb.prototype, 'transactionsByIdsImpl').returns('');
const db = new CatapultDb(Object.assign({ networkId: Mijin_Test_Network }, DefaultPagingOptions));

// Act
db[dbCall](group, [param]);

// Assert
expect(transactionsByIdsImplStub.calledOnce).to.equal(true);
expect(transactionsByIdsImplStub.firstCall.args[0]).to.equal(collection);
transactionsByIdsImplStub.restore();
});
};

const groupToCollectionName = {
confirmed: 'transactions',
unconfirmed: 'unconfirmedTransactions',
partial: 'partialTransactions'
};

describe('transactions by ids', () => {
Object.keys(groupToCollectionName).forEach(group => {
runTransactionsByIdTest('transactionsByIds', validObjectId, group, groupToCollectionName[group]);
});
});
describe('transactions by hashes', () => {
Object.keys(groupToCollectionName).forEach(group => {
runTransactionsByIdTest('transactionsByHashes', validHash, group, groupToCollectionName[group]);
});
});
});
});

describe('names by ids', () => {
Expand Down Expand Up @@ -1367,7 +1411,7 @@ describe('catapult db', () => {

return runDbTest(
{ transactions: dbTransactions },
db => db.transactions(filters, options),
db => db.transactions(TransactionGroups.confirmed, filters, options),
transactionsPage => {
const returnedIds = transactionsPage.data.map(t => t.id);
expect(transactionsPage.data.length).to.equal(expectedObjectIds.length);
Expand All @@ -1385,7 +1429,7 @@ describe('catapult db', () => {
// Act + Assert:
return runDbTest(
{ transactions: dbTransactions },
db => db.transactions({}, paginationOptions),
db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions),
page => {
const expected_keys = ['meta', 'transaction', 'id'];
expect(Object.keys(page.data[0]).sort()).to.deep.equal(expected_keys.sort());
Expand All @@ -1402,7 +1446,7 @@ describe('catapult db', () => {
// Act + Assert:
return runDbTest(
{ transactions: dbTransactions },
db => db.transactions({}, paginationOptions),
db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions),
transactionsPage => {
expect(transactionsPage.data[0].meta.addresses).to.equal(undefined);
}
Expand Down Expand Up @@ -1537,7 +1581,7 @@ describe('catapult db', () => {
// Act + Assert:
return runDbTest(
{ transactions: dbTransactions() },
db => db.transactions([], options),
db => db.transactions(TransactionGroups.confirmed, [], options),
transactionsPage => {
expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(10));
expect(transactionsPage.data[1].id).to.deep.equal(createObjectId(20));
Expand All @@ -1557,7 +1601,7 @@ describe('catapult db', () => {
// Act + Assert:
return runDbTest(
{ transactions: dbTransactions() },
db => db.transactions([], options),
db => db.transactions(TransactionGroups.confirmed, [], options),
transactionsPage => {
expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(30));
expect(transactionsPage.data[1].id).to.deep.equal(createObjectId(20));
Expand All @@ -1578,7 +1622,7 @@ describe('catapult db', () => {
// Act + Assert:
return runDbTest(
{ transactions: dbTransactions() },
db => db.transactions([], options),
db => db.transactions(TransactionGroups.confirmed, [], options),
() => {
expect(queryPagedDocumentsSpy.calledOnce).to.equal(true);
expect(Object.keys(queryPagedDocumentsSpy.firstCall.args[2].$sort)[0]).to.equal('_id');
Expand Down Expand Up @@ -1714,7 +1758,7 @@ describe('catapult db', () => {

return runDbTest(
dbTransactions(),
db => db.transactions({ group }, paginationOptions),
db => db.transactions(group, {}, paginationOptions),
transactionsPage => {
const returnedIds = transactionsPage.data.map(t => t.id);
expect(transactionsPage.data.length).to.equal(expectedObjectIds.length);
Expand All @@ -1724,15 +1768,15 @@ describe('catapult db', () => {
});
};

runGroupTest('confirmed', [10]);
runGroupTest('partial', [20]);
runGroupTest('unconfirmed', [30]);
runGroupTest(TransactionGroups.confirmed, [10]);
runGroupTest(TransactionGroups.partial, [20]);
runGroupTest(TransactionGroups.unconfirmed, [30]);

it('defaults to confirmed', () =>
// Act + Assert:
runDbTest(
dbTransactions(),
db => db.transactions({}, paginationOptions),
db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions),
transactionsPage => {
expect(transactionsPage.data.length).to.equal(1);
expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(10));
Expand Down
Loading

0 comments on commit 2241e8b

Please sign in to comment.