diff --git a/packages/api/index.js b/packages/api/index.js index 17bc3fb38..54fd62f16 100644 --- a/packages/api/index.js +++ b/packages/api/index.js @@ -10,6 +10,7 @@ const MainController = require("./src/controllers/MainController"); const TransactionsController = require("./src/controllers/TransactionsController"); const BlocksController = require("./src/controllers/BlocksController"); const DocumentsController = require("./src/controllers/DocumentsController"); +const IdentitiesController = require("./src/controllers/IdentitiesController"); const packageVersion = require('./package.json').version const Worker = require('./src/worker/index') const {BLOCK_TIME} = require("./src/constants"); @@ -74,8 +75,9 @@ const init = async () => { const transactionsController = new TransactionsController(client, knex) const dataContractsController = new DataContractsController(knex) const documentsController = new DocumentsController(knex) + const identitiesController = new IdentitiesController(knex) - Routes({fastify, mainController, blocksController, transactionsController, dataContractsController, documentsController}) + Routes({fastify, mainController, blocksController, transactionsController, dataContractsController, documentsController, identitiesController}) fastify.setErrorHandler(errorHandler) fastify.listen({ host: "0.0.0.0", port: 3005, listenTextResolver: (address) => console.log(`Platform indexer API has started on the ${address}`)}); diff --git a/packages/api/src/controllers/DocumentsController.js b/packages/api/src/controllers/DocumentsController.js index b481de6e2..038bf1be8 100644 --- a/packages/api/src/controllers/DocumentsController.js +++ b/packages/api/src/controllers/DocumentsController.js @@ -15,7 +15,7 @@ class DocumentsController { response.status(404).send({message: 'not found'}) } - response.send(Document.fromRow(document)); + response.send(document); } getDocumentsByDataContract = async (request, response) => { diff --git a/packages/api/src/controllers/IdentitiesController.js b/packages/api/src/controllers/IdentitiesController.js new file mode 100644 index 000000000..96275dbc7 --- /dev/null +++ b/packages/api/src/controllers/IdentitiesController.js @@ -0,0 +1,21 @@ +const IdentitiesDAO = require("../dao/IdentitiesDAO"); + +class IdentitiesController { + constructor(knex) { + this.identitiesDAO = new IdentitiesDAO(knex) + } + + getIdentityByIdentifier = async (request, response) => { + const {identifier} = request.params; + + const identity = await this.identitiesDAO.getIdentityByIdentifier(identifier) + + if (!identity) { + return response.status(404).send({message: 'not found'}) + } + + response.send(identity) + } +} + +module.exports = IdentitiesController diff --git a/packages/api/src/controllers/TransactionsController.js b/packages/api/src/controllers/TransactionsController.js index 32e8e9c75..975a148f6 100644 --- a/packages/api/src/controllers/TransactionsController.js +++ b/packages/api/src/controllers/TransactionsController.js @@ -1,5 +1,4 @@ const cache = require("../cache"); -const Transaction = require("../models/Transaction"); const TransactionsDAO = require("../dao/TransactionsDAO"); class TransactionsController { diff --git a/packages/api/src/dao/DataContractsDAO.js b/packages/api/src/dao/DataContractsDAO.js index f9f9ce144..696a3d961 100644 --- a/packages/api/src/dao/DataContractsDAO.js +++ b/packages/api/src/dao/DataContractsDAO.js @@ -12,11 +12,11 @@ module.exports = class DataContractsDAO { const subquery = this.knex('data_contracts') .select('data_contracts.id as id', 'data_contracts.identifier as identifier', - 'data_contracts.version as version') + 'data_contracts.version as version', 'data_contracts.state_transition_hash as tx_hash') .select(this.knex.raw(`rank() over (partition by identifier order by version desc) rank`)) const filteredContracts = this.knex.with('with_alias', subquery) - .select( 'id', 'identifier', 'version', 'rank', + .select( 'id', 'identifier', 'version', 'tx_hash', 'rank', this.knex('with_alias').count('*').as('total_count').where('rank', '1')) .select(this.knex.raw(`rank() over (order by id ${order}) row_number`)) .from('with_alias') @@ -25,9 +25,11 @@ module.exports = class DataContractsDAO { .as('filtered_data_contracts') const rows = await this.knex(filteredContracts) - .select('total_count', 'id', 'identifier', 'version', 'row_number') + .select('total_count', 'identifier', 'version', 'row_number', 'filtered_data_contracts.tx_hash', + 'blocks.timestamp as timestamp', 'blocks.hash as block_hash') + .join('state_transitions', 'state_transitions.hash', 'filtered_data_contracts.tx_hash') + .join('blocks', 'blocks.hash', 'state_transitions.block_hash') .whereBetween('row_number', [fromRank, toRank]) - .orderBy('id', order); const totalCount = rows.length > 0 ? Number(rows[0].total_count) : 0; @@ -38,9 +40,12 @@ module.exports = class DataContractsDAO { getDataContractByIdentifier = async (identifier) => { const rows = await this.knex('data_contracts') - .select('data_contracts.identifier as identifier', 'data_contracts.schema as schema', 'data_contracts.version as version') + .select('data_contracts.identifier as identifier', 'data_contracts.schema as schema', + 'data_contracts.version as version', 'state_transitions.hash as tx_hash', 'blocks.timestamp as timestamp') + .join('state_transitions', 'data_contracts.state_transition_hash', 'state_transitions.hash') + .join('blocks', 'blocks.hash', 'state_transitions.block_hash') .where('data_contracts.identifier', identifier) - .orderBy('id', 'desc') + .orderBy('data_contracts.id', 'desc') .limit(1); const [row] = rows diff --git a/packages/api/src/dao/DocumentsDAO.js b/packages/api/src/dao/DocumentsDAO.js index f875285c4..6fd51c6e7 100644 --- a/packages/api/src/dao/DocumentsDAO.js +++ b/packages/api/src/dao/DocumentsDAO.js @@ -8,9 +8,9 @@ module.exports = class DocumentsDAO { getDocumentByIdentifier = async (identifier) => { const subquery = this.knex('documents') - .select('documents.id as id', 'documents.identifier as identifier', - 'data_contracts.identifier as data_contract_identifier', 'documents.data as data', - 'documents.revision as revision', 'documents.state_transition_hash as state_transition_hash', + .select('documents.id', 'documents.identifier as identifier', + 'data_contracts.identifier as data_contract_identifier', 'documents.data as data_contract_data', + 'documents.revision as revision', 'documents.state_transition_hash as tx_hash', 'documents.deleted as deleted') .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.id desc) rank')) .leftJoin('data_contracts', 'data_contracts.id', 'documents.data_contract_id') @@ -18,8 +18,10 @@ module.exports = class DocumentsDAO { .as('documents') const rows = await this.knex(subquery) - .select('id', 'identifier', 'data_contract_identifier', 'data', - 'revision', 'deleted', 'rank', 'state_transition_hash') + .select('identifier', 'data_contract_identifier', 'data_contract_data', + 'revision', 'deleted', 'rank', 'tx_hash', 'blocks.timestamp as timestamp') + .join('state_transitions', 'state_transitions.hash', 'tx_hash') + .join('blocks', 'blocks.hash', 'state_transitions.block_hash') .limit(1); const [row] = rows @@ -28,7 +30,10 @@ module.exports = class DocumentsDAO { return null } - return Document.fromRow(row); + return Document.fromRow({ + ...row, + data: row.data_contract_data + }); } getDocumentsByDataContract = async (identifier, page, limit, order) => { @@ -37,8 +42,8 @@ module.exports = class DocumentsDAO { const subquery = this.knex('documents') .select('documents.id as id', 'documents.identifier as identifier', - 'data_contracts.identifier as data_contract_identifier', 'documents.data as data', - 'documents.revision as revision', 'documents.state_transition_hash as state_transition_hash', + 'data_contracts.identifier as data_contract_identifier', 'documents.data as document_data', + 'documents.revision as revision', 'documents.state_transition_hash as tx_hash', 'documents.deleted as deleted') .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.id desc) rank')) .leftJoin('data_contracts', 'data_contracts.id', 'documents.data_contract_id') @@ -46,7 +51,7 @@ module.exports = class DocumentsDAO { const filteredDocuments = this.knex.with('with_alias', subquery) .select('id', 'identifier', 'rank', 'revision', 'data_contract_identifier', - 'state_transition_hash', 'deleted', 'data', + 'tx_hash', 'deleted', 'document_data', this.knex('with_alias').count('*').as('total_count').where('rank', '1')) .select(this.knex.raw(`rank() over (order by id ${order}) row_number`)) .from('with_alias') @@ -55,13 +60,15 @@ module.exports = class DocumentsDAO { .as('documents') const rows = await this.knex(filteredDocuments) - .select('id', 'identifier', 'row_number', 'revision', 'data_contract_identifier', 'state_transition_hash', 'deleted', 'data', 'total_count') + .select('identifier', 'row_number', 'revision', 'data_contract_identifier', + 'tx_hash', 'deleted', 'document_data', 'total_count', 'blocks.timestamp as timestamp') .whereBetween('row_number', [fromRank, toRank]) - .orderBy('id', order); + .join('state_transitions', 'tx_hash', 'state_transitions.hash') + .join('blocks', 'blocks.hash', 'state_transitions.block_hash') const totalCount = rows.length > 0 ? Number(rows[0].total_count) : 0; - const resultSet = rows.map((row) => Document.fromRow(row)); + const resultSet = rows.map((row) => Document.fromRow({...row, data: row.document_data})); return new PaginatedResultSet(resultSet, page, limit, totalCount) } diff --git a/packages/api/src/dao/IdentitiesDAO.js b/packages/api/src/dao/IdentitiesDAO.js new file mode 100644 index 000000000..e8aaca28f --- /dev/null +++ b/packages/api/src/dao/IdentitiesDAO.js @@ -0,0 +1,67 @@ +const Identity = require("../models/Identity"); + +module.exports = class IdentitiesDAO { + constructor(knex) { + this.knex = knex; + } + + getIdentityByIdentifier = async (identifier) => { + const subquery = this.knex('identities') + .select('identities.id','identities.identifier as identifier', 'identities.state_transition_hash as tx_hash', + 'identities.revision as revision', 'identities.state_transition_hash as state_transition_hash') + .select(this.knex.raw('rank() over (partition by identities.identifier order by identities.id desc) rank')) + .where('identities.identifier', '=', identifier) + .as('all_identities') + + const lastRevisionIdentities = this.knex(subquery) + .select('identifier', 'revision', 'tx_hash', + 'transfers.id as transfer_id', 'transfers.sender as sender', + 'transfers.recipient as recipient', 'transfers.amount as amount') + .where('rank', 1) + .leftJoin('transfers', 'transfers.recipient', 'identifier') + + const documentsSubQuery = this.knex('documents') + .select('documents.id', 'documents.state_transition_hash', 'state_transitions.owner as owner') + .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.id desc) rank')) + .leftJoin('state_transitions', 'documents.state_transition_hash', 'state_transitions.hash') + .where('state_transitions.owner', identifier) + .as('as_documents') + + const dataContractsSubQuery = this.knex('data_contracts') + .select('data_contracts.id', 'data_contracts.state_transition_hash', 'state_transitions.owner as owner') + .select(this.knex.raw('rank() over (partition by data_contracts.identifier order by data_contracts.id desc) rank')) + .leftJoin('state_transitions', 'data_contracts.state_transition_hash', 'state_transitions.hash') + .where('state_transitions.owner', identifier) + .as('as_data_contracts') + + const transfersSubquery = this.knex('transfers') + .select('transfers.id as id', 'transfers.state_transition_hash as tx_hash') + .leftJoin('state_transitions', 'tx_hash', 'state_transitions.hash') + .where('state_transitions.owner', identifier) + + const rows = await this.knex.with('with_alias', lastRevisionIdentities) + .select('identifier', 'revision', + 'transfer_id', 'sender', 'tx_hash', 'blocks.timestamp as timestamp', + 'recipient', 'amount') + .join('state_transitions', 'state_transitions.hash', 'tx_hash') + .join('blocks', 'state_transitions.block_hash', 'blocks.hash') + .select(this.knex('with_alias').sum('amount').where('recipient', identifier).as('total_received')) + .select(this.knex('with_alias').sum('amount').where('sender', identifier).as('total_sent')) + .select(this.knex('state_transitions').count('*').where('owner', identifier).as('total_txs')) + .select(this.knex(documentsSubQuery).count('*').where('rank', 1).as('total_documents')) + .select(this.knex(dataContractsSubQuery).count('*').where('rank', 1).as('total_data_contracts')) + .select(this.knex.with('with_transfers', transfersSubquery).count('*').as('total_transfers')) + .from('with_alias') + .limit(1) + + if (!rows.length) { + return null + } + + const [row] = rows + + const balance = Number(row.total_received ?? 0) - Number(row.total_sent ?? 0) + + return Identity.fromRow({...row, balance}) + } +} diff --git a/packages/api/src/models/DataContract.js b/packages/api/src/models/DataContract.js index 34aa3b267..4d921ad6e 100644 --- a/packages/api/src/models/DataContract.js +++ b/packages/api/src/models/DataContract.js @@ -2,14 +2,18 @@ module.exports = class DataContract { identifier schema version + txHash + timestamp - constructor(identifier, schema, version) { + constructor(identifier, schema, version, txHash, timestamp) { this.identifier = identifier; this.schema = schema; this.version = version; + this.txHash = txHash; + this.timestamp = timestamp; } - static fromRow({identifier, schema, version}) { - return new DataContract(identifier, schema, version) + static fromRow({identifier, schema, version, tx_hash, timestamp}) { + return new DataContract(identifier, schema, version, tx_hash, timestamp) } } diff --git a/packages/api/src/models/Document.js b/packages/api/src/models/Document.js index fe2068677..9a4dabe3e 100644 --- a/packages/api/src/models/Document.js +++ b/packages/api/src/models/Document.js @@ -2,20 +2,23 @@ module.exports = class Document { identifier dataContractIdentifier revision - stateTransitionHash + txHash deleted data + timestamp - constructor(identifier, dataContractIdentifier, revision, stateTransitionHash, deleted, data) { + constructor(identifier, dataContractIdentifier, revision, txHash, deleted, data, timestamp) { this.identifier = identifier; this.dataContractIdentifier = dataContractIdentifier; this.revision = revision; - this.stateTransitionHash = stateTransitionHash; this.deleted = deleted; this.data = data; + this.txHash = txHash; + this.data = data; + this.timestamp = timestamp; } - static fromRow({identifier, data_contract_identifier, revision, state_transition_hash, deleted, data}) { - return new Document(identifier, data_contract_identifier, revision, state_transition_hash, deleted, data) + static fromRow({identifier, data_contract_identifier, revision, tx_hash, deleted, data, timestamp}) { + return new Document(identifier, data_contract_identifier, revision, tx_hash, deleted, data, timestamp) } } diff --git a/packages/api/src/models/Identity.js b/packages/api/src/models/Identity.js new file mode 100644 index 000000000..7bf464b73 --- /dev/null +++ b/packages/api/src/models/Identity.js @@ -0,0 +1,27 @@ +module.exports = class Identity { + identifier + revision + balance + timestamp + txHash + totalTxs + totalTransfers + totalDocuments + totalDataContracts + + constructor(identifier, revision, balance, timestamp, totalTxs, totalDataContracts, totalDocuments, totalTransfers, txHash) { + this.identifier = identifier; + this.revision = revision; + this.balance = balance; + this.timestamp = timestamp; + this.totalTxs = totalTxs; + this.totalDocuments = totalDocuments; + this.totalDataContracts = totalDataContracts; + this.totalTransfers = totalTransfers; + this.txHash = txHash + } + + static fromRow({identifier, revision, balance, timestamp, total_txs, total_data_contracts, total_documents, total_transfers, tx_hash}) { + return new Identity(identifier, revision, balance, timestamp, total_txs, total_data_contracts, total_documents, total_transfers, tx_hash) + } +} diff --git a/packages/api/src/routes.js b/packages/api/src/routes.js index 8a10afe25..d62c35774 100644 --- a/packages/api/src/routes.js +++ b/packages/api/src/routes.js @@ -5,7 +5,7 @@ * @param blockController {BlocksController} * @param transactionsController {TransactionsController} */ -module.exports = ({fastify, mainController, blocksController, transactionsController, dataContractsController, documentsController}) => { +module.exports = ({fastify, mainController, blocksController, transactionsController, dataContractsController, documentsController, identitiesController}) => { const routes = [ { path: '/status', @@ -52,6 +52,11 @@ module.exports = ({fastify, mainController, blocksController, transactionsContro method: 'GET', handler: documentsController.getDocumentByIdentifier }, + { + path: '/identities/:identifier', + method: 'GET', + handler: identitiesController.getIdentityByIdentifier + }, { path: '/search', method: 'GET',