diff --git a/packages/api/README.md b/packages/api/README.md index 1d1cd0641..6d62b67a6 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -63,10 +63,13 @@ Reference: * [Identities](#identities) * [Data Contracts by Identity](#data-contracts-by-identity) * [Documents by Identity](#documents-by-identity) +* [Document Transactions](#document-transactions) * [Transactions By Identity](#transactions-by-identity) * [Transfers by Identity](#transfers-by-identity) * [Transactions history](#transactions-history) * [Transactions gas history](#transactions-gas-history) +* [Contested Data Contracts](#contested-data-contracts) +* [Contested Documents](#contested-documents) * [Rate](#rate) * [Search](#search) * [Decode Raw Transaction](#decode-raw-transaction) @@ -211,6 +214,7 @@ GET /block/12E5592208322B5A3598C98C1811FCDD403DF40F522511D7A965DDE1D96C97C7 ### Blocks by validator Return all blocks proposed by the specific validators * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /validator/B8F90A4F07D9E59C061D41CC8E775093141492A5FD59AB3BBC4241238BB28A18/blocks @@ -240,6 +244,7 @@ GET /validator/B8F90A4F07D9E59C061D41CC8E775093141492A5FD59AB3BBC4241238BB28A18/ ### Blocks Return all blocks with pagination info * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /blocks?start_epoch_index=1000&end_epoch_index=1200&height_min=2000&height_max=4000&gas_min=1&gas_max=99999999999×tamp_start=2024-08-29T23:24:11.516z×tamp_end=2025-08-29T23:24:11.516z&tx_count_min=2&tx_count_max=11&validator=C11C1168DCF9479475CB1355855E30EA75C0CDDA8A8F9EA80591568DD1C33BA8 @@ -285,6 +290,7 @@ Return all validators with pagination info. * `lastProposedBlockHeader` field is nullable * `?isActive=true` boolean can be supplied in the query params to filter by isActive field * `limit` cannot be more then 100 (0 = all validators) +* `page` cannot be less then 1 ``` GET /validators @@ -605,6 +611,7 @@ Return transaction set paged Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string * `limit` cannot be more then 100 +* `page` cannot be less then 1 * `owner` Identity identifier * `status` can be `SUCCESS`, `FAIL` or `ALL` * `transaction_type` number of tx type. Can be set multiple times @@ -686,6 +693,7 @@ Return dataContracts set paged and order by block height or documents count. * Valid `order_by` values are `block_height` or `documents_count` * `name` field is nullable * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /dataContracts?page=1&limit=10&order=asc&order_by=block_height @@ -725,17 +733,68 @@ Allows to get withdrawals documents by contract id and document type GET /document/FUJsiMpQZWGfdrWPEUhBRExMAQB9q6MNfFgRqCdz42UJ?document_type_name=preorder&contract_id=GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec { - "identifier": "FUJsiMpQZWGfdrWPEUhBRExMAQB9q6MNfFgRqCdz42UJ", + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", "revision": 1, - "txHash": "DB51A0366133EC56098815D3BF66F374BD3E53951B415C5D4F487BAB6DAD271D", + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", "deleted": false, - "data": "{\"saltedDomainHash\":\"fFfVmDpvXdr5psoak2U5yeUntEEy3M9pAFHVIn+yEvU=\"}", - "timestamp": "2024-08-25T18:31:50.335Z", + "data": {"saltedDomainHash":"DcKS9AWVE1atKvIokA7JNdUNmyj4SbFUvB6e83whw2g="}, + "timestamp": "2024-12-27T14:31:00.798Z", "isSystem": false, + "entropy": "7beffbed25071ab26c0c7c50b3bab098f42126f2a91f9355f492a2d83beb74aa", + "prefundedVotingBalance": { + "parentNameAndLabel": 20000000000 + }, "typeName": "preorder", - "transitionType": "0", - "owner": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP" + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" +} +``` +Response codes: +``` +200: OK +404: Not found +500: Internal Server Error +``` +--- +### Document Transactions +Return transactions for selected document + +* Valid `order_by` values are `asc` or `desc` +* `limit` cannot be more then 100 +* `page` cannot be less then 1 + +``` +GET /document/ELEeNjGbqCsHNtkoJ51pFHvUyCk5sxgU1jYVuySMhQwN/transactions + +{ + "resultSet": [ + { + "revision": 1, + "gasUsed": 38201380, + "owner": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP", + "hash": "437C949982B41E00506B88C62A94B3E032FADFB010BCA91F3A4C874EB75F9E23", + "timestamp": "2024-08-25T18:32:24.454Z", + "transitionType": 0, + "data": { + "label": "BurgerJoint2", + "records": { + "identity": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP" + }, + "preorderSalt": "GV43pQXjfaSZ5FryGKsyKJBja2L+fwxXz9Npe0MH2WQ=", + "subdomainRules": { + "allowSubdomains": false + }, + "normalizedLabel": "burgerj01nt2", + "parentDomainName": "dash", + "normalizedParentDomainName": "dash" + } + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 1 + } } ``` Response codes: @@ -748,30 +807,32 @@ Response codes: ### Documents by Data Contract Return all documents by the given data contract identifier * `limit` cannot be more then 100 +* `page` cannot be less then 1 +* `document_type_name` optional ``` -GET /dataContract/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?page=1&limit=10&order=asc&type=preorder +GET /dataContract/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?document_type_name=domain&page=1&limit=10&order=desc { "resultSet": [ { - "identifier": "2qHj3sdxdKD4G7ZXGCuF6cB6ADYdtPs2BKNx24YcH6gW", + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", "revision": 1, - "txHash": "EDBC1D2AC12B8BE211086E3B8DD8018C5FB2FECBD46EB77205F9AF24E73BADAD", + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", "deleted": false, - "data": "{\"saltedDomainHash\":\"DcKS9AWVE1atKvIokA7JNdUNmyj4SbFUvB6e83whw2g=\"}", - "timestamp": "2024-08-25T09:11:05.698Z", + "data": "{\"label\":\"web\",\"records\":{\"identity\":\"8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6\"},\"preorderSalt\":\"HVKEY/12WglST1QCqxH9/yJsp8MMb+1GLc8xWw23PCI=\",\"subdomainRules\":{\"allowSubdomains\":false},\"normalizedLabel\":\"web\",\"parentDomainName\":\"dash\",\"normalizedParentDomainName\":\"dash\"}", + "timestamp": "2024-12-27T14:31:00.798Z", "isSystem": false, - "typeName": "preorder", - "transitionType": "0", - "owner": "BHAuKDRVPHkJd99pLoQh8dfjUFobwk5bq6enubEBKpsv" - }, - ... + "entropy": null, + "prefundedVotingBalance": null, + "typeName": "domain", + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" + }, ... ], "pagination": { "page": 1, "limit": 10, - "total": 750 + "total": 521 } } ``` @@ -784,7 +845,7 @@ Response codes: ### Identity by Identifier Return identity by given identifier ``` -GET /identity/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec +GET /identity/3igSMtXaaS9iRQHbWU1w4hHveKdxixwMpgmhLzjVhFZJ { "identifier": "3igSMtXaaS9iRQHbWU1w4hHveKdxixwMpgmhLzjVhFZJ", @@ -899,6 +960,7 @@ Return all identities paged and order by block height, tx count or balance. * Valid `order_by` values are `block_height`, `tx_count` or `balance` * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities?page=1&limit=10&order=asc&order_by=block_height @@ -945,6 +1007,7 @@ Return all withdrawals for identity _Note: this request does not contain any pagination data in the response_ * `limit` cannot be more then 100 +* `page` cannot be less then 1 * returns 404 `not found` if identity don't have withdrawals * Pagination always `null` ``` @@ -982,6 +1045,7 @@ Return all data contracts by the given identity * `name` field is nullable * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/dataContracts?page=1&limit=10&order=asc @@ -1015,30 +1079,33 @@ Response codes: ### Documents by Identity Return all documents by the given identity * `limit` cannot be more then 100 +* `page` cannot be less then 1 +* `document_type_name` document type name _optional_ ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?page=1&limit=10&order=asc&document_type_name=preorder { - pagination: { - page: 1, - limit: 10, - total: 10 - }, - resultSet: [ + "resultSet": [ { - identifier: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - owner: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - dataContractIdentifier: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - revision: 0, - txHash: "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", - deleted: false, - data: null, - timestamp: "2024-03-18T10:13:54.150Z", - isSystem: false, - transitionType: 0, - typeName: 'preorder' + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", + "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "revision": 1, + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", + "deleted": false, + "data": null, + "timestamp": "2024-12-27T14:31:00.798Z", + "isSystem": false, + "entropy": null, + "prefundedVotingBalance": null, + "typeName": "domain", + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" }, ... - ] + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 4 + } } ``` Response codes: @@ -1052,6 +1119,7 @@ Return all transactions made by the given identity Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/transactions?page=1&limit=10&order=asc @@ -1088,6 +1156,7 @@ Response codes: ### Transfers by Identity Return all transfers made by the given identity * `limit` cannot be more then 100 +* `page` cannot be less then 1 * `type` cannot be less, then 0 and more then 8 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/transfers?hash=445E6F081DEE877867816AD3EF492E2C0BD1DDCCDC9C793B23DDDAF8AEA23118&page=1&limit=10&order=asc&type=6 @@ -1188,6 +1257,7 @@ Response codes: 200: OK 500: Internal Server Error ``` +___ ### Transactions history Return a series data for the amount of transactions chart @@ -1222,6 +1292,7 @@ Response codes: 400: Invalid input, check start/end values 500: Internal Server Error ``` +___ ### Transactions Gas history Return a series data for the used gas of transactions chart @@ -1256,6 +1327,88 @@ Response codes: 400: Invalid input, check start/end values 500: Internal Server Error ``` +___ +### Contested Data Contracts +Return a series Data Contracts, which contains contested types + +* `limit` cannot be more then 100 +* `page` cannot be less then 1 +* Valid `order_by` values are `block_height` or `documents_count` + +``` +GET /contested/dataContracts?limit=10&page=1&order=asc&order_by=block_height +{ + "resultSet": [ + { + "identifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "name": "DPNS", + "owner": "11111111111111111111111111111111", + "schema": null, + "version": 0, + "txHash": null, + "timestamp": null, + "isSystem": true, + "documentsCount": 1413, + "contestedDocumentsCount": 231 + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 1 + } +} +``` +Response codes: +``` +200: OK +400: Invalid input, check start/end values +500: Internal Server Error +``` +___ +### Contested Documents +Return a series contested documents + +* `limit` cannot be more then 100 +* `page` cannot be less then 1 +``` +GET /contested/documents?limit=10&page=1&order=asc&document_type_name=domain +{ + "resultSet": [ + { + "identifier": "nLxqkMa9GRgfFGJpW9aE8ykYHxSR8Nat49cCNHXZR5a", + "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "revision": 1, + "txHash": "069422484BEEF731CFDBA601BDAC80BCF1FB009FACE7EE4897F3D4AE72EF85D8", + "deleted": false, + "data": "{\"label\":\"test111\",\"records\":{\"identity\":\"AC5EoXmLSpM6Q9S2BFMrfFFqmhCSiE5yKVdA3L5DkcjU\"},\"preorderSalt\":\"gIUpKYlk2J7wIxr6IWoPrtLW6+wNnaeKp4mfED6aLeI=\",\"subdomainRules\":{\"allowSubdomains\":false},\"normalizedLabel\":\"test111\",\"parentDomainName\":\"dash\",\"normalizedParentDomainName\":\"dash\"}", + "timestamp": "2024-08-26T22:14:06.680Z", + "system": false, + "entropy": null, + "prefundedVotingBalance": { + "parentNameAndLabel": 20000000000 + }, + "documentTypeName": "domain", + "transitionType": 0, + "nonce": null, + "owner": "AC5EoXmLSpM6Q9S2BFMrfFFqmhCSiE5yKVdA3L5DkcjU" + }, + ... + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 231 + } +} +``` +Response codes: +``` +200: OK +400: Invalid input, check start/end values +500: Internal Server Error +``` +___ ### Rate Return a rate DASH to USD ``` @@ -1271,6 +1424,7 @@ Response codes: 500: Internal Server Error 503: Service Temporarily Unavailable ``` +___ ### Decode Raw Transaction Return a decoded State Transition @@ -1289,7 +1443,7 @@ Available transactions type for decode | `MASTERNODE_VOTE` | 8 | - `fundingAddress` can be null -- `prefundedBalance` can be null +- `prefundedVotingBalance` can be null - `contractBounds` always null ``` @@ -1471,7 +1625,7 @@ POST /transaction/decode "allowSubdomains": false } }, - "prefundedBalance": { + "prefundedVotingBalance": { "parentNameAndLabel": 20000000000 } } diff --git a/packages/api/src/controllers/DocumentsController.js b/packages/api/src/controllers/DocumentsController.js index 8fb6c5c67..93958c46b 100644 --- a/packages/api/src/controllers/DocumentsController.js +++ b/packages/api/src/controllers/DocumentsController.js @@ -4,7 +4,7 @@ const Document = require('../models/Document') class DocumentsController { constructor (client, knex, dapi) { - this.documentsDAO = new DocumentsDAO(knex) + this.documentsDAO = new DocumentsDAO(knex, client) this.datacContractsDAO = new DataContractsDAO(knex) this.client = client this.dapi = dapi @@ -75,6 +75,15 @@ class DocumentsController { response.send(documents) } + + getDocumentRevisions = async (request, response) => { + const { identifier } = request.params + const { page = 1, limit = 10, order = 'asc' } = request.query + + const transactions = await this.documentsDAO.getDocumentRevisions(identifier, Number(page ?? 1), Number(limit ?? 10), order) + + response.send(transactions) + } } module.exports = DocumentsController diff --git a/packages/api/src/controllers/MainController.js b/packages/api/src/controllers/MainController.js index dfb61ae93..9edf8e27d 100644 --- a/packages/api/src/controllers/MainController.js +++ b/packages/api/src/controllers/MainController.js @@ -14,7 +14,7 @@ class MainController { constructor (knex, dapi, client) { this.blocksDAO = new BlocksDAO(knex, dapi) this.dataContractsDAO = new DataContractsDAO(knex) - this.documentsDAO = new DocumentsDAO(knex) + this.documentsDAO = new DocumentsDAO(knex, client) this.transactionsDAO = new TransactionsDAO(knex, dapi) this.identitiesDAO = new IdentitiesDAO(knex, dapi, client) this.validatorsDAO = new ValidatorsDAO(knex) diff --git a/packages/api/src/dao/DataContractsDAO.js b/packages/api/src/dao/DataContractsDAO.js index 17aeb5f5e..ee833e5bb 100644 --- a/packages/api/src/dao/DataContractsDAO.js +++ b/packages/api/src/dao/DataContractsDAO.js @@ -68,7 +68,7 @@ module.exports = class DataContractsDAO { 'data_contracts.version as version', 'state_transitions.hash as tx_hash', 'blocks.timestamp as timestamp') .select(this.knex('documents').count('*') .leftJoin('data_contracts', 'data_contracts.id', 'documents.data_contract_id') - .whereRaw(`data_contracts.identifier = '${identifier}'`) + .whereRaw('data_contracts.identifier = ? and revision = ?', [identifier, 1]) .as('documents_count')) .leftJoin('state_transitions', 'data_contracts.state_transition_hash', 'state_transitions.hash') .leftJoin('blocks', 'blocks.hash', 'state_transitions.block_hash') diff --git a/packages/api/src/dao/DocumentsDAO.js b/packages/api/src/dao/DocumentsDAO.js index cd8affe25..7a81dbd78 100644 --- a/packages/api/src/dao/DocumentsDAO.js +++ b/packages/api/src/dao/DocumentsDAO.js @@ -1,28 +1,50 @@ const Document = require('../models/Document') const PaginatedResultSet = require('../models/PaginatedResultSet') +const DocumentActionEnum = require('../enums/DocumentActionEnum') +const { decodeStateTransition } = require('../utils') module.exports = class DocumentsDAO { - constructor (knex) { + constructor (knex, client) { this.knex = knex + this.client = client } getDocumentByIdentifier = async (identifier) => { const subquery = this.knex('documents') - .select('documents.id', 'documents.identifier as identifier', 'documents.owner as document_owner', - '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', 'documents.is_system as is_system', 'document_type_name', - 'transition_type' - ) - .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.id desc) rank')) + .select('documents.id as id', 'documents.identifier as identifier', 'documents.owner as document_owner', + 'data_contracts.identifier as data_contract_identifier', 'documents.data as data', 'document_type_name', + 'documents.revision as revision', 'documents.state_transition_hash as tx_hash', 'state_transitions.data as tx_data', + 'documents.deleted as deleted', 'documents.is_system as is_system', 'documents.transition_type as transition_type') .leftJoin('data_contracts', 'data_contracts.id', 'documents.data_contract_id') + .leftJoin('state_transitions', 'hash', 'documents.state_transition_hash') .where('documents.identifier', '=', identifier) .as('documents') const rows = await this.knex(subquery) - .select('identifier', 'document_owner', 'data_contract_identifier', 'data_contract_data', - 'revision', 'deleted', 'rank', 'tx_hash', 'is_system', 'blocks.timestamp as timestamp', - 'document_type_name', 'transition_type') + .select('identifier', 'document_owner', 'data_contract_identifier', 'transition_type', + 'deleted', 'tx_hash', 'is_system', 'blocks.timestamp as timestamp', 'document_type_name') + .select( + this.knex(subquery) + .select('documents.data') + .orderBy('documents.id', 'desc') + .limit(1) + .as('data') + ) + .select( + this.knex(subquery) + .select('documents.revision') + .orderBy('documents.revision', 'desc') + .limit(1) + .as('revision') + ) + .select( + this.knex(subquery) + .select('documents.tx_data') + .where('documents.transition_type', DocumentActionEnum.Create) + .limit(1) + .as('create_tx_data') + ) + .orderBy('documents.id', 'desc') .leftJoin('state_transitions', 'state_transitions.hash', 'tx_hash') .leftJoin('blocks', 'blocks.hash', 'state_transitions.block_hash') .limit(1) @@ -33,34 +55,64 @@ module.exports = class DocumentsDAO { return null } - return Document.fromRow({ + let transitions = [] + + if (row.create_tx_data) { + const decodedTransitions = await decodeStateTransition(this.client, row.create_tx_data) + + transitions = decodedTransitions.transitions ?? [] + } + + const [transitionWithEntropy] = transitions?.filter(transition => transition.entropy !== '') + + const document = Document.fromRow({ ...row, - data: row.data_contract_data, owner: row.document_owner }) + + return Document.fromObject({ + ...document, + entropy: transitionWithEntropy?.entropy, + prefundedVotingBalance: transitionWithEntropy?.prefundedVotingBalance, + nonce: transitionWithEntropy?.nonce + }) } getDocumentsByDataContract = async (identifier, typeName, page, limit, order) => { const fromRank = ((page - 1) * limit) + 1 const toRank = fromRank + limit - 1 - const typeQuery = typeName - ? `document_type_name = '${typeName}'` - : 'true' + let typeQuery = 'data_contracts.identifier = ?' + const queryBindings = [identifier] + + if (typeName) { + typeQuery = typeQuery + ' and document_type_name = ?' + queryBindings.push(typeName) + } + + const dataSubquery = this.knex('documents') + .select('documents.data as data', 'documents.identifier as identifier') + .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.revision desc) rank')) + .orderBy('documents.id', 'desc') + .as('documents_data') + + const filterDataSubquery = this.knex(dataSubquery) + .select('data', 'identifier') + .where('documents_data.rank', '1') + .as('documents_data') const subquery = this.knex('documents') .select('documents.id as id', 'documents.identifier as identifier', 'documents.owner as document_owner', - 'data_contracts.identifier as data_contract_identifier', 'documents.data as document_data', + 'data_contracts.identifier as data_contract_identifier', 'document_type_name', 'documents.data as document_data', 'documents.revision as revision', 'documents.state_transition_hash as tx_hash', - 'documents.deleted as deleted', 'documents.is_system as is_system', 'document_type_name', 'transition_type') + 'documents.deleted as deleted', 'documents.is_system as is_system', 'transition_type', 'prefunded_voting_balance') .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') - .where('data_contracts.identifier', identifier) - .andWhereRaw(typeQuery) + .whereRaw(typeQuery, queryBindings) const filteredDocuments = this.knex.with('with_alias', subquery) - .select('id', 'identifier', 'document_owner', 'rank', 'revision', 'data_contract_identifier', - 'tx_hash', 'deleted', 'is_system', 'document_data', 'document_type_name', 'transition_type', + .select('id', 'with_alias.identifier as identifier', 'document_owner', 'rank', 'revision', 'data_contract_identifier', + 'tx_hash', 'deleted', 'is_system', 'document_data', 'document_type_name', 'transition_type', 'prefunded_voting_balance', 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') @@ -68,11 +120,12 @@ module.exports = class DocumentsDAO { .as('documents') const rows = await this.knex(filteredDocuments) - .select('documents.id as id', 'identifier', 'document_owner', 'row_number', 'revision', 'data_contract_identifier', - 'tx_hash', 'deleted', 'document_data', 'total_count', 'is_system', 'blocks.timestamp as timestamp', + .select('documents.id as id', 'documents.identifier as identifier', 'document_owner', 'row_number', 'revision', 'data_contract_identifier', + 'tx_hash', 'deleted', 'document_data', 'total_count', 'is_system', 'blocks.timestamp as timestamp', 'prefunded_voting_balance', 'document_type_name', 'transition_type') .whereBetween('row_number', [fromRank, toRank]) .leftJoin('state_transitions', 'tx_hash', 'state_transitions.hash') + .leftJoin(filterDataSubquery, 'documents.identifier', '=', 'documents_data.identifier') .leftJoin('blocks', 'blocks.hash', 'state_transitions.block_hash') .orderBy('id', order) @@ -86,4 +139,31 @@ module.exports = class DocumentsDAO { return new PaginatedResultSet(resultSet, page, limit, totalCount) } + + getDocumentRevisions = async (identifier, page, limit, order) => { + const fromRank = ((page - 1) * limit) + 1 + const toRank = fromRank + limit - 1 + + const subquery = this.knex('documents') + .select( + 'documents.id as id', 'revision', 'transition_type', 'gas_used', 'timestamp', + 'documents.owner as owner', 'state_transitions.hash as hash', 'documents.data as data') + .select(this.knex.raw(`rank() over (order by state_transitions.id ${order}) rank`)) + .where('documents.identifier', '=', identifier) + .leftJoin('state_transitions', 'state_transition_hash', 'state_transitions.hash') + .leftJoin('blocks', 'state_transitions.block_hash', 'blocks.hash') + .as('subquery') + + const rows = await this.knex(subquery) + .select('revision', 'gas_used', 'subquery.owner', 'hash as tx_hash', 'timestamp', 'transition_type', 'data') + .select(this.knex(subquery).count('*').as('total_count')) + .whereBetween('rank', [fromRank, toRank]) + .orderBy('id', order) + + const [row] = rows + + const totalCount = row?.total_count + + return new PaginatedResultSet(rows.map(Document.fromRow), page, limit, Number(totalCount)) + } } diff --git a/packages/api/src/dao/IdentitiesDAO.js b/packages/api/src/dao/IdentitiesDAO.js index 9b33b01d2..03a4df002 100644 --- a/packages/api/src/dao/IdentitiesDAO.js +++ b/packages/api/src/dao/IdentitiesDAO.js @@ -83,7 +83,7 @@ module.exports = class IdentitiesDAO { .limit(1) const statisticSubquery = this.knex('state_transitions') - .whereRaw(`owner = identifier and type=${IDENTITY_CREDIT_WITHDRAWAL}`) + .whereRaw('owner = identifier and type = ?', [IDENTITY_CREDIT_WITHDRAWAL]) .as('statistic') const rows = await this.knex.with('with_alias', mainQuery) @@ -124,12 +124,12 @@ module.exports = class IdentitiesDAO { .select( this.knex(statisticSubquery) .count('id') - .whereRaw(`type=${IDENTITY_CREDIT_WITHDRAWAL}`) + .whereRaw('type=?', [IDENTITY_CREDIT_WITHDRAWAL]) .as('total_withdrawals')) .select( this.knex(statisticSubquery) .count('id') - .whereRaw(`type=${IDENTITY_TOP_UP}`) + .whereRaw('type=?', [IDENTITY_TOP_UP]) .as('total_top_ups')) .from('with_alias') @@ -363,7 +363,7 @@ module.exports = class IdentitiesDAO { 'documents.revision as revision', 'documents.state_transition_hash as tx_hash', 'documents.deleted as deleted', 'documents.is_system as document_is_system', 'document_type_name', 'transition_type') .select(this.knex.raw('rank() over (partition by documents.identifier order by documents.id desc) rank')) - .andWhereRaw(typeQuery, queryBindings) + .whereRaw(typeQuery, queryBindings) const filteredDocuments = this.knex.with('with_alias', subquery) .select('with_alias.id as document_id', 'identifier', 'document_owner', 'revision', 'data_contract_id', @@ -423,14 +423,17 @@ module.exports = class IdentitiesDAO { const fromRank = (page - 1) * limit + 1 const toRank = fromRank + limit - 1 - let searchQuery = `(transfers.sender = '${identifier}' OR transfers.recipient = '${identifier}')` + let searchQuery = '(transfers.sender = ? OR transfers.recipient = ?)' + const searchBingings = [identifier, identifier] if (typeof type === 'number') { - searchQuery = searchQuery + ` AND state_transitions.type = ${type}` + searchQuery = searchQuery + ' AND state_transitions.type = ?' + searchBingings.push(type) } if (hash) { - searchQuery = searchQuery + ` AND state_transitions.hash = '${hash}'` + searchQuery = searchQuery + ' AND state_transitions.hash = ?' + searchBingings.push(hash) } const subquery = this.knex('transfers') @@ -443,7 +446,7 @@ module.exports = class IdentitiesDAO { 'state_transitions.gas_used as gas_used' ) .select(this.knex.raw(`rank() over (order by transfers.id ${order}) rank`)) - .whereRaw(searchQuery) + .whereRaw(searchQuery, searchBingings) .leftJoin('state_transitions', 'state_transitions.hash', 'transfers.state_transition_hash') const rows = await this.knex.with('with_alias', subquery) diff --git a/packages/api/src/models/Document.js b/packages/api/src/models/Document.js index 449177de0..7bced0b2b 100644 --- a/packages/api/src/models/Document.js +++ b/packages/api/src/models/Document.js @@ -7,30 +7,37 @@ module.exports = class Document { data timestamp system - typeName + entropy + prefundedVotingBalance + documentTypeName transitionType + nonce - constructor (identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, isSystem, typeName, transitionType) { + constructor (identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, isSystem, documentTypeName, transitionType, prefundedVotingBalance, entropy, nonce) { this.identifier = identifier ? identifier.trim() : null this.owner = owner ? owner.trim() : null this.dataContractIdentifier = dataContractIdentifier ? dataContractIdentifier.trim() : null this.revision = revision ?? null this.deleted = deleted ?? null + this.data = data ?? null this.txHash = txHash ?? null this.data = data ?? null this.timestamp = timestamp ?? null this.system = isSystem ?? null - this.typeName = typeName ?? null + this.documentTypeName = documentTypeName ?? null this.transitionType = transitionType ?? null + this.entropy = entropy ?? null + this.prefundedVotingBalance = prefundedVotingBalance ?? null + this.nonce = nonce ?? null } // eslint-disable-next-line camelcase - static fromRow ({ identifier, owner, data_contract_identifier, revision, tx_hash, deleted, data, timestamp, is_system, document_type_name, transition_type }) { - return new Document(identifier, owner, data_contract_identifier, revision, tx_hash, deleted, data ? JSON.stringify(data) : null, timestamp, is_system, document_type_name, Number(transition_type)) + static fromRow ({ identifier, owner, data_contract_identifier, revision, tx_hash, deleted, data, timestamp, is_system, document_type_name, transition_type, prefunded_voting_balance }) { + return new Document(identifier, owner, data_contract_identifier, revision, tx_hash, deleted, data ? JSON.stringify(data) : null, timestamp, is_system, document_type_name, Number(transition_type), prefunded_voting_balance) } - static fromObject ({ identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, system, typeName, transitionType }) { - return new Document(identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, system, typeName, transitionType) + static fromObject ({ identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, system, documentTypeName, transitionType, entropy, prefundedVotingBalance, nonce }) { + return new Document(identifier, owner, dataContractIdentifier, revision, txHash, deleted, data, timestamp, system, documentTypeName, transitionType, prefundedVotingBalance, entropy, nonce) } } diff --git a/packages/api/src/routes.js b/packages/api/src/routes.js index 3e4a71ba9..c4cff9d38 100644 --- a/packages/api/src/routes.js +++ b/packages/api/src/routes.js @@ -154,6 +154,19 @@ module.exports = ({ } } }, + { + path: '/document/:identifier/revisions', + method: 'GET', + handler: documentsController.getDocumentRevisions, + schema: { + params: { + type: 'object', + properties: { + identifier: { $ref: 'identifier#' } + } + } + } + }, { path: '/identities', method: 'GET', diff --git a/packages/api/src/utils.js b/packages/api/src/utils.js index eaff1a6f1..0465e1fd1 100644 --- a/packages/api/src/utils.js +++ b/packages/api/src/utils.js @@ -79,14 +79,14 @@ const decodeStateTransition = async (client, base64) => { switch (documentTransition.getAction()) { case DocumentActionEnum.Create: { - const prefundedBalance = documentTransition.getPrefundedVotingBalance() + const prefundedVotingBalance = documentTransition.getPrefundedVotingBalance() out.entropy = Buffer.from(documentTransition.getEntropy()).toString('hex') out.data = documentTransition.getData() - out.prefundedBalance = prefundedBalance + out.prefundedVotingBalance = prefundedVotingBalance ? Object.fromEntries( - Object.entries(prefundedBalance) + Object.entries(prefundedVotingBalance) .map(prefund => [prefund[0], Number(prefund[1])]) ) : null diff --git a/packages/api/test/integration/data.contracts.spec.js b/packages/api/test/integration/data.contracts.spec.js index c8574f8ee..ed5df9643 100644 --- a/packages/api/test/integration/data.contracts.spec.js +++ b/packages/api/test/integration/data.contracts.spec.js @@ -89,10 +89,13 @@ describe('DataContracts routes', () => { documents: dataContracts[dataContracts.length - 1].dataContract.documents }) const document = await fixtures.document(knex, { - data_contract_id: dataContract.id, owner: identity.identifier, is_system: true + data_contract_id: dataContract.id, + owner: identity.identifier, + is_system: true, + prefunded_voting_balance: i % 2 === 0 ? {} : undefined }) dataContract.documents.push(document) - documents.push({ transaction: null, block: null, dataContract, document }) + documents.push({ transaction: contractCreateTransaction, block: block2, dataContract, document }) dataContracts[dataContracts.length - 1].transaction = contractCreateTransaction dataContracts[dataContracts.length - 1].dataContract = dataContract } @@ -290,19 +293,6 @@ describe('DataContracts routes', () => { assert.deepEqual(body, expectedDataContract) }) - // - // it('should return last revision of data contract by identifier', async () => { - // const {body} = await client.get('/dataContract/Gc7HqRGqmA4ZSafQ6zXeKH8Rh4AjNjjWsztotJDLpMXa') - // .expect(200) - // .expect('Content-Type', 'application/json; charset=utf-8'); - // - // assert.equal(body.identifier, 'Gc7HqRGqmA4ZSafQ6zXeKH8Rh4AjNjjWsztotJDLpMXa') - // assert.equal(body.txHash, '4107CE20DB3BE2B2A3B3F3ABA9F68438428E734E4ACF39D4F6D03B0F9B187829') - // assert.equal(body.owner, 'FRMXvU2vRqk9xTya3MTB58ieBt27izpPyoX3fVLf3HuA') - // assert.equal(body.version, 2) - // assert.equal(body.timestamp, '2024-02-22T14:23:57.592Z') - // }); - it('should return 404 if data contract not found', async () => { await client.get('/dataContract/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec') .expect(404) diff --git a/packages/api/test/integration/documents.spec.js b/packages/api/test/integration/documents.spec.js index 8062872e3..bc710bd0b 100644 --- a/packages/api/test/integration/documents.spec.js +++ b/packages/api/test/integration/documents.spec.js @@ -88,25 +88,7 @@ describe('Documents routes', () => { document_type_name: 'note', data: { type: 'note', - identifier: '7TsrNHXDy14fYoRcoYjZHH14K4riMGU2VeHMwopG82DL', - dataContractObject: { - $format_version: '0', - ownerId: dataContract.owner, - id: dataContract.identifier, - version: 0, - documentSchemas: { - note: { - type: 'object', - properties: { - message: { - type: 'string', - position: 0 - } - }, - additionalProperties: false - } - } - } + identifier: '7TsrNHXDy14fYoRcoYjZHH14K4riMGU2VeHMwopG82DL' }, identifier: '7TsrNHXDy14fYoRcoYjZHH14K4riMGU2VeHMwopG82DL' }) @@ -128,7 +110,8 @@ describe('Documents routes', () => { const documentTransaction = await fixtures.transaction(knex, { block_hash: block.hash, type: StateTransitionEnum.DOCUMENTS_BATCH, - owner: identity.identifier + owner: identity.identifier, + data: 'AgAOCeQUD4t3d4EL5WxH8KtcvZvtHnc6vZ+f3y/memaf9wEAAABgCLhdmCbncK0httWF8BDx37Oz8q3GSSMpu++P3sGx1wIEbm90ZdpXZPiQJeml9oBjOQnbWPb39tNYLERTk/FarViCHJ8r8Jo86sqi8SuYeboiPVuMZsMQbv5Y7cURVW8x7pZ2QSsBB21lc3NhZ2USMFR1dG9yaWFsIENJIFRlc3QgQCBUaHUsIDA4IEF1ZyAyMDI0IDIwOjI1OjAzIEdNVAAAAUEfLtRrTrHXdpT9Pzp4PcNiKV13nnAYAqrl0w3KfWI8QR5f7TTen0N66ZUU7R7AoXV8kliIwVqpxiCVwChbh2XiYQ==' }) const document = await fixtures.document(knex, { data_contract_id: dataContract.id, @@ -138,6 +121,23 @@ describe('Documents routes', () => { documents.push({ transaction: documentTransaction, block, dataContract, document }) } + + for (let i = 5; i < 30; i++) { + const documentTransaction = await fixtures.transaction(knex, { + block_hash: block.hash, + type: StateTransitionEnum.DOCUMENTS_BATCH, + owner: identity.identifier, + data: 'AgAOCeQUD4t3d4EL5WxH8KtcvZvtHnc6vZ+f3y/memaf9wEAAABgCLhdmCbncK0httWF8BDx37Oz8q3GSSMpu++P3sGx1wIEbm90ZdpXZPiQJeml9oBjOQnbWPb39tNYLERTk/FarViCHJ8r8Jo86sqi8SuYeboiPVuMZsMQbv5Y7cURVW8x7pZ2QSsBB21lc3NhZ2USMFR1dG9yaWFsIENJIFRlc3QgQCBUaHUsIDA4IEF1ZyAyMDI0IDIwOjI1OjAzIEdNVAAAAUEfLtRrTrHXdpT9Pzp4PcNiKV13nnAYAqrl0w3KfWI8QR5f7TTen0N66ZUU7R7AoXV8kliIwVqpxiCVwChbh2XiYQ==' + }) + const document = await fixtures.document(knex, { + data_contract_id: dataContract.id, + state_transition_hash: documentTransaction.hash, + owner: identity.identifier, + document_type_name: 'test' + }) + + documents.push({ transaction: documentTransaction, block, dataContract, document }) + } }) after(async () => { @@ -157,37 +157,46 @@ describe('Documents routes', () => { // so we'll verify the data we're sending to DAPI. const expectedDocument = { dataContractIdentifier: document.dataContract.identifier, - deleted: false, + identifier: document.document.identifier, + revision: 1, + txHash: document.transaction.hash, + deleted: document.document.deleted, + data: JSON.stringify(document.document.data), + timestamp: document.block.timestamp, + entropy: 'f09a3ceacaa2f12b9879ba223d5b8c66c3106efe58edc511556f31ee9676412b', + documentTypeName: document.document.document_type_name, + prefundedVotingBalance: null, + owner: document.document.owner, + system: document.document.is_system, + nonce: 2, + transitionType: 0 + } + + assert.deepEqual(body, expectedDocument) + }) + + it('should return system document', async () => { + const [document] = documents.filter(e => e.document.is_system) + + const { body } = await client.get(`/document/${document.document.identifier}`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + + const expectedDocument = { + dataContractIdentifier: document.dataContract.identifier, identifier: document.document.identifier, system: true, + revision: 1, + deleted: document.document.deleted, + data: JSON.stringify(document.document.data), + timestamp: new Date(0).toISOString(), + entropy: 'f09a3ceacaa2f12b9879ba223d5b8c66c3106efe58edc511556f31ee9676412b', + documentTypeName: document.document.document_type_name, + prefundedVotingBalance: null, owner: document.document.owner, - revision: 0, - timestamp: '1970-01-01T00:00:00.000Z', txHash: document.transaction.hash, - typeName: 'note', - transitionType: 0, - data: JSON.stringify({ - type: 'note', - identifier: '7TsrNHXDy14fYoRcoYjZHH14K4riMGU2VeHMwopG82DL', - dataContractObject: { - id: document.dataContract.identifier, - ownerId: document.dataContract.owner, - version: 0, - $format_version: '0', - documentSchemas: { - note: { - type: 'object', - properties: { - message: { - type: 'string', - position: 0 - } - }, - additionalProperties: false - } - } - } - }) + nonce: 2, + transitionType: 0 } assert.deepEqual(body, expectedDocument) @@ -201,6 +210,35 @@ describe('Documents routes', () => { }) }) + describe('getDocumentRevisions()', async () => { + it('should return document transactions by identifier', async () => { + const [document] = documents.filter(e => !e.document.is_system) + + const { body } = await client.get(`/document/${document.document.identifier}/revisions`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + + const expectedDocument = { + revision: document.document.revision, + owner: document.transaction.owner, + txHash: document.transaction.hash, + timestamp: document.block.timestamp, + transitionType: document.document.transition_type, + data: '{}', + dataContractIdentifier: null, + deleted: null, + documentTypeName: null, + entropy: null, + identifier: null, + nonce: null, + prefundedVotingBalance: null, + system: null + } + + assert.deepEqual(body.resultSet, [expectedDocument]) + }) + }) + describe('getDocumentsByDataContract()', async () => { it('should return default set of documents', async () => { const { body } = await client.get(`/dataContract/${dataContract.identifier}/documents`) @@ -208,11 +246,44 @@ describe('Documents routes', () => { .expect('Content-Type', 'application/json; charset=utf-8') assert.equal(body.resultSet.length, 10) - assert.equal(body.pagination.total, 30) + assert.equal(body.pagination.total, 55) assert.equal(body.pagination.page, 1) assert.equal(body.pagination.limit, 10) const expectedDocuments = documents + .slice(0, 10) + .map(({ block, document, dataContract, transaction }) => ({ + identifier: document.identifier, + dataContractIdentifier: dataContract.identifier, + revision: document.revision, + txHash: transaction.hash ?? null, + deleted: document.deleted, + data: JSON.stringify(document.data), + timestamp: block.timestamp, + owner: document.owner, + system: document.is_system, + entropy: null, + documentTypeName: document.document_type_name, + transitionType: document.transition_type, + prefundedVotingBalance: null, + nonce: null + })) + + assert.deepEqual(body.resultSet, expectedDocuments) + }) + + it('should return default set of documents by type_name', async () => { + const { body } = await client.get(`/dataContract/${dataContract.identifier}/documents?document_type_name=test`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + + assert.equal(body.resultSet.length, 10) + assert.equal(body.pagination.total, 25) + assert.equal(body.pagination.page, 1) + assert.equal(body.pagination.limit, 10) + + const expectedDocuments = documents + .filter(({ document }) => document.document_type_name === 'test') .slice(0, 10) .map(({ block, document, dataContract, transaction }) => ({ identifier: document.identifier, @@ -234,10 +305,13 @@ describe('Documents routes', () => { } : {}), transitionType: 0, - typeName: document.document_type_name, + entropy: null, + prefundedVotingBalance: null, + documentTypeName: document.document_type_name, timestamp: block.timestamp, owner: document.owner, - system: document.is_system + system: document.is_system, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -261,24 +335,15 @@ describe('Documents routes', () => { revision: document.revision, txHash: transaction.hash, deleted: document.deleted, - data: JSON.stringify(document.data?.dataContractObject - ? { - type: document.data.type, - identifier: document.data.identifier, - dataContractObject: { - id: document.data?.dataContractObject?.id ?? null, - ownerId: document.data?.dataContractObject?.ownerId ?? null, - version: document.data?.dataContractObject?.version ?? null, - $format_version: document.data?.dataContractObject?.$format_version ?? null, - documentSchemas: document.data?.dataContractObject?.documentSchemas ?? null - } - } - : {}), + data: JSON.stringify(document.data), transitionType: 0, - typeName: document.document_type_name, + documentTypeName: document.document_type_name, timestamp: block.timestamp, owner: document.owner, - system: document.is_system + system: document.is_system, + entropy: null, + prefundedVotingBalance: null, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -290,7 +355,7 @@ describe('Documents routes', () => { .expect('Content-Type', 'application/json; charset=utf-8') assert.equal(body.resultSet.length, 10) - assert.equal(body.pagination.total, 30) + assert.equal(body.pagination.total, 55) assert.equal(body.pagination.page, 1) assert.equal(body.pagination.limit, 10) @@ -317,10 +382,13 @@ describe('Documents routes', () => { } : {}), transitionType: 0, - typeName: 'type_name', + documentTypeName: document.document_type_name, timestamp: document.is_system ? null : block.timestamp, owner: document.owner, - system: document.is_system + system: document.is_system, + entropy: null, + prefundedVotingBalance: null, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -354,10 +422,13 @@ describe('Documents routes', () => { } : {}), transitionType: 0, - typeName: 'type_name', + documentTypeName: 'type_name', timestamp: block.timestamp, owner: document.owner, - system: document.is_system + system: document.is_system, + entropy: null, + prefundedVotingBalance: null, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -391,10 +462,13 @@ describe('Documents routes', () => { } : {}), transitionType: 0, - typeName: 'type_name', + documentTypeName: document.document_type_name, timestamp: document.is_system ? null : block.timestamp, owner: document.owner, - system: document.is_system + system: document.is_system, + entropy: null, + prefundedVotingBalance: null, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) diff --git a/packages/api/test/integration/identities.spec.js b/packages/api/test/integration/identities.spec.js index a62cadcd5..f4205764c 100644 --- a/packages/api/test/integration/identities.spec.js +++ b/packages/api/test/integration/identities.spec.js @@ -963,14 +963,17 @@ describe('Identities routes', () => { identifier: _document.document.identifier, owner: identity.identifier, dataContractIdentifier: _document.dataContract.identifier, - revision: 0, + revision: 1, txHash: _document.transaction.hash, deleted: false, data: null, + entropy: null, + prefundedVotingBalance: null, + documentTypeName: 'type_name', timestamp: _document.block.timestamp.toISOString(), system: false, transitionType: 0, - typeName: 'type_name' + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -1021,14 +1024,17 @@ describe('Identities routes', () => { identifier: _document.document.identifier, owner: identity.identifier, dataContractIdentifier: _document.dataContract.identifier, - revision: 0, + revision: 1, txHash: _document.transaction.hash, deleted: false, data: null, + entropy: null, + prefundedVotingBalance: null, + documentTypeName: 'type_name', timestamp: _document.block.timestamp.toISOString(), system: false, transitionType: 0, - typeName: 'type_name' + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -1081,14 +1087,17 @@ describe('Identities routes', () => { identifier: _document.document.identifier, owner: identity.identifier, dataContractIdentifier: _document.dataContract.identifier, - revision: 0, + revision: 1, txHash: _document.transaction.hash, deleted: false, data: null, timestamp: _document.block.timestamp.toISOString(), system: false, transitionType: 0, - typeName: 'my_type' + documentTypeName: 'my_type', + prefundedVotingBalance: null, + entropy: null, + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -1139,14 +1148,17 @@ describe('Identities routes', () => { identifier: _document.document.identifier, owner: identity.identifier, dataContractIdentifier: _document.dataContract.identifier, - revision: 0, + revision: 1, txHash: _document.transaction.hash, deleted: false, data: null, + entropy: null, + prefundedVotingBalance: null, + documentTypeName: 'type_name', timestamp: _document.block.timestamp.toISOString(), system: false, transitionType: 0, - typeName: 'type_name' + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) @@ -1197,14 +1209,17 @@ describe('Identities routes', () => { identifier: _document.document.identifier, owner: identity.identifier, dataContractIdentifier: _document.dataContract.identifier, - revision: 0, + revision: 1, txHash: _document.transaction.hash, deleted: false, data: null, + entropy: null, + prefundedVotingBalance: null, + documentTypeName: 'type_name', timestamp: _document.block.timestamp.toISOString(), system: false, transitionType: 0, - typeName: 'type_name' + nonce: null })) assert.deepEqual(body.resultSet, expectedDocuments) diff --git a/packages/api/test/integration/main.spec.js b/packages/api/test/integration/main.spec.js index 78c12803b..aded49649 100644 --- a/packages/api/test/integration/main.spec.js +++ b/packages/api/test/integration/main.spec.js @@ -25,6 +25,7 @@ describe('Other routes', () => { let dataContractTransaction let dataContract let documentTransaction + let document let transactions before(async () => { @@ -104,9 +105,10 @@ describe('Other routes', () => { block_hash: block.hash, type: StateTransitionEnum.DOCUMENTS_BATCH, owner: identity.identifier, + data: 'AgAOCeQUD4t3d4EL5WxH8KtcvZvtHnc6vZ+f3y/memaf9wEAAABgCLhdmCbncK0httWF8BDx37Oz8q3GSSMpu++P3sGx1wIEbm90ZdpXZPiQJeml9oBjOQnbWPb39tNYLERTk/FarViCHJ8r8Jo86sqi8SuYeboiPVuMZsMQbv5Y7cURVW8x7pZ2QSsBB21lc3NhZ2USMFR1dG9yaWFsIENJIFRlc3QgQCBUaHUsIDA4IEF1ZyAyMDI0IDIwOjI1OjAzIEdNVAAAAUEfLtRrTrHXdpT9Pzp4PcNiKV13nnAYAqrl0w3KfWI8QR5f7TTen0N66ZUU7R7AoXV8kliIwVqpxiCVwChbh2XiYQ==', index: 2 }) - await fixtures.document(knex, { + document = await fixtures.document(knex, { state_transition_hash: documentTransaction.hash, owner: identity.identifier, data_contract_id: dataContract.id @@ -215,7 +217,7 @@ describe('Other routes', () => { blockHash: documentTransaction.block_hash, blockHeight: null, type: documentTransaction.type, - data: '{}', + data: documentTransaction.data, timestamp: block.timestamp.toISOString(), gasUsed: 0, status: 'SUCCESS', @@ -328,6 +330,31 @@ describe('Other routes', () => { assert.deepEqual({ dataContracts: [expectedDataContract] }, body) }) + it('should search by document', async () => { + const { body } = await client.get(`/search?query=${document.identifier}`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + + const expectedDataContract = { + identifier: document.identifier, + dataContractIdentifier: dataContract.identifier, + revision: 1, + txHash: document.state_transition_hash, + deleted: document.deleted, + data: JSON.stringify(document.data), + timestamp: new Date(0).toISOString(), + system: false, + entropy: 'f09a3ceacaa2f12b9879ba223d5b8c66c3106efe58edc511556f31ee9676412b', + prefundedVotingBalance: null, + documentTypeName: document.document_type_name, + transitionType: 0, + owner: document.owner, + nonce: 2 + } + + assert.deepEqual({ document: expectedDataContract }, body) + }) + it('should search by identity DPNS', async () => { mock.method(DAPI.prototype, 'getIdentityBalance', async () => 0) diff --git a/packages/api/test/unit/utils.spec.js b/packages/api/test/unit/utils.spec.js index 4b3dcded5..39c4c877e 100644 --- a/packages/api/test/unit/utils.spec.js +++ b/packages/api/test/unit/utils.spec.js @@ -82,11 +82,11 @@ describe('Utils', () => { id: '7TsrNHXDy14fYoRcoYjZHH14K4riMGU2VeHMwopG82DL', dataContractId: 'FhKAsUnPbqe7K4TZxgRdtPUrfSvNCtYV8iPsvjX7ZG58', revision: 1, - prefundedBalance: null, + prefundedVotingBalance: null, type: 'note', + entropy: 'f09a3ceacaa2f12b9879ba223d5b8c66c3106efe58edc511556f31ee9676412b', action: 0, nonce: 2, - entropy: 'f09a3ceacaa2f12b9879ba223d5b8c66c3106efe58edc511556f31ee9676412b', data: { message: 'Tutorial CI Test @ Thu, 08 Aug 2024 20:25:03 GMT' } diff --git a/packages/api/test/utils/fixtures.js b/packages/api/test/utils/fixtures.js index 951df8736..2b8ceb6f1 100644 --- a/packages/api/test/utils/fixtures.js +++ b/packages/api/test/utils/fixtures.js @@ -132,7 +132,8 @@ const fixtures = { owner, is_system, transition_type, - document_type_name + document_type_name, + prefunded_voting_balance }) => { if (!identifier) { identifier = generateIdentifier() @@ -149,14 +150,15 @@ const fixtures = { const row = { identifier, state_transition_hash, - revision: revision ?? 0, + revision: revision ?? 1, data: data ?? {}, deleted: deleted ?? false, data_contract_id, owner, is_system: is_system ?? false, transition_type: transition_type ?? 0, - document_type_name: document_type_name ?? 'type_name' + document_type_name: document_type_name ?? 'type_name', + prefunded_voting_balance } const result = await knex('documents').insert(row).returning('id') diff --git a/packages/frontend/src/app/api/content.md b/packages/frontend/src/app/api/content.md index 5e6329668..5c3b68380 100644 --- a/packages/frontend/src/app/api/content.md +++ b/packages/frontend/src/app/api/content.md @@ -30,10 +30,13 @@ Reference: * [Identities](#identities) * [Data Contracts by Identity](#data-contracts-by-identity) * [Documents by Identity](#documents-by-identity) +* [Document Transactions](#document-transactions) * [Transactions By Identity](#transactions-by-identity) * [Transfers by Identity](#transfers-by-identity) * [Transactions history](#transactions-history) * [Transactions gas history](#transactions-gas-history) +* [Contested Data Contracts](#contested-data-contracts) +* [Contested Documents](#contested-documents) * [Rate](#rate) * [Search](#search) * [Decode Raw Transaction](#decode-raw-transaction) @@ -178,6 +181,7 @@ GET /block/12E5592208322B5A3598C98C1811FCDD403DF40F522511D7A965DDE1D96C97C7 ### Blocks by validator Return all blocks proposed by the specific validators * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /validator/B8F90A4F07D9E59C061D41CC8E775093141492A5FD59AB3BBC4241238BB28A18/blocks @@ -207,6 +211,7 @@ GET /validator/B8F90A4F07D9E59C061D41CC8E775093141492A5FD59AB3BBC4241238BB28A18/ ### Blocks Return all blocks with pagination info * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /blocks?start_epoch_index=1000&end_epoch_index=1200&height_min=2000&height_max=4000&gas_min=1&gas_max=99999999999×tamp_start=2024-08-29T23:24:11.516z×tamp_end=2025-08-29T23:24:11.516z&tx_count_min=2&tx_count_max=11&validator=C11C1168DCF9479475CB1355855E30EA75C0CDDA8A8F9EA80591568DD1C33BA8 @@ -252,6 +257,7 @@ Return all validators with pagination info. * `lastProposedBlockHeader` field is nullable * `?isActive=true` boolean can be supplied in the query params to filter by isActive field * `limit` cannot be more then 100 (0 = all validators) +* `page` cannot be less then 1 ``` GET /validators @@ -572,6 +578,7 @@ Return transaction set paged Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string * `limit` cannot be more then 100 +* `page` cannot be less then 1 * `owner` Identity identifier * `status` can be `SUCCESS`, `FAIL` or `ALL` * `transaction_type` number of tx type. Can be set multiple times @@ -653,6 +660,7 @@ Return dataContracts set paged and order by block height or documents count. * Valid `order_by` values are `block_height` or `documents_count` * `name` field is nullable * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /dataContracts?page=1&limit=10&order=asc&order_by=block_height @@ -692,17 +700,68 @@ Allows to get withdrawals documents by contract id and document type GET /document/FUJsiMpQZWGfdrWPEUhBRExMAQB9q6MNfFgRqCdz42UJ?document_type_name=preorder&contract_id=GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec { - "identifier": "FUJsiMpQZWGfdrWPEUhBRExMAQB9q6MNfFgRqCdz42UJ", + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", "revision": 1, - "txHash": "DB51A0366133EC56098815D3BF66F374BD3E53951B415C5D4F487BAB6DAD271D", + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", "deleted": false, - "data": "{\"saltedDomainHash\":\"fFfVmDpvXdr5psoak2U5yeUntEEy3M9pAFHVIn+yEvU=\"}", - "timestamp": "2024-08-25T18:31:50.335Z", + "data": {"saltedDomainHash":"DcKS9AWVE1atKvIokA7JNdUNmyj4SbFUvB6e83whw2g="}, + "timestamp": "2024-12-27T14:31:00.798Z", "isSystem": false, + "entropy": "7beffbed25071ab26c0c7c50b3bab098f42126f2a91f9355f492a2d83beb74aa", + "prefundedVotingBalance": { + "parentNameAndLabel": 20000000000 + }, "typeName": "preorder", - "transitionType": "0", - "owner": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP" + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" +} +``` +Response codes: +``` +200: OK +404: Not found +500: Internal Server Error +``` +--- +### Document Transactions +Return transactions for selected document + +* Valid `order_by` values are `asc` or `desc` +* `limit` cannot be more then 100 +* `page` cannot be less then 1 + +``` +GET /document/ELEeNjGbqCsHNtkoJ51pFHvUyCk5sxgU1jYVuySMhQwN/transactions + +{ + "resultSet": [ + { + "revision": 1, + "gasUsed": 38201380, + "owner": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP", + "hash": "437C949982B41E00506B88C62A94B3E032FADFB010BCA91F3A4C874EB75F9E23", + "timestamp": "2024-08-25T18:32:24.454Z", + "transitionType": 0, + "data": { + "label": "BurgerJoint2", + "records": { + "identity": "Gn15UqiQ6gpqzXcDhv4adwsJ1KgpG6xx9e3rij9n4ctP" + }, + "preorderSalt": "GV43pQXjfaSZ5FryGKsyKJBja2L+fwxXz9Npe0MH2WQ=", + "subdomainRules": { + "allowSubdomains": false + }, + "normalizedLabel": "burgerj01nt2", + "parentDomainName": "dash", + "normalizedParentDomainName": "dash" + } + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 1 + } } ``` Response codes: @@ -715,30 +774,32 @@ Response codes: ### Documents by Data Contract Return all documents by the given data contract identifier * `limit` cannot be more then 100 +* `page` cannot be less then 1 +* `document_type_name` optional ``` -GET /dataContract/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?page=1&limit=10&order=asc&type=preorder +GET /dataContract/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?document_type_name=domain&page=1&limit=10&order=desc { "resultSet": [ { - "identifier": "2qHj3sdxdKD4G7ZXGCuF6cB6ADYdtPs2BKNx24YcH6gW", + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", "revision": 1, - "txHash": "EDBC1D2AC12B8BE211086E3B8DD8018C5FB2FECBD46EB77205F9AF24E73BADAD", + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", "deleted": false, - "data": "{\"saltedDomainHash\":\"DcKS9AWVE1atKvIokA7JNdUNmyj4SbFUvB6e83whw2g=\"}", - "timestamp": "2024-08-25T09:11:05.698Z", + "data": "{\"label\":\"web\",\"records\":{\"identity\":\"8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6\"},\"preorderSalt\":\"HVKEY/12WglST1QCqxH9/yJsp8MMb+1GLc8xWw23PCI=\",\"subdomainRules\":{\"allowSubdomains\":false},\"normalizedLabel\":\"web\",\"parentDomainName\":\"dash\",\"normalizedParentDomainName\":\"dash\"}", + "timestamp": "2024-12-27T14:31:00.798Z", "isSystem": false, - "typeName": "preorder", - "transitionType": "0", - "owner": "BHAuKDRVPHkJd99pLoQh8dfjUFobwk5bq6enubEBKpsv" - }, - ... + "entropy": null, + "prefundedVotingBalance": null, + "typeName": "domain", + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" + }, ... ], "pagination": { "page": 1, "limit": 10, - "total": 750 + "total": 521 } } ``` @@ -751,7 +812,7 @@ Response codes: ### Identity by Identifier Return identity by given identifier ``` -GET /identity/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec +GET /identity/3igSMtXaaS9iRQHbWU1w4hHveKdxixwMpgmhLzjVhFZJ { "identifier": "3igSMtXaaS9iRQHbWU1w4hHveKdxixwMpgmhLzjVhFZJ", @@ -866,6 +927,7 @@ Return all identities paged and order by block height, tx count or balance. * Valid `order_by` values are `block_height`, `tx_count` or `balance` * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities?page=1&limit=10&order=asc&order_by=block_height @@ -912,6 +974,7 @@ Return all withdrawals for identity _Note: this request does not contain any pagination data in the response_ * `limit` cannot be more then 100 +* `page` cannot be less then 1 * returns 404 `not found` if identity don't have withdrawals * Pagination always `null` ``` @@ -949,6 +1012,7 @@ Return all data contracts by the given identity * `name` field is nullable * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/dataContracts?page=1&limit=10&order=asc @@ -982,30 +1046,33 @@ Response codes: ### Documents by Identity Return all documents by the given identity * `limit` cannot be more then 100 +* `page` cannot be less then 1 +* `document_type_name` document type name _optional_ ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/documents?page=1&limit=10&order=asc&document_type_name=preorder { - pagination: { - page: 1, - limit: 10, - total: 10 - }, - resultSet: [ + "resultSet": [ { - identifier: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - owner: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - dataContractIdentifier: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", - revision: 0, - txHash: "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", - deleted: false, - data: null, - timestamp: "2024-03-18T10:13:54.150Z", - isSystem: false, - transitionType: 0, - typeName: 'preorder' + "identifier": "47JuExXJrZaG3dLfrL2gnAH8zhYh6z9VutF8NvgRQbQJ", + "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "revision": 1, + "txHash": "5CA1D01931D7C236194D3364D410946FAF6C12FDC0FB56DB3B05ADB881B43B1A", + "deleted": false, + "data": null, + "timestamp": "2024-12-27T14:31:00.798Z", + "isSystem": false, + "entropy": null, + "prefundedVotingBalance": null, + "typeName": "domain", + "owner": "8J8k9aQ5Hotx8oLdnYAhYpyBJJGg4wZALptKLuDE9Df6" }, ... - ] + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 4 + } } ``` Response codes: @@ -1019,6 +1086,7 @@ Return all transactions made by the given identity Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string * `limit` cannot be more then 100 +* `page` cannot be less then 1 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/transactions?page=1&limit=10&order=asc @@ -1055,6 +1123,7 @@ Response codes: ### Transfers by Identity Return all transfers made by the given identity * `limit` cannot be more then 100 +* `page` cannot be less then 1 * `type` cannot be less, then 0 and more then 8 ``` GET /identities/GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec/transfers?hash=445E6F081DEE877867816AD3EF492E2C0BD1DDCCDC9C793B23DDDAF8AEA23118&page=1&limit=10&order=asc&type=6 @@ -1155,6 +1224,7 @@ Response codes: 200: OK 500: Internal Server Error ``` +___ ### Transactions history Return a series data for the amount of transactions chart @@ -1189,6 +1259,7 @@ Response codes: 400: Invalid input, check start/end values 500: Internal Server Error ``` +___ ### Transactions Gas history Return a series data for the used gas of transactions chart @@ -1223,6 +1294,88 @@ Response codes: 400: Invalid input, check start/end values 500: Internal Server Error ``` +___ +### Contested Data Contracts +Return a series Data Contracts, which contains contested types + +* `limit` cannot be more then 100 +* `page` cannot be less then 1 +* Valid `order_by` values are `block_height` or `documents_count` + +``` +GET /contested/dataContracts?limit=10&page=1&order=asc&order_by=block_height +{ + "resultSet": [ + { + "identifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "name": "DPNS", + "owner": "11111111111111111111111111111111", + "schema": null, + "version": 0, + "txHash": null, + "timestamp": null, + "isSystem": true, + "documentsCount": 1413, + "contestedDocumentsCount": 231 + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 1 + } +} +``` +Response codes: +``` +200: OK +400: Invalid input, check start/end values +500: Internal Server Error +``` +___ +### Contested Documents +Return a series contested documents + +* `limit` cannot be more then 100 +* `page` cannot be less then 1 +``` +GET /contested/documents?limit=10&page=1&order=asc&document_type_name=domain +{ + "resultSet": [ + { + "identifier": "nLxqkMa9GRgfFGJpW9aE8ykYHxSR8Nat49cCNHXZR5a", + "dataContractIdentifier": "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "revision": 1, + "txHash": "069422484BEEF731CFDBA601BDAC80BCF1FB009FACE7EE4897F3D4AE72EF85D8", + "deleted": false, + "data": "{\"label\":\"test111\",\"records\":{\"identity\":\"AC5EoXmLSpM6Q9S2BFMrfFFqmhCSiE5yKVdA3L5DkcjU\"},\"preorderSalt\":\"gIUpKYlk2J7wIxr6IWoPrtLW6+wNnaeKp4mfED6aLeI=\",\"subdomainRules\":{\"allowSubdomains\":false},\"normalizedLabel\":\"test111\",\"parentDomainName\":\"dash\",\"normalizedParentDomainName\":\"dash\"}", + "timestamp": "2024-08-26T22:14:06.680Z", + "system": false, + "entropy": null, + "prefundedVotingBalance": { + "parentNameAndLabel": 20000000000 + }, + "documentTypeName": "domain", + "transitionType": 0, + "nonce": null, + "owner": "AC5EoXmLSpM6Q9S2BFMrfFFqmhCSiE5yKVdA3L5DkcjU" + }, + ... + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 231 + } +} +``` +Response codes: +``` +200: OK +400: Invalid input, check start/end values +500: Internal Server Error +``` +___ ### Rate Return a rate DASH to USD ``` @@ -1238,6 +1391,7 @@ Response codes: 500: Internal Server Error 503: Service Temporarily Unavailable ``` +___ ### Decode Raw Transaction Return a decoded State Transition @@ -1256,7 +1410,7 @@ Available transactions type for decode | `MASTERNODE_VOTE` | 8 | - `fundingAddress` can be null -- `prefundedBalance` can be null +- `prefundedVotingBalance` can be null - `contractBounds` always null ``` @@ -1438,7 +1592,7 @@ POST /transaction/decode "allowSubdomains": false } }, - "prefundedBalance": { + "prefundedVotingBalance": { "parentNameAndLabel": 20000000000 } }