From f7e3983c991c8e457d998ff2c0a68d5241d450de Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:41:31 +0200 Subject: [PATCH] Revert "refactor(js-drive): remove obsolete javascript code (js-drive, rs-drive-nodejs) (#930)" This reverts commit 2a54d4b42e31cccb336629864046d91c7406bc4b. --- .pnp.cjs | 630 +-- ...-utils-npm-4.2.0-434cf92d50-82fdd1cc2a.zip | Bin 61581 -> 0 bytes .yarn/cache/fsevents-patch-2882183fbf-8.zip | Bin 23675 -> 0 bytes ...ymbols-npm-1.0.2-50e53af115-2309c42607.zip | Bin 0 -> 10027 bytes ...llable-npm-1.2.4-03fc17459c-1a28d57dc4.zip | Bin 0 -> 9785 bytes ...-array-npm-1.1.8-147f090d0d-aa0f9f0716.zip | Bin 0 -> 8267 bytes ...ntries-npm-1.1.5-7a8fcbc43e-d658696f74.zip | Bin 0 -> 13922 bytes packages/js-drive/.env.example | 71 + packages/js-drive/.eslintrc | 36 + packages/js-drive/.mocharc.yml | 4 + packages/js-drive/CHANGELOG.md | 509 +++ packages/js-drive/Dockerfile | 114 + packages/js-drive/LICENSE | 20 + packages/js-drive/README.md | 55 + packages/js-drive/db/.gitignore | 2 + .../lib/abci/closeAbciServerFactory.js | 25 + .../lib/abci/errors/AbstractAbciError.js | 68 + .../lib/abci/errors/DPPValidationAbciError.js | 44 + .../lib/abci/errors/InternalAbciError.js | 25 + .../abci/errors/InvalidArgumentAbciError.js | 16 + .../lib/abci/errors/NotFoundAbciError.js | 16 + .../lib/abci/errors/UnavailableAbciError.js | 16 + .../lib/abci/errors/UnimplementedAbciError.js | 16 + .../abci/errors/VerboseInternalAbciError.js | 30 + .../abci/errors/createContextLoggerFactory.js | 24 + .../enrichErrorWithContextLoggerFactory.js | 39 + .../abci/errors/wrapInErrorHandlerFactory.js | 63 + .../abci/handlers/checkTxHandlerFactory.js | 44 + .../NetworkProtocolVersionIsNotSetError.js | 9 + ...NotSupportedNetworkProtocolVersionError.js | 30 + .../abci/handlers/extendVoteHandlerFactory.js | 67 + .../handlers/finalizeBlockHandlerFactory.js | 145 + .../lib/abci/handlers/infoHandlerFactory.js | 94 + .../abci/handlers/initChainHandlerFactory.js | 150 + .../handlers/prepareProposalHandlerFactory.js | 189 + .../handlers/processProposalHandlerFactory.js | 99 + .../handlers/proposal/beginBlockFactory.js | 224 + .../broadcastWithdrawalTransactionsFactory.js | 64 + .../createConsensusParamUpdateFactory.js | 69 + .../createCoreChainLockUpdateFactory.js | 49 + .../handlers/proposal/deliverTxFactory.js | 281 ++ .../abci/handlers/proposal/endBlockFactory.js | 125 + .../fees/addStateTransitionFeesToBlockFees.js | 21 + .../proposal/processProposalFactory.js | 139 + ...otateAndCreateValidatorSetUpdateFactory.js | 51 + .../lib/abci/handlers/proposal/statuses.js | 5 + .../proposal/verifyChainLockFactory.js | 69 + .../query/dataContractQueryHandlerFactory.js | 74 + .../query/documentQueryHandlerFactory.js | 97 + .../query/getProofsQueryHandlerFactory.js | 110 + ...iesByPublicKeyHashesQueryHandlerFactory.js | 70 + .../query/identityQueryHandlerFactory.js | 75 + .../response/createQueryResponseFactory.js | 65 + .../lib/abci/handlers/queryHandlerFactory.js | 58 + .../unserializeStateTransitionFactory.js | 120 + packages/js-drive/lib/abci/handlers/timers.js | 10 + .../validator/createValidatorSetUpdate.js | 49 + .../verifyVoteExtensionHandlerFactory.js | 92 + .../blockExecution/BlockExecutionContext.js | 383 ++ .../BlockExecutionContextRepository.js | 69 + .../js-drive/lib/blockExecution/BlockInfo.js | 54 + .../BlockExecutionContextNotFoundError.js | 23 + .../ContextsAreMoreThanStackMaxSizeError.js | 9 + .../js-drive/lib/core/LatestCoreChainLock.js | 42 + .../lib/core/SimplifiedMasternodeList.js | 47 + packages/js-drive/lib/core/ZmqClient.js | 129 + packages/js-drive/lib/core/ensureBlock.js | 35 + .../lib/core/errors/MissingChainLockError.js | 11 + .../errors/NotEnoughBlocksForValidSMLError.js | 23 + .../lib/core/errors/QuorumsNotFoundError.js | 31 + .../lib/core/fetchQuorumMembersFactory.js | 38 + .../lib/core/fetchSimplifiedMNListFactory.js | 23 + .../lib/core/fetchTransactionFactory.js | 33 + .../lib/core/getRandomQuorumFactory.js | 113 + .../updateSimplifiedMasternodeListFactory.js | 113 + .../core/waitForChainLockedHeightFactory.js | 49 + .../core/waitForCoreChainLockSyncFactory.js | 103 + .../lib/core/waitForCoreSyncFactory.js | 47 + packages/js-drive/lib/createDIContainer.js | 894 ++++ .../DataContractStoreRepository.js | 181 + .../lib/document/DocumentRepository.js | 330 ++ .../lib/document/errors/InvalidQueryError.js | 7 + .../lib/document/fetchDataContractFactory.js | 44 + .../lib/document/fetchDocumentsFactory.js | 46 + .../groveDB/createDocumentTreePath.js | 15 + .../lib/document/proveDocumentsFactory.js | 39 + .../lib/dpp/CachedStateRepositoryDecorator.js | 344 ++ .../js-drive/lib/dpp/DriveStateRepository.js | 660 +++ .../lib/dpp/LoggedStateRepositoryDecorator.js | 676 +++ packages/js-drive/lib/errorHandlerFactory.js | 59 + packages/js-drive/lib/errors/DriveError.js | 23 + .../getFeatureFlagForHeightFactory.js | 48 + .../getLatestFeatureFlagFactory.js | 52 + .../IdentityBalanceStoreRepository.js | 200 + .../IdentityPublicKeyStoreRepository.js | 109 + .../lib/identity/IdentityStoreRepository.js | 295 ++ .../SpentAssetLockTransactionsRepository.js | 73 + .../LastSyncedCoreHeightRepository.js | 68 + .../createMasternodeIdentityFactory.js | 83 + .../masternode/createOperatorIdentifier.js | 20 + .../createRewardShareDocumentFactory.js | 88 + .../masternode/createVotingIdentifier.js | 21 + .../InvalidIdentityPublicKeyTypeError.js | 22 + .../errors/InvalidMasternodeIdentityError.js | 23 + .../errors/InvalidPayoutScriptError.js | 22 + .../getPublicKeyFromPayoutScript.js | 21 + ...thdrawPubKeyTypeFromPayoutScriptFactory.js | 38 + .../masternode/handleNewMasternodeFactory.js | 128 + .../handleRemovedMasternodeFactory.js | 63 + .../handleUpdatedPubKeyOperatorFactory.js | 145 + .../handleUpdatedScriptPayoutFactory.js | 117 + .../handleUpdatedVotingAddressFactory.js | 77 + .../synchronizeMasternodeIdentitiesFactory.js | 258 ++ ...WithdrawalTransactionIdAndStatusFactory.js | 76 + packages/js-drive/lib/storage/GroveDBStore.js | 519 +++ .../js-drive/lib/storage/StorageResult.js | 68 + packages/js-drive/lib/test/.eslintrc | 9 + packages/js-drive/lib/test/bootstrap.js | 78 + .../lib/test/createTestDIContainer.js | 30 + .../fixtures/createDataContractDocuments.js | 28 + .../lib/test/fixtures/createIndices.js | 36 + .../lib/test/fixtures/createProperties.js | 17 + .../getBlockExecutionContextObjectFixture.js | 90 + .../lib/test/fixtures/getSmlFixture.js | 87 + .../getSystemIdentityPublicKeysFixture.js | 28 + .../test/mock/BlockExecutionContextMock.js | 67 + .../BlockExecutionContextRepositoryMock.js | 15 + .../BlockExecutionStoreTransactionsMock.js | 21 + packages/js-drive/lib/test/mock/DriveMock.js | 29 + .../lib/test/mock/GroveDBStoreMock.js | 43 + packages/js-drive/lib/test/mock/LoggerMock.js | 25 + .../js-drive/lib/test/mock/RootTreeMock.js | 11 + .../lib/test/mock/StateViewTransactionMock.js | 20 + packages/js-drive/lib/test/mock/StoreMock.js | 13 + .../lib/test/mock/StoreRepositoryMock.js | 17 + .../test/mock/getBiggestPossibleIdentity.js | 46 + .../lib/test/util/allocateRandomMemory.js | 16 + .../js-drive/lib/test/util/setTimeoutShim.js | 14 + packages/js-drive/lib/util/ExecutionTimer.js | 95 + .../lib/util/millisToProtoTimestamp.js | 28 + packages/js-drive/lib/util/noopLogger.js | 8 + packages/js-drive/lib/util/printErrorFace.js | 35 + .../lib/util/protoTimestampToMillis.js | 11 + packages/js-drive/lib/util/rejectAfter.js | 25 + packages/js-drive/lib/util/sanitizeUrl.js | 15 + packages/js-drive/lib/util/shuffleArray.js | 14 + packages/js-drive/lib/util/timeToMillis.js | 15 + packages/js-drive/lib/util/wait.js | 10 + packages/js-drive/lib/validator/Validator.js | 72 + .../lib/validator/ValidatorNetworkInfo.js | 29 + .../js-drive/lib/validator/ValidatorSet.js | 171 + .../errors/PublicKeyShareIsNotPresentError.js | 23 + .../ValidatorSetIsNotInitializedError.js | 9 + packages/js-drive/package.json | 104 + packages/js-drive/scripts/abci.js | 181 + packages/js-drive/scripts/echo.js | 34 + packages/js-drive/test/.eslintrc | 12 + packages/js-drive/test/README.md | 54 + .../abci/handlers/queryHandlerFactory.spec.js | 150 + .../BlockExecutionContextRepository.spec.js | 121 + .../core/SimplifiedMasternodeList.spec.js | 103 + ...ateSimplifiedMasternodeListFactory.spec.js | 92 + .../core/waitForCoreSyncFactory.spec.js | 54 + .../integration/createDIContainer.spec.js | 40 + .../DataContractStoreRepository.spec.js | 438 ++ .../document/DocumentRepository.spec.js | 3408 +++++++++++++++ .../document/fetchDataContractFactory.spec.js | 83 + .../document/fetchDocumentsFactory.spec.js | 221 + .../document/proveDocumentsFactory.spec.js | 198 + .../integration/fee/feesPrediction.spec.js | 549 +++ .../integration/groveDB/GroveDBStore.spec.js | 423 ++ .../IdentityBalanceStoreRepository.spec.js | 452 ++ .../IdentityPublicKeyStoreRepository.spec.js | 177 + .../identity/IdentityStoreRepository.spec.js | 544 +++ ...entAssetLockTransactionsRepository.spec.js | 85 + .../LastSyncedCoreHeightRepository.spec.js | 89 + ...hronizeMasternodeIdentitiesFactory.spec.js | 1093 +++++ .../unit/abci/closeAbciServerFactory.spec.js | 29 + ...nrichErrorWithContextLoggerFactory.spec.js | 38 + .../errors/wrapInErrorHandlerFactory.spec.js | 109 + .../handlers/checkTxHandlerFactory.spec.js | 54 + .../handlers/extendVoteHandlerFactory.spec.js | 68 + .../finalizeBlockHandlerFactory.spec.js | 217 + .../abci/handlers/infoHandlerFactory.spec.js | 129 + .../handlers/initChainHandlerFactory.spec.js | 164 + .../prepareProposalHandlerFactory.spec.js | 232 ++ .../processProposalHandlerFactory.spec.js | 160 + .../proposal/beginBlockFactory.spec.js | 278 ++ ...dcastWithdrawalTransactionsFactory.spec.js | 69 + .../createConsensusParamUpdateFactory.spec.js | 90 + .../createCoreChainLockUpdateFactory.spec.js | 65 + .../proposal/deliverTxFactory.spec.js | 319 ++ .../handlers/proposal/endBlockFactory.spec.js | 134 + .../proposal/processProposalFactory.spec.js | 170 + ...AndCreateValidatorSetUpdateFactory.spec.js | 103 + .../proposal/verifyChainLockFactory.spec.js | 117 + .../dataContractQueryHandlerFactory.spec.js | 124 + .../query/documentQueryHandlerFactory.spec.js | 167 + .../getProofsQueryHandlerFactory.spec.js | 122 + ...PublicKeyHashesQueryHandlerFactory.spec.js | 135 + .../query/identityQueryHandlerFactory.spec.js | 112 + .../createQueryResponseFactory.spec.js | 77 + .../abci/handlers/queryHandlerFactory.spec.js | 129 + .../unserializeStateTransitionFactory.spec.js | 204 + .../createValidatorSetUpdate.spec.js | 57 + .../verifyVoteExtensionHandlerFactory.spec.js | 98 + .../BlockExecutionContext.spec.js | 427 ++ .../unit/core/LatestCoreChainLock.spec.js | 44 + .../test/unit/core/ensureBlock.spec.js | 66 + .../unit/core/getRandomQuorumFactory.spec.js | 196 + ...ateSimplifiedMasternodeListFactory.spec.js | 195 + .../waitForChainLockedHeightFactory.spec.js | 63 + .../waitForCoreChainLockSyncFactory.spec.js | 86 + .../CachedStateRepositoryDecorator.spec.js | 247 ++ .../unit/dpp/DriveStateRepository.spec.js | 732 ++++ .../LoggedStateRepositoryDecorator.spec.js | 983 +++++ .../getFeatureFlagForHeightFactory.spec.js | 65 + .../getLatestFeatureFlagFactory.spec.js | 53 + .../createMasternodeIdentityFactory.spec.js | 191 + .../createOperatorIdentifier.spec.js | 23 + .../masternode/createVotingIdentifier.spec.js | 23 + .../getPublicKeyFromPayoutScript.spec.js | 43 + ...wPubKeyTypeFromPayoutScriptFactory.spec.js | 44 + .../handleNewMasternodeFactory.spec.js | 178 + .../handleUpdatedScriptPayoutFactory.spec.js | 157 + .../handleUpdatedVotingAddressFactory.spec.js | 102 + ...rawalTransactionIdAndStatusFactory.spec.js | 89 + .../test/unit/util/ExecutionTimer.spec.js | 43 + .../unit/util/errorHandlerFactory.spec.js | 142 + .../test/unit/util/rejectAfter.spec.js | 34 + .../test/unit/util/sanitizeUrl.spec.js | 13 + packages/js-drive/test/unit/util/wait.spec.js | 30 + .../test/unit/validator/Validator.spec.js | 37 + .../validator/ValidatorNetworkInfo.spec.js | 24 + .../test/unit/validator/ValidatorSet.spec.js | 253 ++ packages/rs-drive-nodejs/.eslintrc | 35 + packages/rs-drive-nodejs/.gitignore | 2 + packages/rs-drive-nodejs/.mocharc.yml | 2 + packages/rs-drive-nodejs/Cargo.toml | 23 + packages/rs-drive-nodejs/Drive.js | 940 +++++ packages/rs-drive-nodejs/FeeResult.js | 95 + packages/rs-drive-nodejs/GroveDB.js | 342 ++ packages/rs-drive-nodejs/appendStack.js | 36 + packages/rs-drive-nodejs/docker/build.sh | 44 + packages/rs-drive-nodejs/package.json | 50 + packages/rs-drive-nodejs/scripts/build.sh | 23 + packages/rs-drive-nodejs/src/converter.rs | 534 +++ packages/rs-drive-nodejs/src/fee/mod.rs | 39 + packages/rs-drive-nodejs/src/fee/result.rs | 167 + packages/rs-drive-nodejs/src/lib.rs | 3682 +++++++++++++++++ packages/rs-drive-nodejs/test/.eslintrc | 12 + packages/rs-drive-nodejs/test/Drive.spec.js | 1181 ++++++ packages/rs-drive-nodejs/test/GroveDB.spec.js | 1240 ++++++ packages/rs-drive-nodejs/test/utils.js | 16 + yarn.lock | 240 +- 255 files changed, 38128 insertions(+), 827 deletions(-) delete mode 100644 .yarn/cache/@eslint-community-eslint-utils-npm-4.2.0-434cf92d50-82fdd1cc2a.zip delete mode 100644 .yarn/cache/fsevents-patch-2882183fbf-8.zip create mode 100644 .yarn/cache/has-symbols-npm-1.0.2-50e53af115-2309c42607.zip create mode 100644 .yarn/cache/is-callable-npm-1.2.4-03fc17459c-1a28d57dc4.zip create mode 100644 .yarn/cache/is-typed-array-npm-1.1.8-147f090d0d-aa0f9f0716.zip create mode 100644 .yarn/cache/object.entries-npm-1.1.5-7a8fcbc43e-d658696f74.zip create mode 100644 packages/js-drive/.env.example create mode 100644 packages/js-drive/.eslintrc create mode 100644 packages/js-drive/.mocharc.yml create mode 100644 packages/js-drive/CHANGELOG.md create mode 100644 packages/js-drive/Dockerfile create mode 100644 packages/js-drive/LICENSE create mode 100644 packages/js-drive/README.md create mode 100644 packages/js-drive/db/.gitignore create mode 100644 packages/js-drive/lib/abci/closeAbciServerFactory.js create mode 100644 packages/js-drive/lib/abci/errors/AbstractAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/DPPValidationAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/InternalAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/InvalidArgumentAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/NotFoundAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/UnavailableAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/UnimplementedAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/VerboseInternalAbciError.js create mode 100644 packages/js-drive/lib/abci/errors/createContextLoggerFactory.js create mode 100644 packages/js-drive/lib/abci/errors/enrichErrorWithContextLoggerFactory.js create mode 100644 packages/js-drive/lib/abci/errors/wrapInErrorHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/checkTxHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/errors/NetworkProtocolVersionIsNotSetError.js create mode 100644 packages/js-drive/lib/abci/handlers/errors/NotSupportedNetworkProtocolVersionError.js create mode 100644 packages/js-drive/lib/abci/handlers/extendVoteHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/finalizeBlockHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/infoHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/initChainHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/prepareProposalHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/processProposalHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/beginBlockFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/createConsensusParamUpdateFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/createCoreChainLockUpdateFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/deliverTxFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/endBlockFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/fees/addStateTransitionFeesToBlockFees.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/processProposalFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/statuses.js create mode 100644 packages/js-drive/lib/abci/handlers/proposal/verifyChainLockFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/dataContractQueryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/documentQueryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/getProofsQueryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/identityQueryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/query/response/createQueryResponseFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/queryHandlerFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/stateTransition/unserializeStateTransitionFactory.js create mode 100644 packages/js-drive/lib/abci/handlers/timers.js create mode 100644 packages/js-drive/lib/abci/handlers/validator/createValidatorSetUpdate.js create mode 100644 packages/js-drive/lib/abci/handlers/verifyVoteExtensionHandlerFactory.js create mode 100644 packages/js-drive/lib/blockExecution/BlockExecutionContext.js create mode 100644 packages/js-drive/lib/blockExecution/BlockExecutionContextRepository.js create mode 100644 packages/js-drive/lib/blockExecution/BlockInfo.js create mode 100644 packages/js-drive/lib/blockExecution/errors/BlockExecutionContextNotFoundError.js create mode 100644 packages/js-drive/lib/blockExecution/errors/ContextsAreMoreThanStackMaxSizeError.js create mode 100644 packages/js-drive/lib/core/LatestCoreChainLock.js create mode 100644 packages/js-drive/lib/core/SimplifiedMasternodeList.js create mode 100644 packages/js-drive/lib/core/ZmqClient.js create mode 100644 packages/js-drive/lib/core/ensureBlock.js create mode 100644 packages/js-drive/lib/core/errors/MissingChainLockError.js create mode 100644 packages/js-drive/lib/core/errors/NotEnoughBlocksForValidSMLError.js create mode 100644 packages/js-drive/lib/core/errors/QuorumsNotFoundError.js create mode 100644 packages/js-drive/lib/core/fetchQuorumMembersFactory.js create mode 100644 packages/js-drive/lib/core/fetchSimplifiedMNListFactory.js create mode 100644 packages/js-drive/lib/core/fetchTransactionFactory.js create mode 100644 packages/js-drive/lib/core/getRandomQuorumFactory.js create mode 100644 packages/js-drive/lib/core/updateSimplifiedMasternodeListFactory.js create mode 100644 packages/js-drive/lib/core/waitForChainLockedHeightFactory.js create mode 100644 packages/js-drive/lib/core/waitForCoreChainLockSyncFactory.js create mode 100644 packages/js-drive/lib/core/waitForCoreSyncFactory.js create mode 100644 packages/js-drive/lib/createDIContainer.js create mode 100644 packages/js-drive/lib/dataContract/DataContractStoreRepository.js create mode 100644 packages/js-drive/lib/document/DocumentRepository.js create mode 100644 packages/js-drive/lib/document/errors/InvalidQueryError.js create mode 100644 packages/js-drive/lib/document/fetchDataContractFactory.js create mode 100644 packages/js-drive/lib/document/fetchDocumentsFactory.js create mode 100644 packages/js-drive/lib/document/groveDB/createDocumentTreePath.js create mode 100644 packages/js-drive/lib/document/proveDocumentsFactory.js create mode 100644 packages/js-drive/lib/dpp/CachedStateRepositoryDecorator.js create mode 100644 packages/js-drive/lib/dpp/DriveStateRepository.js create mode 100644 packages/js-drive/lib/dpp/LoggedStateRepositoryDecorator.js create mode 100644 packages/js-drive/lib/errorHandlerFactory.js create mode 100644 packages/js-drive/lib/errors/DriveError.js create mode 100644 packages/js-drive/lib/featureFlag/getFeatureFlagForHeightFactory.js create mode 100644 packages/js-drive/lib/featureFlag/getLatestFeatureFlagFactory.js create mode 100644 packages/js-drive/lib/identity/IdentityBalanceStoreRepository.js create mode 100644 packages/js-drive/lib/identity/IdentityPublicKeyStoreRepository.js create mode 100644 packages/js-drive/lib/identity/IdentityStoreRepository.js create mode 100644 packages/js-drive/lib/identity/SpentAssetLockTransactionsRepository.js create mode 100644 packages/js-drive/lib/identity/masternode/LastSyncedCoreHeightRepository.js create mode 100644 packages/js-drive/lib/identity/masternode/createMasternodeIdentityFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/createOperatorIdentifier.js create mode 100644 packages/js-drive/lib/identity/masternode/createRewardShareDocumentFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/createVotingIdentifier.js create mode 100644 packages/js-drive/lib/identity/masternode/errors/InvalidIdentityPublicKeyTypeError.js create mode 100644 packages/js-drive/lib/identity/masternode/errors/InvalidMasternodeIdentityError.js create mode 100644 packages/js-drive/lib/identity/masternode/errors/InvalidPayoutScriptError.js create mode 100644 packages/js-drive/lib/identity/masternode/getPublicKeyFromPayoutScript.js create mode 100644 packages/js-drive/lib/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/handleNewMasternodeFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/handleRemovedMasternodeFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/handleUpdatedPubKeyOperatorFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/handleUpdatedScriptPayoutFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/handleUpdatedVotingAddressFactory.js create mode 100644 packages/js-drive/lib/identity/masternode/synchronizeMasternodeIdentitiesFactory.js create mode 100644 packages/js-drive/lib/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.js create mode 100644 packages/js-drive/lib/storage/GroveDBStore.js create mode 100644 packages/js-drive/lib/storage/StorageResult.js create mode 100644 packages/js-drive/lib/test/.eslintrc create mode 100644 packages/js-drive/lib/test/bootstrap.js create mode 100644 packages/js-drive/lib/test/createTestDIContainer.js create mode 100644 packages/js-drive/lib/test/fixtures/createDataContractDocuments.js create mode 100644 packages/js-drive/lib/test/fixtures/createIndices.js create mode 100644 packages/js-drive/lib/test/fixtures/createProperties.js create mode 100644 packages/js-drive/lib/test/fixtures/getBlockExecutionContextObjectFixture.js create mode 100644 packages/js-drive/lib/test/fixtures/getSmlFixture.js create mode 100644 packages/js-drive/lib/test/fixtures/getSystemIdentityPublicKeysFixture.js create mode 100644 packages/js-drive/lib/test/mock/BlockExecutionContextMock.js create mode 100644 packages/js-drive/lib/test/mock/BlockExecutionContextRepositoryMock.js create mode 100644 packages/js-drive/lib/test/mock/BlockExecutionStoreTransactionsMock.js create mode 100644 packages/js-drive/lib/test/mock/DriveMock.js create mode 100644 packages/js-drive/lib/test/mock/GroveDBStoreMock.js create mode 100644 packages/js-drive/lib/test/mock/LoggerMock.js create mode 100644 packages/js-drive/lib/test/mock/RootTreeMock.js create mode 100644 packages/js-drive/lib/test/mock/StateViewTransactionMock.js create mode 100644 packages/js-drive/lib/test/mock/StoreMock.js create mode 100644 packages/js-drive/lib/test/mock/StoreRepositoryMock.js create mode 100644 packages/js-drive/lib/test/mock/getBiggestPossibleIdentity.js create mode 100644 packages/js-drive/lib/test/util/allocateRandomMemory.js create mode 100644 packages/js-drive/lib/test/util/setTimeoutShim.js create mode 100644 packages/js-drive/lib/util/ExecutionTimer.js create mode 100644 packages/js-drive/lib/util/millisToProtoTimestamp.js create mode 100644 packages/js-drive/lib/util/noopLogger.js create mode 100644 packages/js-drive/lib/util/printErrorFace.js create mode 100644 packages/js-drive/lib/util/protoTimestampToMillis.js create mode 100644 packages/js-drive/lib/util/rejectAfter.js create mode 100644 packages/js-drive/lib/util/sanitizeUrl.js create mode 100644 packages/js-drive/lib/util/shuffleArray.js create mode 100644 packages/js-drive/lib/util/timeToMillis.js create mode 100644 packages/js-drive/lib/util/wait.js create mode 100644 packages/js-drive/lib/validator/Validator.js create mode 100644 packages/js-drive/lib/validator/ValidatorNetworkInfo.js create mode 100644 packages/js-drive/lib/validator/ValidatorSet.js create mode 100644 packages/js-drive/lib/validator/errors/PublicKeyShareIsNotPresentError.js create mode 100644 packages/js-drive/lib/validator/errors/ValidatorSetIsNotInitializedError.js create mode 100644 packages/js-drive/package.json create mode 100644 packages/js-drive/scripts/abci.js create mode 100644 packages/js-drive/scripts/echo.js create mode 100644 packages/js-drive/test/.eslintrc create mode 100644 packages/js-drive/test/README.md create mode 100644 packages/js-drive/test/integration/abci/handlers/queryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/integration/blockExecution/BlockExecutionContextRepository.spec.js create mode 100644 packages/js-drive/test/integration/core/SimplifiedMasternodeList.spec.js create mode 100644 packages/js-drive/test/integration/core/updateSimplifiedMasternodeListFactory.spec.js create mode 100644 packages/js-drive/test/integration/core/waitForCoreSyncFactory.spec.js create mode 100644 packages/js-drive/test/integration/createDIContainer.spec.js create mode 100644 packages/js-drive/test/integration/dataContract/DataContractStoreRepository.spec.js create mode 100644 packages/js-drive/test/integration/document/DocumentRepository.spec.js create mode 100644 packages/js-drive/test/integration/document/fetchDataContractFactory.spec.js create mode 100644 packages/js-drive/test/integration/document/fetchDocumentsFactory.spec.js create mode 100644 packages/js-drive/test/integration/document/proveDocumentsFactory.spec.js create mode 100644 packages/js-drive/test/integration/fee/feesPrediction.spec.js create mode 100644 packages/js-drive/test/integration/groveDB/GroveDBStore.spec.js create mode 100644 packages/js-drive/test/integration/identity/IdentityBalanceStoreRepository.spec.js create mode 100644 packages/js-drive/test/integration/identity/IdentityPublicKeyStoreRepository.spec.js create mode 100644 packages/js-drive/test/integration/identity/IdentityStoreRepository.spec.js create mode 100644 packages/js-drive/test/integration/identity/SpentAssetLockTransactionsRepository.spec.js create mode 100644 packages/js-drive/test/integration/identity/masternode/LastSyncedCoreHeightRepository.spec.js create mode 100644 packages/js-drive/test/integration/identity/masternode/synchronizeMasternodeIdentitiesFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/closeAbciServerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/errors/enrichErrorWithContextLoggerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/errors/wrapInErrorHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/checkTxHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/extendVoteHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/finalizeBlockHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/infoHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/initChainHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/prepareProposalHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/processProposalHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/beginBlockFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/createConsensusParamUpdateFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/createCoreChainLockUpdateFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/deliverTxFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/endBlockFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/processProposalFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/proposal/verifyChainLockFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/dataContractQueryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/documentQueryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/getProofsQueryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/identityQueryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/query/response/createQueryResponseFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/queryHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/stateTransition/unserializeStateTransitionFactory.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/validator/createValidatorSetUpdate.spec.js create mode 100644 packages/js-drive/test/unit/abci/handlers/verifyVoteExtensionHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/blockExecution/BlockExecutionContext.spec.js create mode 100644 packages/js-drive/test/unit/core/LatestCoreChainLock.spec.js create mode 100644 packages/js-drive/test/unit/core/ensureBlock.spec.js create mode 100644 packages/js-drive/test/unit/core/getRandomQuorumFactory.spec.js create mode 100644 packages/js-drive/test/unit/core/updateSimplifiedMasternodeListFactory.spec.js create mode 100644 packages/js-drive/test/unit/core/waitForChainLockedHeightFactory.spec.js create mode 100644 packages/js-drive/test/unit/core/waitForCoreChainLockSyncFactory.spec.js create mode 100644 packages/js-drive/test/unit/dpp/CachedStateRepositoryDecorator.spec.js create mode 100644 packages/js-drive/test/unit/dpp/DriveStateRepository.spec.js create mode 100644 packages/js-drive/test/unit/dpp/LoggedStateRepositoryDecorator.spec.js create mode 100644 packages/js-drive/test/unit/featureFlag/getFeatureFlagForHeightFactory.spec.js create mode 100644 packages/js-drive/test/unit/featureFlag/getLatestFeatureFlagFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/createMasternodeIdentityFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/createOperatorIdentifier.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/createVotingIdentifier.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/getPublicKeyFromPayoutScript.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/handleNewMasternodeFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/handleUpdatedScriptPayoutFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/masternode/handleUpdatedVotingAddressFactory.spec.js create mode 100644 packages/js-drive/test/unit/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.spec.js create mode 100644 packages/js-drive/test/unit/util/ExecutionTimer.spec.js create mode 100644 packages/js-drive/test/unit/util/errorHandlerFactory.spec.js create mode 100644 packages/js-drive/test/unit/util/rejectAfter.spec.js create mode 100644 packages/js-drive/test/unit/util/sanitizeUrl.spec.js create mode 100644 packages/js-drive/test/unit/util/wait.spec.js create mode 100644 packages/js-drive/test/unit/validator/Validator.spec.js create mode 100644 packages/js-drive/test/unit/validator/ValidatorNetworkInfo.spec.js create mode 100644 packages/js-drive/test/unit/validator/ValidatorSet.spec.js create mode 100644 packages/rs-drive-nodejs/.eslintrc create mode 100644 packages/rs-drive-nodejs/.gitignore create mode 100644 packages/rs-drive-nodejs/.mocharc.yml create mode 100644 packages/rs-drive-nodejs/Cargo.toml create mode 100644 packages/rs-drive-nodejs/Drive.js create mode 100644 packages/rs-drive-nodejs/FeeResult.js create mode 100644 packages/rs-drive-nodejs/GroveDB.js create mode 100644 packages/rs-drive-nodejs/appendStack.js create mode 100755 packages/rs-drive-nodejs/docker/build.sh create mode 100644 packages/rs-drive-nodejs/package.json create mode 100755 packages/rs-drive-nodejs/scripts/build.sh create mode 100644 packages/rs-drive-nodejs/src/converter.rs create mode 100644 packages/rs-drive-nodejs/src/fee/mod.rs create mode 100644 packages/rs-drive-nodejs/src/fee/result.rs create mode 100644 packages/rs-drive-nodejs/src/lib.rs create mode 100644 packages/rs-drive-nodejs/test/.eslintrc create mode 100644 packages/rs-drive-nodejs/test/Drive.spec.js create mode 100644 packages/rs-drive-nodejs/test/GroveDB.spec.js create mode 100644 packages/rs-drive-nodejs/test/utils.js diff --git a/.pnp.cjs b/.pnp.cjs index e60b0f72138..f1e2e3fbd7f 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4157,13 +4157,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/json-schema", "npm:7.0.11"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:7.0.9", {\ - "packageLocation": "./.yarn/cache/@types-json-schema-npm-7.0.9-361918cff3-259d0e25f1.zip/node_modules/@types/json-schema/",\ - "packageDependencies": [\ - ["@types/json-schema", "npm:7.0.11"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["@types/json5", [\ @@ -4415,216 +4408,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-0e22d802b6/0/cache/@typescript-eslint-eslint-plugin-npm-5.55.0-16386bf9af-e3239ec601.zip/node_modules/@typescript-eslint/eslint-plugin/",\ - "packageDependencies": [\ - ["@typescript-eslint/eslint-plugin", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ - ["@eslint-community/regexpp", "npm:4.4.0"],\ - ["@types/eslint", null],\ - ["@types/typescript", null],\ - ["@types/typescript-eslint__parser", null],\ - ["@typescript-eslint/parser", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ - ["@typescript-eslint/scope-manager", "npm:5.55.0"],\ - ["@typescript-eslint/type-utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["@typescript-eslint/utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["debug", "virtual:c2bff3e67180802999655a22d390062982690e911b9d9225c258f3b25e7409f3867b2682c16232b77415f560a09d05a95042dc512a5b8c566c42bbbed88b0bbc#npm:4.3.4"],\ - ["eslint", "npm:7.32.0"],\ - ["grapheme-splitter", "npm:1.0.4"],\ - ["ignore", "npm:5.2.0"],\ - ["natural-compare-lite", "npm:1.4.0"],\ - ["semver", "npm:7.3.7"],\ - ["tsutils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:3.21.0"],\ - ["typescript", "patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=f456af"]\ - ],\ - "packagePeers": [\ - "@types/eslint",\ - "@types/typescript-eslint__parser",\ - "@types/typescript",\ - "@typescript-eslint/parser",\ - "eslint",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/parser", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-parser-npm-5.55.0-ee38253ad6-48a20dc7e6.zip/node_modules/@typescript-eslint/parser/",\ - "packageDependencies": [\ - ["@typescript-eslint/parser", "npm:5.55.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-parser-virtual-2089cd6370/0/cache/@typescript-eslint-parser-npm-5.55.0-ee38253ad6-48a20dc7e6.zip/node_modules/@typescript-eslint/parser/",\ - "packageDependencies": [\ - ["@typescript-eslint/parser", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ - ["@types/eslint", null],\ - ["@types/typescript", null],\ - ["@typescript-eslint/scope-manager", "npm:5.55.0"],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["@typescript-eslint/typescript-estree", "virtual:0c2ac79ebf72f4493d7516f3ee57421c8337be2bdf9498590055ee112e7fdf62dd0b817c13f6c8a030baf64dbe843920deec4c3c8ea8bd6888b7baf950492c3a#npm:5.55.0"],\ - ["debug", "virtual:c2bff3e67180802999655a22d390062982690e911b9d9225c258f3b25e7409f3867b2682c16232b77415f560a09d05a95042dc512a5b8c566c42bbbed88b0bbc#npm:4.3.4"],\ - ["eslint", "npm:7.32.0"],\ - ["typescript", "patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=f456af"]\ - ],\ - "packagePeers": [\ - "@types/eslint",\ - "@types/typescript",\ - "eslint",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/scope-manager", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-scope-manager-npm-5.55.0-d7744f8a94-f253db88f6.zip/node_modules/@typescript-eslint/scope-manager/",\ - "packageDependencies": [\ - ["@typescript-eslint/scope-manager", "npm:5.55.0"],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["@typescript-eslint/visitor-keys", "npm:5.55.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/type-utils", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-type-utils-npm-5.55.0-333e5c4b16-5c60d44135.zip/node_modules/@typescript-eslint/type-utils/",\ - "packageDependencies": [\ - ["@typescript-eslint/type-utils", "npm:5.55.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-type-utils-virtual-0c2ac79ebf/0/cache/@typescript-eslint-type-utils-npm-5.55.0-333e5c4b16-5c60d44135.zip/node_modules/@typescript-eslint/type-utils/",\ - "packageDependencies": [\ - ["@typescript-eslint/type-utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["@types/eslint", null],\ - ["@types/typescript", null],\ - ["@typescript-eslint/typescript-estree", "virtual:0c2ac79ebf72f4493d7516f3ee57421c8337be2bdf9498590055ee112e7fdf62dd0b817c13f6c8a030baf64dbe843920deec4c3c8ea8bd6888b7baf950492c3a#npm:5.55.0"],\ - ["@typescript-eslint/utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["debug", "virtual:c2bff3e67180802999655a22d390062982690e911b9d9225c258f3b25e7409f3867b2682c16232b77415f560a09d05a95042dc512a5b8c566c42bbbed88b0bbc#npm:4.3.4"],\ - ["eslint", "npm:7.32.0"],\ - ["tsutils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:3.21.0"],\ - ["typescript", "patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=f456af"]\ - ],\ - "packagePeers": [\ - "@types/eslint",\ - "@types/typescript",\ - "eslint",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/types", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-types-npm-5.55.0-694e3d296a-7d851f09a2.zip/node_modules/@typescript-eslint/types/",\ - "packageDependencies": [\ - ["@typescript-eslint/types", "npm:5.55.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/typescript-estree", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-typescript-estree-npm-5.55.0-aefc08af17-d24a11aee3.zip/node_modules/@typescript-eslint/typescript-estree/",\ - "packageDependencies": [\ - ["@typescript-eslint/typescript-estree", "npm:5.55.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:0c2ac79ebf72f4493d7516f3ee57421c8337be2bdf9498590055ee112e7fdf62dd0b817c13f6c8a030baf64dbe843920deec4c3c8ea8bd6888b7baf950492c3a#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-891ed1ca66/0/cache/@typescript-eslint-typescript-estree-npm-5.55.0-aefc08af17-d24a11aee3.zip/node_modules/@typescript-eslint/typescript-estree/",\ - "packageDependencies": [\ - ["@typescript-eslint/typescript-estree", "virtual:0c2ac79ebf72f4493d7516f3ee57421c8337be2bdf9498590055ee112e7fdf62dd0b817c13f6c8a030baf64dbe843920deec4c3c8ea8bd6888b7baf950492c3a#npm:5.55.0"],\ - ["@types/typescript", null],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["@typescript-eslint/visitor-keys", "npm:5.55.0"],\ - ["debug", "virtual:c2bff3e67180802999655a22d390062982690e911b9d9225c258f3b25e7409f3867b2682c16232b77415f560a09d05a95042dc512a5b8c566c42bbbed88b0bbc#npm:4.3.4"],\ - ["globby", "npm:11.1.0"],\ - ["is-glob", "npm:4.0.3"],\ - ["semver", "npm:7.3.7"],\ - ["tsutils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:3.21.0"],\ - ["typescript", "patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=f456af"]\ - ],\ - "packagePeers": [\ - "@types/typescript",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-ad07d3460a/0/cache/@typescript-eslint-typescript-estree-npm-5.55.0-aefc08af17-d24a11aee3.zip/node_modules/@typescript-eslint/typescript-estree/",\ - "packageDependencies": [\ - ["@typescript-eslint/typescript-estree", "virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:5.55.0"],\ - ["@types/typescript", null],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["@typescript-eslint/visitor-keys", "npm:5.55.0"],\ - ["debug", "virtual:c2bff3e67180802999655a22d390062982690e911b9d9225c258f3b25e7409f3867b2682c16232b77415f560a09d05a95042dc512a5b8c566c42bbbed88b0bbc#npm:4.3.4"],\ - ["globby", "npm:11.1.0"],\ - ["is-glob", "npm:4.0.3"],\ - ["semver", "npm:7.3.7"],\ - ["tsutils", "virtual:ad07d3460ae416a99f304e6734121d5396603792c47f7c9a1b9c5c798d407da37779ac7289222241db922614d91f9d569b0f43bc10c027fcc720138b2f32e9fc#npm:3.21.0"],\ - ["typescript", null]\ - ],\ - "packagePeers": [\ - "@types/typescript",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/utils", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-utils-npm-5.55.0-6a927fceb5-368cfc3fb9.zip/node_modules/@typescript-eslint/utils/",\ - "packageDependencies": [\ - ["@typescript-eslint/utils", "npm:5.55.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0", {\ - "packageLocation": "./.yarn/__virtual__/@typescript-eslint-utils-virtual-67e332304c/0/cache/@typescript-eslint-utils-npm-5.55.0-6a927fceb5-368cfc3fb9.zip/node_modules/@typescript-eslint/utils/",\ - "packageDependencies": [\ - ["@typescript-eslint/utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["@eslint-community/eslint-utils", "virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:4.3.0"],\ - ["@types/eslint", null],\ - ["@types/json-schema", "npm:7.0.11"],\ - ["@types/semver", "npm:7.3.13"],\ - ["@typescript-eslint/scope-manager", "npm:5.55.0"],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["@typescript-eslint/typescript-estree", "virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:5.55.0"],\ - ["eslint", "npm:7.32.0"],\ - ["eslint-scope", "npm:5.1.1"],\ - ["semver", "npm:7.3.7"]\ - ],\ - "packagePeers": [\ - "@types/eslint",\ - "eslint"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@typescript-eslint/visitor-keys", [\ - ["npm:5.55.0", {\ - "packageLocation": "./.yarn/cache/@typescript-eslint-visitor-keys-npm-5.55.0-7f3c07beeb-0b24c72dff.zip/node_modules/@typescript-eslint/visitor-keys/",\ - "packageDependencies": [\ - ["@typescript-eslint/visitor-keys", "npm:5.55.0"],\ - ["@typescript-eslint/types", "npm:5.55.0"],\ - ["eslint-visitor-keys", "npm:3.3.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@ungap/promise-all-settled", [\ - ["npm:1.1.2", {\ - "packageLocation": "./.yarn/cache/@ungap-promise-all-settled-npm-1.1.2-c0f42e147b-08d37fdfa2.zip/node_modules/@ungap/promise-all-settled/",\ - "packageDependencies": [\ - ["@typescript-eslint/eslint-plugin", "npm:5.55.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0", {\ "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-0e22d802b6/0/cache/@typescript-eslint-eslint-plugin-npm-5.55.0-16386bf9af-e3239ec601.zip/node_modules/@typescript-eslint/eslint-plugin/",\ "packageDependencies": [\ @@ -4798,7 +4581,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/__virtual__/@typescript-eslint-utils-virtual-67e332304c/0/cache/@typescript-eslint-utils-npm-5.55.0-6a927fceb5-368cfc3fb9.zip/node_modules/@typescript-eslint/utils/",\ "packageDependencies": [\ ["@typescript-eslint/utils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:5.55.0"],\ - ["@eslint-community/eslint-utils", "virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:4.2.0"],\ + ["@eslint-community/eslint-utils", "virtual:67e332304c8830574d5d9be2a388885a47a9962cf1d2441a6ada47207b10c98d9a1a1914d73816338b986563032864745d812b3a7df145ee8f3bb51baa4027e5#npm:4.3.0"],\ ["@types/eslint", null],\ ["@types/json-schema", "npm:7.0.11"],\ ["@types/semver", "npm:7.3.13"],\ @@ -5762,17 +5545,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["array-buffer-byte-length", [\ - ["npm:1.0.0", {\ - "packageLocation": "./.yarn/cache/array-buffer-byte-length-npm-1.0.0-331671f28a-044e101ce1.zip/node_modules/array-buffer-byte-length/",\ - "packageDependencies": [\ - ["array-buffer-byte-length", "npm:1.0.0"],\ - ["call-bind", "npm:1.0.2"],\ - ["is-array-buffer", "npm:3.0.2"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["array-differ", [\ ["npm:3.0.0", {\ "packageLocation": "./.yarn/cache/array-differ-npm-3.0.0-ddc0d89007-117edd9df5.zip/node_modules/array-differ/",\ @@ -8306,15 +8078,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["define-properties", [\ - ["npm:1.2.0", {\ - "packageLocation": "./.yarn/cache/define-properties-npm-1.2.0-3547cd0fd2-e60aee6a19.zip/node_modules/define-properties/",\ - "packageDependencies": [\ - ["define-properties", "npm:1.2.0"],\ - ["has-property-descriptors", "npm:1.0.0"],\ - ["object-keys", "npm:1.1.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/define-properties-npm-1.2.0-3547cd0fd2-e60aee6a19.zip/node_modules/define-properties/",\ "packageDependencies": [\ @@ -8877,47 +8640,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["es-abstract", [\ - ["npm:1.21.2", {\ - "packageLocation": "./.yarn/cache/es-abstract-npm-1.21.2-f4ebace1ab-037f55ee5e.zip/node_modules/es-abstract/",\ - "packageDependencies": [\ - ["es-abstract", "npm:1.21.2"],\ - ["array-buffer-byte-length", "npm:1.0.0"],\ - ["available-typed-arrays", "npm:1.0.5"],\ - ["call-bind", "npm:1.0.2"],\ - ["es-set-tostringtag", "npm:2.0.1"],\ - ["es-to-primitive", "npm:1.2.1"],\ - ["function.prototype.name", "npm:1.1.5"],\ - ["get-intrinsic", "npm:1.2.0"],\ - ["get-symbol-description", "npm:1.0.0"],\ - ["globalthis", "npm:1.0.3"],\ - ["gopd", "npm:1.0.1"],\ - ["has", "npm:1.0.3"],\ - ["has-property-descriptors", "npm:1.0.0"],\ - ["has-proto", "npm:1.0.1"],\ - ["has-symbols", "npm:1.0.3"],\ - ["internal-slot", "npm:1.0.5"],\ - ["is-array-buffer", "npm:3.0.2"],\ - ["is-callable", "npm:1.2.7"],\ - ["is-negative-zero", "npm:2.0.2"],\ - ["is-regex", "npm:1.1.4"],\ - ["is-shared-array-buffer", "npm:1.0.2"],\ - ["is-string", "npm:1.0.7"],\ - ["is-typed-array", "npm:1.1.10"],\ - ["is-weakref", "npm:1.0.2"],\ - ["object-inspect", "npm:1.12.3"],\ - ["object-keys", "npm:1.1.1"],\ - ["object.assign", "npm:4.1.4"],\ - ["regexp.prototype.flags", "npm:1.4.3"],\ - ["safe-regex-test", "npm:1.0.0"],\ - ["string.prototype.trim", "npm:1.2.7"],\ - ["string.prototype.trimend", "npm:1.0.6"],\ - ["string.prototype.trimstart", "npm:1.0.6"],\ - ["typed-array-length", "npm:1.0.4"],\ - ["unbox-primitive", "npm:1.0.2"],\ - ["which-typed-array", "npm:1.1.9"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.21.2", {\ "packageLocation": "./.yarn/cache/es-abstract-npm-1.21.2-f4ebace1ab-037f55ee5e.zip/node_modules/es-abstract/",\ "packageDependencies": [\ @@ -8986,7 +8708,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/cache/es-to-primitive-npm-1.2.1-b7a7eac6c5-4ead6671a2.zip/node_modules/es-to-primitive/",\ "packageDependencies": [\ ["es-to-primitive", "npm:1.2.1"],\ - ["is-callable", "npm:1.2.7"],\ + ["is-callable", "npm:1.2.4"],\ ["is-date-object", "npm:1.0.5"],\ ["is-symbol", "npm:1.0.4"]\ ],\ @@ -9159,96 +8881,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "SOFT"\ }],\ ["virtual:12a596dc9572e25ce715d3736dc74b317c5ca5cfb3b4f67864b1e76b3a401006f84d381aaf975bb5b4da9cafac5125e6869fc78d5202f4c95780c81479112f32#npm:14.2.1", {\ - "packageLocation": "./.yarn/__virtual__/eslint-config-airbnb-base-virtual-13e91749d9/0/cache/eslint-config-airbnb-base-npm-14.2.1-50131c00fb-858bea748a.zip/node_modules/eslint-config-airbnb-base/",\ - "packageDependencies": [\ - ["eslint-config-airbnb-base", "virtual:12a596dc9572e25ce715d3736dc74b317c5ca5cfb3b4f67864b1e76b3a401006f84d381aaf975bb5b4da9cafac5125e6869fc78d5202f4c95780c81479112f32#npm:14.2.1"],\ - ["@types/eslint", null],\ - ["@types/eslint-plugin-import", null],\ - ["confusing-browser-globals", "npm:1.0.10"],\ - ["eslint", "npm:7.32.0"],\ - ["eslint-plugin-import", "virtual:12a596dc9572e25ce715d3736dc74b317c5ca5cfb3b4f67864b1e76b3a401006f84d381aaf975bb5b4da9cafac5125e6869fc78d5202f4c95780c81479112f32#npm:2.25.3"],\ - ["object.assign", "npm:4.1.4"],\ - ["object.entries", "npm:1.1.6"]\ - ],\ - "packagePeers": [\ - "@types/eslint-plugin-import",\ - "@types/eslint",\ - "eslint-plugin-import",\ - "eslint"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:6af0d12a158a5376029b6f93868ad76c8cef559d686712b5fdba042f7ad0c5dda2a26841fa0c3de4742b982bbf3adffd70106d2304236284de339051257ae8c1#npm:15.0.0", {\ - "packageLocation": "./.yarn/__virtual__/eslint-config-airbnb-base-virtual-f7322ec32f/0/cache/eslint-config-airbnb-base-npm-15.0.0-802837dd26-38626bad2c.zip/node_modules/eslint-config-airbnb-base/",\ - "packageDependencies": [\ - ["eslint-config-airbnb-base", "virtual:6af0d12a158a5376029b6f93868ad76c8cef559d686712b5fdba042f7ad0c5dda2a26841fa0c3de4742b982bbf3adffd70106d2304236284de339051257ae8c1#npm:15.0.0"],\ - ["@types/eslint", null],\ - ["@types/eslint-plugin-import", null],\ - ["confusing-browser-globals", "npm:1.0.10"],\ - ["eslint", "npm:7.32.0"],\ - ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"],\ - ["object.assign", "npm:4.1.4"],\ - ["object.entries", "npm:1.1.6"],\ - ["semver", "npm:6.3.0"]\ - ],\ - "packagePeers": [\ - "@types/eslint-plugin-import",\ - "@types/eslint",\ - "eslint-plugin-import",\ - "eslint"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:14.2.1", {\ - "packageLocation": "./.yarn/__virtual__/eslint-config-airbnb-base-virtual-768b93baf9/0/cache/eslint-config-airbnb-base-npm-14.2.1-50131c00fb-858bea748a.zip/node_modules/eslint-config-airbnb-base/",\ - "packageDependencies": [\ - ["eslint-config-airbnb-base", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:14.2.1"],\ - ["@types/eslint", null],\ - ["@types/eslint-plugin-import", null],\ - ["confusing-browser-globals", "npm:1.0.10"],\ - ["eslint", "npm:7.32.0"],\ - ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"],\ - ["object.assign", "npm:4.1.4"],\ - ["object.entries", "npm:1.1.6"]\ - ],\ - "packagePeers": [\ - "@types/eslint-plugin-import",\ - "@types/eslint",\ - "eslint-plugin-import",\ - "eslint"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["eslint-config-airbnb-typescript", [\ - ["npm:17.0.0", {\ - "packageLocation": "./.yarn/cache/eslint-config-airbnb-typescript-npm-17.0.0-e1f8a377d2-e598ae7bcc.zip/node_modules/eslint-config-airbnb-typescript/",\ - "packageDependencies": [\ - ["eslint-config-airbnb-typescript", "npm:17.0.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:17.0.0", {\ - "packageLocation": "./.yarn/__virtual__/eslint-config-airbnb-typescript-virtual-6af0d12a15/0/cache/eslint-config-airbnb-typescript-npm-17.0.0-e1f8a377d2-e598ae7bcc.zip/node_modules/eslint-config-airbnb-typescript/",\ + "packageLocation": "./.yarn/__virtual__/eslint-config-airbnb-base-virtual-13e91749d9/0/cache/eslint-config-airbnb-base-npm-14.2.1-50131c00fb-858bea748a.zip/node_modules/eslint-config-airbnb-base/",\ "packageDependencies": [\ - ["eslint-config-airbnb-typescript", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:17.0.0"],\ + ["eslint-config-airbnb-base", "virtual:12a596dc9572e25ce715d3736dc74b317c5ca5cfb3b4f67864b1e76b3a401006f84d381aaf975bb5b4da9cafac5125e6869fc78d5202f4c95780c81479112f32#npm:14.2.1"],\ ["@types/eslint", null],\ ["@types/eslint-plugin-import", null],\ - ["@types/typescript-eslint__eslint-plugin", null],\ - ["@types/typescript-eslint__parser", null],\ - ["@typescript-eslint/eslint-plugin", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ - ["@typescript-eslint/parser", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ + ["confusing-browser-globals", "npm:1.0.10"],\ ["eslint", "npm:7.32.0"],\ - ["eslint-config-airbnb-base", "virtual:6af0d12a158a5376029b6f93868ad76c8cef559d686712b5fdba042f7ad0c5dda2a26841fa0c3de4742b982bbf3adffd70106d2304236284de339051257ae8c1#npm:15.0.0"],\ - ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"]\ + ["eslint-plugin-import", "virtual:12a596dc9572e25ce715d3736dc74b317c5ca5cfb3b4f67864b1e76b3a401006f84d381aaf975bb5b4da9cafac5125e6869fc78d5202f4c95780c81479112f32#npm:2.25.3"],\ + ["object.assign", "npm:4.1.4"],\ + ["object.entries", "npm:1.1.5"]\ ],\ "packagePeers": [\ "@types/eslint-plugin-import",\ "@types/eslint",\ - "@types/typescript-eslint__eslint-plugin",\ - "@types/typescript-eslint__parser",\ - "@typescript-eslint/eslint-plugin",\ - "@typescript-eslint/parser",\ "eslint-plugin-import",\ "eslint"\ ],\ @@ -9263,7 +8909,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["confusing-browser-globals", "npm:1.0.10"],\ ["eslint", "npm:7.32.0"],\ ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"],\ - ["object.assign", "npm:4.1.2"],\ + ["object.assign", "npm:4.1.4"],\ ["object.entries", "npm:1.1.6"],\ ["semver", "npm:6.3.0"]\ ],\ @@ -9284,7 +8930,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["confusing-browser-globals", "npm:1.0.10"],\ ["eslint", "npm:7.32.0"],\ ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"],\ - ["object.assign", "npm:4.1.2"],\ + ["object.assign", "npm:4.1.4"],\ ["object.entries", "npm:1.1.5"]\ ],\ "packagePeers": [\ @@ -9474,36 +9120,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "eslint"\ ],\ "linkType": "HARD"\ - }],\ - ["virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3", {\ - "packageLocation": "./.yarn/__virtual__/eslint-plugin-import-virtual-c35e0ef15e/0/cache/eslint-plugin-import-npm-2.25.3-f5faefaae3-8bdf4b1faf.zip/node_modules/eslint-plugin-import/",\ - "packageDependencies": [\ - ["eslint-plugin-import", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:2.25.3"],\ - ["@types/eslint", null],\ - ["@types/typescript-eslint__parser", null],\ - ["@typescript-eslint/parser", "virtual:ad53cff31b1dbd4927a99e71702e3b8b10338636eaff010987c27c9ccea2d52af36900a9e36a4231cbb6e5464248ccc9c1da5d1d24d9b0f4f95660296b1060a6#npm:5.55.0"],\ - ["array-includes", "npm:3.1.4"],\ - ["array.prototype.flat", "npm:1.2.5"],\ - ["debug", "virtual:0684f4c444aee59cc66233fcce3e4e9a25ed7e35886aed11393a5d4d03c3e7ec6f43fe70e91d783fa0717aa30724ce3a9a32ae0cd75006d103d8f81d5b9758c1#npm:2.6.9"],\ - ["doctrine", "npm:2.1.0"],\ - ["eslint", "npm:7.32.0"],\ - ["eslint-import-resolver-node", "npm:0.3.6"],\ - ["eslint-module-utils", "virtual:c35e0ef15e09d951132a1d1de0f686a9b852da3009392a39bfdef5e548591355b594808c6c5c6c2baa4339946ff8c9883343770439fdd88cdf8d08a9cc9843db#npm:2.7.1"],\ - ["has", "npm:1.0.3"],\ - ["is-core-module", "npm:2.8.1"],\ - ["is-glob", "npm:4.0.3"],\ - ["minimatch", "npm:3.1.2"],\ - ["object.values", "npm:1.1.5"],\ - ["resolve", "patch:resolve@npm%3A1.22.0#~builtin::version=1.22.0&hash=07638b"],\ - ["tsconfig-paths", "npm:3.12.0"]\ - ],\ - "packagePeers": [\ - "@types/eslint",\ - "@types/typescript-eslint__parser",\ - "@typescript-eslint/parser",\ - "eslint"\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["eslint-plugin-jsdoc", [\ @@ -9571,13 +9187,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:3.3.0", {\ - "packageLocation": "./.yarn/cache/eslint-visitor-keys-npm-3.3.0-d329af7c8c-d59e68a7c5.zip/node_modules/eslint-visitor-keys/",\ - "packageDependencies": [\ - ["eslint-visitor-keys", "npm:3.3.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:3.3.0", {\ "packageLocation": "./.yarn/cache/eslint-visitor-keys-npm-3.3.0-d329af7c8c-d59e68a7c5.zip/node_modules/eslint-visitor-keys/",\ "packageDependencies": [\ @@ -10184,8 +9793,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["function.prototype.name", "npm:1.1.5"],\ ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.1.3"],\ - ["es-abstract", "npm:1.19.1"],\ + ["define-properties", "npm:1.2.0"],\ + ["es-abstract", "npm:1.21.2"],\ ["functions-have-names", "npm:1.2.3"]\ ],\ "linkType": "HARD"\ @@ -10286,16 +9895,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["get-intrinsic", [\ - ["npm:1.2.0", {\ - "packageLocation": "./.yarn/cache/get-intrinsic-npm-1.2.0-eb08ea9b1d-78fc0487b7.zip/node_modules/get-intrinsic/",\ - "packageDependencies": [\ - ["get-intrinsic", "npm:1.2.0"],\ - ["function-bind", "npm:1.1.1"],\ - ["has", "npm:1.0.3"],\ - ["has-symbols", "npm:1.0.3"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/get-intrinsic-npm-1.2.0-eb08ea9b1d-78fc0487b7.zip/node_modules/get-intrinsic/",\ "packageDependencies": [\ @@ -10521,7 +10120,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/cache/globalthis-npm-1.0.3-96cd56020d-fbd7d760dc.zip/node_modules/globalthis/",\ "packageDependencies": [\ ["globalthis", "npm:1.0.3"],\ - ["define-properties", "npm:1.1.3"]\ + ["define-properties", "npm:1.2.0"]\ ],\ "linkType": "HARD"\ }]\ @@ -10689,13 +10288,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["has-bigints", [\ - ["npm:1.0.2", {\ - "packageLocation": "./.yarn/cache/has-bigints-npm-1.0.2-52732e614d-390e31e7be.zip/node_modules/has-bigints/",\ - "packageDependencies": [\ - ["has-bigints", "npm:1.0.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/has-bigints-npm-1.0.2-52732e614d-390e31e7be.zip/node_modules/has-bigints/",\ "packageDependencies": [\ @@ -10725,7 +10317,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/cache/has-property-descriptors-npm-1.0.0-56289b918d-a6d3f0a266.zip/node_modules/has-property-descriptors/",\ "packageDependencies": [\ ["has-property-descriptors", "npm:1.0.0"],\ - ["get-intrinsic", "npm:1.1.1"]\ + ["get-intrinsic", "npm:1.2.0"]\ ],\ "linkType": "HARD"\ }]\ @@ -10740,10 +10332,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["has-symbols", [\ - ["npm:1.0.3", {\ - "packageLocation": "./.yarn/cache/has-symbols-npm-1.0.3-1986bff2c4-a054c40c63.zip/node_modules/has-symbols/",\ + ["npm:1.0.2", {\ + "packageLocation": "./.yarn/cache/has-symbols-npm-1.0.2-50e53af115-2309c42607.zip/node_modules/has-symbols/",\ "packageDependencies": [\ - ["has-symbols", "npm:1.0.3"]\ + ["has-symbols", "npm:1.0.2"]\ ],\ "linkType": "HARD"\ }],\ @@ -10760,7 +10352,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/cache/has-tostringtag-npm-1.0.0-b1fcf3ab55-cc12eb28cb.zip/node_modules/has-tostringtag/",\ "packageDependencies": [\ ["has-tostringtag", "npm:1.0.0"],\ - ["has-symbols", "npm:1.0.3"]\ + ["has-symbols", "npm:1.0.2"]\ ],\ "linkType": "HARD"\ }]\ @@ -11211,16 +10803,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["internal-slot", [\ - ["npm:1.0.5", {\ - "packageLocation": "./.yarn/cache/internal-slot-npm-1.0.5-a2241f3e66-97e84046bf.zip/node_modules/internal-slot/",\ - "packageDependencies": [\ - ["internal-slot", "npm:1.0.5"],\ - ["get-intrinsic", "npm:1.2.0"],\ - ["has", "npm:1.0.3"],\ - ["side-channel", "npm:1.0.4"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.5", {\ "packageLocation": "./.yarn/cache/internal-slot-npm-1.0.5-a2241f3e66-97e84046bf.zip/node_modules/internal-slot/",\ "packageDependencies": [\ @@ -11337,10 +10919,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["is-callable", [\ - ["npm:1.2.7", {\ - "packageLocation": "./.yarn/cache/is-callable-npm-1.2.7-808a303e61-61fd57d03b.zip/node_modules/is-callable/",\ + ["npm:1.2.4", {\ + "packageLocation": "./.yarn/cache/is-callable-npm-1.2.4-03fc17459c-1a28d57dc4.zip/node_modules/is-callable/",\ "packageDependencies": [\ - ["is-callable", "npm:1.2.7"]\ + ["is-callable", "npm:1.2.4"]\ ],\ "linkType": "HARD"\ }],\ @@ -11474,13 +11056,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["is-negative-zero", [\ - ["npm:2.0.2", {\ - "packageLocation": "./.yarn/cache/is-negative-zero-npm-2.0.2-0adac91f15-f3232194c4.zip/node_modules/is-negative-zero/",\ - "packageDependencies": [\ - ["is-negative-zero", "npm:2.0.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:2.0.2", {\ "packageLocation": "./.yarn/cache/is-negative-zero-npm-2.0.2-0adac91f15-f3232194c4.zip/node_modules/is-negative-zero/",\ "packageDependencies": [\ @@ -11581,14 +11156,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["is-shared-array-buffer", [\ - ["npm:1.0.2", {\ - "packageLocation": "./.yarn/cache/is-shared-array-buffer-npm-1.0.2-32e4181fcd-9508929cf1.zip/node_modules/is-shared-array-buffer/",\ - "packageDependencies": [\ - ["is-shared-array-buffer", "npm:1.0.2"],\ - ["call-bind", "npm:1.0.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/is-shared-array-buffer-npm-1.0.2-32e4181fcd-9508929cf1.zip/node_modules/is-shared-array-buffer/",\ "packageDependencies": [\ @@ -11622,7 +11189,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./.yarn/cache/is-symbol-npm-1.0.4-eb9baac703-92805812ef.zip/node_modules/is-symbol/",\ "packageDependencies": [\ ["is-symbol", "npm:1.0.4"],\ - ["has-symbols", "npm:1.0.3"]\ + ["has-symbols", "npm:1.0.2"]\ ],\ "linkType": "HARD"\ }]\ @@ -11653,7 +11220,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["npm:1.1.8", {\ "packageLocation": "./.yarn/cache/is-typed-array-npm-1.1.8-147f090d0d-aa0f9f0716.zip/node_modules/is-typed-array/",\ "packageDependencies": [\ - ["is-typed-array", "npm:1.1.10"],\ + ["is-typed-array", "npm:1.1.8"],\ ["available-typed-arrays", "npm:1.0.5"],\ ["call-bind", "npm:1.0.2"],\ ["for-each", "npm:0.3.3"],\ @@ -11691,14 +11258,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["is-weakref", [\ - ["npm:1.0.2", {\ - "packageLocation": "./.yarn/cache/is-weakref-npm-1.0.2-ff80e8c314-95bd9a57cd.zip/node_modules/is-weakref/",\ - "packageDependencies": [\ - ["is-weakref", "npm:1.0.2"],\ - ["call-bind", "npm:1.0.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/is-weakref-npm-1.0.2-ff80e8c314-95bd9a57cd.zip/node_modules/is-weakref/",\ "packageDependencies": [\ @@ -14239,13 +13798,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["object-inspect", [\ - ["npm:1.12.3", {\ - "packageLocation": "./.yarn/cache/object-inspect-npm-1.12.3-1e7d20f5ff-dabfd824d9.zip/node_modules/object-inspect/",\ - "packageDependencies": [\ - ["object-inspect", "npm:1.12.3"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.12.3", {\ "packageLocation": "./.yarn/cache/object-inspect-npm-1.12.3-1e7d20f5ff-dabfd824d9.zip/node_modules/object-inspect/",\ "packageDependencies": [\ @@ -14284,17 +13836,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["object.assign", [\ - ["npm:4.1.4", {\ - "packageLocation": "./.yarn/cache/object.assign-npm-4.1.4-fb3deb1c3a-76cab513a5.zip/node_modules/object.assign/",\ - "packageDependencies": [\ - ["object.assign", "npm:4.1.4"],\ - ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.2.0"],\ - ["has-symbols", "npm:1.0.3"],\ - ["object-keys", "npm:1.1.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:4.1.4", {\ "packageLocation": "./.yarn/cache/object.assign-npm-4.1.4-fb3deb1c3a-76cab513a5.zip/node_modules/object.assign/",\ "packageDependencies": [\ @@ -14313,8 +13854,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["object.entries", "npm:1.1.5"],\ ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.1.3"],\ - ["es-abstract", "npm:1.19.1"]\ + ["define-properties", "npm:1.2.0"],\ + ["es-abstract", "npm:1.21.2"]\ ],\ "linkType": "HARD"\ }],\ @@ -14329,18 +13870,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["object.values", [\ - ["npm:1.1.5", {\ - "packageLocation": "./.yarn/cache/object.values-npm-1.1.5-f1de7f3742-0f17e99741.zip/node_modules/object.values/",\ - "packageDependencies": [\ - ["object.entries", "npm:1.1.6"],\ - ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.2.0"],\ - ["es-abstract", "npm:1.21.2"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["object.values", [\ ["npm:1.1.5", {\ "packageLocation": "./.yarn/cache/object.values-npm-1.1.5-f1de7f3742-0f17e99741.zip/node_modules/object.values/",\ @@ -15574,7 +15103,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["regexp.prototype.flags", "npm:1.4.3"],\ ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.1.3"],\ + ["define-properties", "npm:1.2.0"],\ ["functions-have-names", "npm:1.2.3"]\ ],\ "linkType": "HARD"\ @@ -15911,18 +15440,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["safe-regex2", [\ - ["npm:2.0.0", {\ - "packageLocation": "./.yarn/cache/safe-regex2-npm-2.0.0-eadecc9909-f5e182fca0.zip/node_modules/safe-regex2/",\ - "packageDependencies": [\ - ["safe-regex-test", "npm:1.0.0"],\ - ["call-bind", "npm:1.0.2"],\ - ["get-intrinsic", "npm:1.2.0"],\ - ["is-regex", "npm:1.1.4"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["safe-stable-stringify", [\ ["npm:1.1.1", {\ "packageLocation": "./.yarn/cache/safe-stable-stringify-npm-1.1.1-1c282e1c55-e32a30720e.zip/node_modules/safe-stable-stringify/",\ @@ -16751,29 +16268,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["string.prototype.trim", [\ - ["npm:1.2.7", {\ - "packageLocation": "./.yarn/cache/string.prototype.trim-npm-1.2.7-3fbaf3b9d2-05b7b2d6af.zip/node_modules/string.prototype.trim/",\ - "packageDependencies": [\ - ["string.prototype.trim", "npm:1.2.7"],\ - ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.2.0"],\ - ["es-abstract", "npm:1.21.2"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["string.prototype.trimend", [\ - ["npm:1.0.6", {\ - "packageLocation": "./.yarn/cache/string.prototype.trimend-npm-1.0.6-304246ecc1-0fdc34645a.zip/node_modules/string.prototype.trimend/",\ - "packageDependencies": [\ - ["string.prototype.trimend", "npm:1.0.6"],\ - ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.2.0"],\ - ["es-abstract", "npm:1.21.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.6", {\ "packageLocation": "./.yarn/cache/string.prototype.trimend-npm-1.0.6-304246ecc1-0fdc34645a.zip/node_modules/string.prototype.trimend/",\ "packageDependencies": [\ @@ -16786,16 +16281,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["string.prototype.trimstart", [\ - ["npm:1.0.6", {\ - "packageLocation": "./.yarn/cache/string.prototype.trimstart-npm-1.0.6-0926caea6c-89080feef4.zip/node_modules/string.prototype.trimstart/",\ - "packageDependencies": [\ - ["string.prototype.trimstart", "npm:1.0.6"],\ - ["call-bind", "npm:1.0.2"],\ - ["define-properties", "npm:1.2.0"],\ - ["es-abstract", "npm:1.21.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.6", {\ "packageLocation": "./.yarn/cache/string.prototype.trimstart-npm-1.0.6-0926caea6c-89080feef4.zip/node_modules/string.prototype.trimstart/",\ "packageDependencies": [\ @@ -17795,43 +17280,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["tsutils", [\ - ["npm:3.21.0", {\ - "packageLocation": "./.yarn/cache/tsutils-npm-3.21.0-347e6636c5-1843f4c1b2.zip/node_modules/tsutils/",\ - "packageDependencies": [\ - ["tsutils", "npm:3.21.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:3.21.0", {\ - "packageLocation": "./.yarn/__virtual__/tsutils-virtual-32b99c9531/0/cache/tsutils-npm-3.21.0-347e6636c5-1843f4c1b2.zip/node_modules/tsutils/",\ - "packageDependencies": [\ - ["tsutils", "virtual:0e22d802b65219681b64a9f99af596d56d444fb6f03cdf776b56a06fb9ddeefe4b0a611780f0b0eea0b47a1f1fba5a366d19cd6561bbc1e55271f08c190cd76f#npm:3.21.0"],\ - ["@types/typescript", null],\ - ["tslib", "npm:1.14.1"],\ - ["typescript", "patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=f456af"]\ - ],\ - "packagePeers": [\ - "@types/typescript",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:ad07d3460ae416a99f304e6734121d5396603792c47f7c9a1b9c5c798d407da37779ac7289222241db922614d91f9d569b0f43bc10c027fcc720138b2f32e9fc#npm:3.21.0", {\ - "packageLocation": "./.yarn/__virtual__/tsutils-virtual-5e94504d8c/0/cache/tsutils-npm-3.21.0-347e6636c5-1843f4c1b2.zip/node_modules/tsutils/",\ - "packageDependencies": [\ - ["tsutils", "virtual:ad07d3460ae416a99f304e6734121d5396603792c47f7c9a1b9c5c798d407da37779ac7289222241db922614d91f9d569b0f43bc10c027fcc720138b2f32e9fc#npm:3.21.0"],\ - ["@types/typescript", null],\ - ["tslib", "npm:1.14.1"],\ - ["typescript", null]\ - ],\ - "packagePeers": [\ - "@types/typescript",\ - "typescript"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["tunnel-agent", [\ ["npm:0.6.0", {\ "packageLocation": "./.yarn/cache/tunnel-agent-npm-0.6.0-64345ab7eb-05f6510358.zip/node_modules/tunnel-agent/",\ @@ -18012,17 +17460,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["unbox-primitive", [\ - ["npm:1.0.2", {\ - "packageLocation": "./.yarn/cache/unbox-primitive-npm-1.0.2-cb56a05066-b7a1cf5862.zip/node_modules/unbox-primitive/",\ - "packageDependencies": [\ - ["unbox-primitive", "npm:1.0.2"],\ - ["call-bind", "npm:1.0.2"],\ - ["has-bigints", "npm:1.0.2"],\ - ["has-symbols", "npm:1.0.3"],\ - ["which-boxed-primitive", "npm:1.0.2"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/unbox-primitive-npm-1.0.2-cb56a05066-b7a1cf5862.zip/node_modules/unbox-primitive/",\ "packageDependencies": [\ @@ -18222,7 +17659,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["inherits", "npm:2.0.4"],\ ["is-arguments", "npm:1.1.1"],\ ["is-generator-function", "npm:1.0.10"],\ - ["is-typed-array", "npm:1.1.10"],\ + ["is-typed-array", "npm:1.1.8"],\ ["safe-buffer", "npm:5.2.1"],\ ["which-typed-array", "npm:1.1.9"]\ ],\ @@ -18943,19 +18380,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["which-typed-array", [\ - ["npm:1.1.9", {\ - "packageLocation": "./.yarn/cache/which-typed-array-npm-1.1.9-9559c95dfc-fe0178ca44.zip/node_modules/which-typed-array/",\ - "packageDependencies": [\ - ["which-typed-array", "npm:1.1.9"],\ - ["available-typed-arrays", "npm:1.0.5"],\ - ["call-bind", "npm:1.0.2"],\ - ["for-each", "npm:0.3.3"],\ - ["gopd", "npm:1.0.1"],\ - ["has-tostringtag", "npm:1.0.0"],\ - ["is-typed-array", "npm:1.1.10"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.1.9", {\ "packageLocation": "./.yarn/cache/which-typed-array-npm-1.1.9-9559c95dfc-fe0178ca44.zip/node_modules/which-typed-array/",\ "packageDependencies": [\ diff --git a/.yarn/cache/@eslint-community-eslint-utils-npm-4.2.0-434cf92d50-82fdd1cc2a.zip b/.yarn/cache/@eslint-community-eslint-utils-npm-4.2.0-434cf92d50-82fdd1cc2a.zip deleted file mode 100644 index 39c9fa566c48bb24584f88f0d0b7c4b5e495d4c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61581 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 z 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 TXA$?l@%u0H{=nz={vQ1eZ9`8k diff --git a/.yarn/cache/fsevents-patch-2882183fbf-8.zip b/.yarn/cache/fsevents-patch-2882183fbf-8.zip deleted file mode 100644 index c4511f19bda176f6c1a39da1fbf5c56a981f61c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23675 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 C+4lDU diff --git a/.yarn/cache/has-symbols-npm-1.0.2-50e53af115-2309c42607.zip b/.yarn/cache/has-symbols-npm-1.0.2-50e53af115-2309c42607.zip new file mode 100644 index 0000000000000000000000000000000000000000..ece6cfd19c5757799a08cf36b510009e873c0f60 GIT binary patch literal 10027 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 SDG(6Y_kXDOR|tQt?*9NhJlP$`N_=i&o|sV z`EK> z`}|RJdj=x|OG^VoOOwCMAi!Y%_y_SGTg$e`$9=)Uz$kxb#%N+}?qF?aWNr1{+|)68 z0KStML-gtfehzBS#}6NyR0$jT6)~FO-6Wr^9=WUIty@~IAG&dzI};VvsHRn<);=wY zAvyp`3A%goY({{3`Ve!Cf&2w{yku#CAiQ&)s3!Fmi4E=SL9BjofpJk)=C@OAVt@V; zjLerLt~T_qfvmvQd*atR9~qG}@Uqjxxek$9>J}XMG$?9&xI%~ahiY=XT`s+|%VyV- zPL{P69uAUr!SImo_3|$v<-Vgh=H7bj1WdHZNO*M}{%Sb*$GZO6#wfw%sK0*w`u>*| zu(vd~axgcwvbHlZvUW1DGcYw_g8#D{p4xNG_vO&1U!=$OBz?U8qsRWU8yHRgpxGG# zq6Lxtm{COM4ru!6;UcB9n@)*(MdAFj3to3+lLxeL7bG{@Onv*@=JYVE2_D#PGngz! z|H(KV$CDS24sRbBygoe>6L+;QoH@~pLM38RBX0r&~ewWS#^q2x$CQf0W;vq2;)TiSZ#4m~)G zk_*+;nJP$J&4EZQBjriJ^ATIFdHY(@%m&q0pj#yOL@%c)A%p9mo7=`_y%?&ulg|oj z-O#Az4FLwm1qlX5{JVn6ND7O}y%W_@lD1xCMr_+tL*cCRnc(%&ae4_ICgxnIh0e9p6@)d!Al1*%pqAIHl>T_^C$5aO;3UBfWl$;Yh z>~V|P(su6)95cnRHRJtQ%4<(h^1!+c3?<|Wm%qpO&85dDBlg(yORFmILsB0K zFRYz1++=*;W!!;?{WO2KFw7K{ztD z@_WgwLo8+$p;8<~?C?}rs@?+@B?$3W?hKBx!)9-*?c=FEOl3CWzTEITjoR@ZZIfxX zYG%JkU#@g>hi}D&p8vAgsenCa@es-u>k;DOVha8C#(egUfAhue7DVcYj~v=?F-X%T zZiy-V`W^UX*l?ElqosAw=?b7}t>VP?A%^>SlSH3u6}ky_1sX~H zkj9!$CXIEfi2P;N_ItJ9=$IQlW%H z%h0Gz5p#m+jmB0YgwOOZr#2d4*@;96iznxO{dNhNF`d0eA)>A)mSOuiAA+CkiOrf=bZ{H5g=bd z2*80tb3p4tgkKyGAy_iXfnP5Q5(|mY)}Yg%$0ElVu)Ze*=H<=uiE&rr?9G#wyqnL5 z)5(yQY|smHuS(1XWk*Z|bg0oD3HA!c#)i6&Y-MK+69CSNg+1BWb1{vff{&Ce1xont zlX+msC>m}prcHbckb31a_uiqgt~O}~7g1Wq`=`cHQlt=Q?MajuT(c>_Hh;J;j=mUc z!(Z{Ns55#)rcmQox>G`3=RLS4DLVlX5dKjz>L-T8Aj&depnF4zKKkm&&~N$zMXuG$ z0`IltN2xhuJBxum-Idgwi<_o(H?8d250mO@gwv2XRyga>igh|o$~H?Kv?@Zng2?jQ zSwEU-%!QajN_pBu=BckzH+`Rzki) zhE790)7|ZMP<@kD9ATKj(jjS^0}J?)LfJSZ#E2x^BL9_^ORmc*8=5!N3Y|b?Y{oQ4 zZTEt-THsoZC92ur5ThfbgM_!g(IBY(IGFcdRoE*C3)scC$!4>pz3==ARQSEleKmEH zJ5a;w68IxA6Q6a7rR748mnPsA=LrD1}Q^7teP zhlW=beiIs3^r;pHWxy{aXLD62L%a0AFE4Awx%8LFapr5IFcv`@!gx!zEn=-K8|~?l zMh(IK!)diA7vmyClWnd3%*LxRw@5LS&T8yLjVkuac{e|4T!(*fU+MGqST4*5wQ0P( zz}y(%SkKc%?IX0THhX>Wy{CagsE`T|#vA-k4|#Nk{}C+_{mw%iOza(=#X+6uYSsSG zU|@(?|0N3e!}iI=>eThD7vG?G*YiVdU=L(cl<#F48mlM1?(!j_S!gThkQw?^u0hEg z>-6E%!(j^!%D<_98%2ja@5-P?Y;VFN3v62pH51TO#EDCvW#VGo@ z%WUGR1%#UkCu!iFe0|>w?<%QFBJ3@XkejUhEevD-NJq7bS;1k*Z%t&1)m8W`ebjE# zuG?0LwXFD_c-bG@V~gdqeiRR7jNQN{-P3zU#AdR6EeQUk@vT5mox!n-x*;Zy2>Hy5Rd;hGnQkS1;H z18r3mDB$Hk2+z@%NK6>^y-R@84fn-6nZ}w}M0QU%RZ?L+ZJ$EMUE-rS>#i}(d}Y=B zzN>MPxMzwX>s-Z_IP0#J_GPXs<%NL|pUr!ZcAo8waWJbT%{T)E=C^oqlRUT-d5U%y z=nwfyzz|R>&Uh3>Zwn%!q!eblX@Af+HbJOwW;HPg)ll{nLTC_O6YbLWRdxu9Hajzv ztiX)`a>*CQkO+lPq{uMHP+*B9xOH+bHlY*P%Qf-fB9PEzLL9qMbhFT0%5g~`V{*wc z-TT8Sgr`H0x;CzPcDRYAhvu%Spkv3hG(RC|l^sa(YsH|vQ=zx4)6>z3&6az~#O6qBUzm&vDU9r35ur`&^6nhE z*=dU}s}_Q^DS$m@72KFT$M_c9ctyYs``xKSl-dHi4Sc64pemBap%D3yag0SqJVa)t z(4qIra760DYW5lfU(+mM!8Oj@E~!+~GlVIrDpAXYA{>t@)5cll(yf=MxnKCmp;`_+ zZsd59LE@~L)fy5;xCO(FnEE&;up_&z5E@*du!1PwFv7sji2ty zQOc)iHc`m2QVeu;E!LFs@Btk}*$xSP*-f*mNC`Tx z_9E_YOjX7rP@={}DryfXn#^S4`kZQZDaBhWO2!tXbevX(>$?`XonWud)%ysUqw(Bj-e_b>|;UVVpmh3Smnhv zTkZ&_%}^{Up9Tr^x9f5-S(N^q22^>0*Fi1jbnO_*RgB!_jPCa?l8qYBgRj_pxLcOD zUql(KSU@=+%_B|0B25z*c0bP9>=EjdOaNd~)r8noqi+~C9VzZ-2dm@Kb z9(I=^&1fm{9-*vc2fTLY%z)u2gqU*cwa;^$zytg2;VbJMV_}_OA6a;PHg!^VlW&fN zns(|$ID^8Cr>m8*^%c&fIYnJAbdDQj9x|MmzK$Jk{mF#PmlCRhY4+ztJ+?(gpvvk` z+3U;D?~6WzaxmyXN0igJV`e;4 zS#p|0T7nrub!D20CJuw|G3rW3i^iKIoRj5}-iP({Re&p@8KPJ%O`#+JcRe-U)^a4& zY&#Sbxu2qC_-zB}BK9apdw1?%{4+=_Q%e?=e*p&e00Ra_@w+L|#=ywparycfCR$qs zs>oVTGo$dDdIu&TD5-F_EGCkYXT!8X{XT6xQFs=;^zeVRm$9h3;#yDss+$2f5x7o*-0LZ~<1BKqbJND$lKqF*dprq;2Q zGX3J*z`-cZ;D&$E z@}|4KGbT;TZmb1d0__&*rsRAf28IX8mA`+|1jp|n<|VP9y`@)xJY3>IAk^vMC1+D} z%`VnU>NF;h!Gj%_q96N%Wmd&!7uc37LRgKC#ILw7gO+8K>gUE^mdYhx*6uq+0bG#S z4L@yQMIbfQ7jdO3C&H(Me4Aq;y+fMtCl{x9SHbiRjVw}$j4LM5L8zB7ipC01+)3LX z6&PO~+8YdiqFegmnDih#fL!l`8rqOM@fd+_dO8MGad&Ms-Dl3zYzMu3%N*^vI20nI z-ad6!>`ZxF--cRg+6A4X1aTudf~dWZI)UuG@rlbR>-yi4hr)a2s@w&vDd#o^z;mp4 zH3*|D6FCOh#idzg*ELIn{m1f{7wC2yoz=l83x~vOS3U<4jT_WpFfZ)VB8sEt=e>=p zvzbc&sjgkKWl41Ps>G)nswHHcH!7VQ-7!@aL+cwiy2jZtipfbmPqQ)8JZWywEnHsR z{DPpcl-41f(E^*t%!3@s-;L6TTyMv(T=2`^q-29Vp5Dva-z!7R|@nG##6@M~#Tfy{1!^N!_#7y0 zcQl2g6jpT><;FcMeh@6FuOQ7bNaft%BIxV5foApt3&D1oo;6&XLClBd7c6GrnuhYj^-WU|&TE$y`JQVVo?CW| zD0T`@KMTzVx^o3c##4lJO6L*6>P#Y0V7-SIR*bR*RQ3$Upi@NQ>0Bo;V7l2t+m%I% zwhKufp;1B0sj5&QX`q~Jf5Lwttp^fLxqO=KADr7$Nh#oy_ z6N}r?i1nWkaQ53eI>6MOPCmnmbo#EWj`@xx0f$!RzO0uObv7(zh&$$82%J*r4aXdR zA~Su;8m9_ZPWy-3H1Sd<8+fLw_g#)O_o|EAjqvw-O$b*vs! zbMtgo8Z+u9G3d3g;-1ZGb)eE)AvXYE9Vs)Eg=p= zqWC0HyZZiY$Q%N-$JVp^&Enw;?=T9v0v+7bgg85f#$6gXZln-zzFuT$m)8UqC{K=A zSxcQaiXaYk&reEUTcjmYyyC}0yiD|bEutAqgcFsHIw&W}uF+!cY7Jv9e*uU1WS6%Z{w`j#xOu~s5}&H3F+$1R~hUp7`269%cbS+Ws z)|~MQ@*w(i?&D(dkJ*#PJH+vn+)_2VK;Ey)smWC;gFgb(4eF6j$WF@WEweWjNonCb)OXDV$p| z-@OOv4|y^|s6+CSd&CIdGMz+J+j0!>#XW49I(R~XzoR?d{J?7HTDPtuxi9pI-8Yow zVz&p7duFdTJW^?sseYVnLS$6F91I^?O&wJN%s&}~M2Z%TB9j$68(vF%2p?8KRk}VD zQj|SY*A`%D`-IxiULyyRLx=u&xWwaX{e31V>}(Cl7bJ%W zF2XXD%nMvzn*~#733{K>vga)Xc~2IT-PZE3coyQ9Mxp@|?}An=`1!fypmo0C0Yo<}k!h$z)bn43*3ovOXAhJi;gd%l?J3=2teySsOr zNBO-3ZZT#A??%*jtYGbC^gO*xzMIeuuY#t%w{Lb5+j_LcB}bqo#n=>`Eh-k9s7f}! zLuxSAO&}~o;oWUdt$${pn*xJ)#sBajl7nX|BSUe)-Cb{}^Tj-XL6PALpwhIyxdMqg zudiF5G(tLure)_H-|JjScAYOHw4?Mh)9jH6`FI!^iWC`}_5gV&?16o+NM$+MK<0cI zu1}00vtp|w*F)aT9e-)uz|^;X`2j#-i|!QL_K~FC7vXKysoJXDwfE!mNMsZcIzyL!*_E6 z^{(-^goQ+kp<+cRijrakgsaYv%eCE znn(ng*XmvZ)rUCYj!8%)buF0aZh~PmBpTPmE}ZK1VC6_ZlWAqSy*vpe9@1~shYel# zBWj1lL<`m@aW_+@b{X!a!+q7wkjt6XayIUiH*hE{(>B5WZsJ{Mu25>=RK`!Re@R2x@V|ZRwHlk1Zz@ydeHJ8v8Xbw>phDqfu z5TT$^vV?v#GOZW>Soq`WD|tXbeg0nBf%~C&)Df@Ti^d%{Wmqn3#8Fu^J(*=$FIGS! z@Hp@mJWVdDaB5)fd`Y%EAe(pfjm)M1Z#Xquh9#B`kz#k!ok+NO*wu8{&U64>)|yo~ z{Uy|-DN7t*+%g%CG{CQepLE~YNV@~UM;`X=2pGqb(WE|JEK|LCm}l&!Ca*+JarClR z^^)IE9Af6W2|))B)y6Tik$+4MIKJJBmA(xtts_P9$E?IigVJ{_rI)%pR$?`sQ4=t@ooE5h*ieBXaY zuh1yfXzG7|XAhm<%M2*Zz9+AX^VpFvu|Co==~wkTXKXa~!dJW#U`5RxZOiVg;_{i1 zEcawBuYI`DGIjijKaDj;v%Xuc(A-)D4jDCL^!AyLy!I)>R8p zNpU;#B%C&%1y&>8YDFFt2ptniPuu``CJtGNq9OVM(&1L#@51dlr0C&=`2o+#r%y2DVK7yZ=jPBx6&I)!X;?mO%Z zP}b^YwGd=Gj5iSX64+>`u^80B!qV6qq>pyz;bteG)@^hRrT5-C*dd5Us$Z=`cS&5} z=r_R%+Hf3&KX{3n?1M4QMg5W1oQizAs)Ur{fZ5MM@=Bs#0zFJ%#L#u-bE1U<8>g<4 zABn?sdvQWXygJK7iH5)f2)+?u1>o$Uxvr&rLyN8hJFZ!EL2jF8Tp&J^&s>9Jxp7gC z1FBsYVXh!vGaI0pN>Cdc3_zTJ`C{C}HCesdHFO!tZ9rFX*BFlYvP2$n+GQVzOF}nQ zQZd}tM&rn9H}Dv^1+UjIbCNBFhFw}+jyl-!ph_9V>e->7P8%Z*Ka~seJ6$Sac+V7! zS0-V|E)^Yrr4o<%!LN7s*V_7j+g|>y0K;f%?qKF<_-u!{l|zvg>#^U(9yJ{L-!09b z%*9mXL?q?JfAzlSc!$&r78K}J_e{ffVWzKP>mSA473rx(VqfY3Bef(3xw^rjqRa|uh8Pqe1yY{@nbzd*IX7Mo{ z!uq|CIjTA#kUG^^vIV7HgdaOW$3W%HA!EXvYjw@$N}h*q49r0ooFD`vyD!(XXS%PI z6bMq$ax0{5uKKr7e2l19ox@5n*s3XVn4gVJJshMC4-e>z3qWyI>n(}m2ODkvf)Vk3 zNtq#Os>i=Z*=SnuRc{(2lW}r_2zeI|l}BCuDb1^4vLnd+x^Edv;G(RTx4o{!xF4#EH8-?Jrtb=f1 zOn+rhJRJvubdR_HF#S&v{|WP?{y)#Ac*5lS8vYLRkKBqsng5=j@Hf-=aWwsF(f-OW z_%mDKc{!hFDg4c>`9Co2jGptJX8`=oD|{U7|6ayF@&TU1o~!hK!+z~-{VVJ*&Hg#< zxorA34e@bR{;#wrh35(P%j|hZ&M%GW={OK%eAH$C75A6o^yhee8ZFN?rzcjv?}z`& zdS1RK=JO4}znLtL8xa3mzW>|~e5&#j^7#(N-^ecX-yr{UKjS&_c})2?G63&4$p0T< zKIc83Q~%~o6aI$B_@6WE6Z4nt^PVPo0zO_31S21lT>iBspWfb2xTjWp{`!9+=KBu+ cUvU3187N3YJ&s&3FqFs7)ng_L{U4?Ke1z1#D_y1{-?vRp}Mx`XByBkRX$zg`>?vn2AMsh%8ke2Rl=@b;Clc3&sm>Yd!MyyeHCS3;c%gTmRA*%NPpe@^M!VIwFVlS=mUVp4pt_1tcw5j zQIdy`n%gnkyMRoLnGJ1i4PE}Rgn>f(>rX0Ph@q6;S&%_PLDB!glEuW>+#YCa1hjr@ zZt4&@fZWE0>3`r7ngJgc>Pvt@sf=;M=N*SR7ojW16=PL=aZD@5e`YtI{AG^Fl$ox^ zUl&$*&+Wr21|Ln=W}@Igo+g?m2@@u7Y4bXE$#0z6xbe1Rl!b;1naGZsGLFb1YftBp ziLS)(4qg4%TC2t-dnJ8Oq;{ILb$P4VUrAK>@};2)*L7Mn4ZCn@jlc)wqs@vcq?aZQ)GEpvYIHCHg)ue?K=rsq-Lw0W~ia@GzOr394JqWIUca#nYOE;%&1o%vvP7{&OvufY0-*IqDCP&{x@P*i{LRaQzwOkPDyS6K!)$A;Rx ztAY8l&U=L4Th~zqkUzOG9azTRr&JZM>1PC=P@Q$_nHLM*^@f6`_hcPytgnoDj65~_ zOtYbVC8KIQQ;*hZi)y~^^zg?~BJi+XA)SbVYiux_)Tl1~9Shv11PI!dcV+>Fg!YB_ z98G0r8T_XpFxOZsEhc}`H+XqRWTeB7X7gKJu!LqxpPRy^^y%ZQSQeHQgHmsj2#3}Mj{udHdWNVY`mgT8jR z?;b6``&#ho@THf5q$_%ow>vH9IDKyHQ@T<5;K$Yg0iQvvA74qTh>Bjmf00d+c?H*s z4l9BX)Vx`C@O6Wz+A}AhYc3SVfdwMyDZ2tQmsyBKvmd_AS7D`o1(=g0eY|vOct{XB zc~NB-_tf1~b~Se9T+ngQmj7UjN*kh`1)H|`-qj5mLWD3qGuNg_FlBiY@*>(j*xA_> zf&1Ki@=|aEc4reN#iE!?=TQ{ec!_I5a<@S%aT&oQtGvO|I)pUErt#NB2`zohS8*l@ z-e+ozBkYRwv>FZ+@F&K}_`9+R3WvcB)orX=D|F#`jWF0vN>U@3Gtx!upJN1jg?9jL z3Cf8bAIfk+MIT0=>k{Y)LDU^4km81-z;Wx!*`b;t%7R%8C$IR7pydX`eDetPbKS&l zo|NyD$0&RVY+`usGa%tJcxFUVp-jlY+@M1f^$pJp`$dT`ey7>q4m#(DEc|Vot9)&B znoh}5Wkx0(+2Uhjdn)2+D2?ju04B9MX~J?GtO;4)24x*Bvx&nml-$BU8nJ->QIGVV zXowhup7DfFU7`FteyNx;O4{z?7Yh*ziuw=nYi@09;>==U=b*ZzFvo`JX?=@KMS-n3 z8(wd&#r3ReQ#}V5jL7AntU&vf1hFXV_VVkX;|gvRrkcCclGEuzWS%9wWqEYITUtQ^@FB#yaA*w0BN!;6yP}5b>E+*ap(D$TO(NzquI`H^_t8iaZdwVf2 zZz56^e!x~dL)UL+n&x<^VBAU40dddLM$GEzK$;`UVpv|~?i9Rz{*k3XLgUz_)R?}@ zf%`mHxSP&Cmf-H*{PUq;c3WWG zqXq+gcyZ;)q24oyfs4SQclnUAMzFtcTK{loKp%#egr*R1K8=0Hle3oI)iGz3fe3b+ zVtVx4NhUy6n3HgW*zZXkS4+4`+gohZb1S##VJr(tdIqC|M>?1> znP^HG?W2X`oV8KLMrE5hFol`~afS^-$y_b)=L4-Hu+zbp z#RRCNQR4Q!udsC_kw$0SQZmOVjC7r&e@4LS2H2B?nnCZBs8eY>4w2~8=RfrbL{u2t zp-%GNw7W2%#dWKugt9&98Rr=_JY&{1apD}k>AjqPe!}dxEBxY1NP95}D`A#Sb*6nV zEd?D37~{nW#_F?g;nd4se~*E?aR2}IG8BIp1NJ6%_78R`!clFEbg)oR1gL*rv;Q)` zA4d@NzsHgMEwX|eh{3FnP9VJb$buBJ`T3bo!C)f1?S?ni!}066i#<1Qwa zSCDQ`WE&>o#n0-IgA}7blJ58%&WIUbMk65l!LwV#qjuZHB8j&OT4t39k1AC$E|?8n z?M$?k&D)5meIy0$Vxb}j+S(Hx=Y^b>k>>HFOhr(ZGH9gNcB$`{NkqDxgc3xr!`yDrNuPh z#V^PD0W>PG!GV`Bxl&GkS#HLEF^?8g%67hYoNFlR@OhMZ^@@Zv8*pu*rSMHAcsIA) z=${UAfsfQ5*K+uzu#|A}rDS5{%`f)R2w?nTC2XT%PH!jAxrRvdNRD{EfGsM@AUY zva$XsVgy738M8j>KKD1LrkO{YI4e2g@p_I$}n zB03?>d6(Gb)Z_Ezz`>DLjx$ORjf}tCLWLjCde@<@t#1h(o4T3UEG1(#3hNrgvQ}v3 z^xVq$(^SDm=yj)DkD6SI77Q~%Ox*Wuq4SBruUh$YLJP$2tr8{n~C3nRby?)SB};&BiWyyt@1SkI3?>D~74XP)u9Wef?7 z%{iBcZ|QU|m77#xCGgwKz^?9HlYT6;sQ& zta4xfHm_{t?EI13h76+^eD6iSdjsT@GJN5ZRTXV+uhsPnW^#nXHmbb`eE>j!;R99e zx}~HaQ~bP-gUEoy(^j?^)gmNgK~J`cy8h!(2piFdMNBMMCb{ycERtB^00KSXEo+qs z_32bDD+WkMTb=g4_@ejGo^B_}fpb)oDTLrYtj{3tN&Zqd(a8LF`pOFCLKPD;OU`c%S zl3XOy7FVxwh33V4vTT121A20%V&N%T&66q#;`aTtYKH1NMfIZleMLde_1>7hZwct) zjocZIHC9T$d5To3JAJj7qd-z=L!5a?J6MCMou!VRwfsest&p0I=&1H;LeZ6|ht@mr z<99TjlT@3Pbf@?PRh1lk5Z;yTiOSLM*+a@YIhI({1Q}n;jc_C+R{UUFWxRUi6R}>} z*AP(gGA?#aO44gfOR?4V4hqyn z-iX2B{1rdBt001sXm8a59xU3;$lJqeyISoIdu(O-=2DIdT73iMwJ~@e8R_tWwi}H$ zs}^vq=4bbSLDSfHDM!?@ADn_T0}Kr>L&J`4jh|MT>u6?9zm6DTUNePb#dqTsYSL+X zj>C?pF)vDWTzuH%j7Elw3|8`XSrbPcB7GwEd6&vCv|?mO^XklvhcDN^A_gco7L+Kk`M)WC@4?#KaYJ8Ng;U&FiR&;!B7}4~x`H z9Vw&bM|fyi8IAD|n`v&Wy5XTCL?mt8+74jR6=GX3#k z1I#B{n0J!Q=E`7d1$_UcqtG(=XpExc{ra_B%ZcYmoOnc&%R%{gjimr=Q{o`+Q(lZw zA5JpuF!VaX;28*`VD4e=4gh^EPTlf{NJ z&LLCOCLe4|0SU}5ZQp$SCmd&w-TGl#Wm`L`TPK=q%oW0#EDjNz)u8F=A{<*B#O73= z_LE5K4OB(~hL6ag0JDzyF%_PL6TUt;>R_^hE?;Y6ZjT$ZS!}$m=X%OK{sxLW)HcD3 z%sdOrx_APzxU{jeb;sucI}J9O#+g9{>=^XJu`I?)+L+`%#rB^aQ_>5&GcYG>;_Oq) z?1@b~zeQPx)b~=O(99apdMw0WTZ2sZ_EnR4lcWRCVIokV3yuJrPD(`Xaq-F;w}Ni% zEd25_8(PmiJMv{D2GFqC%gb`-Efv~H&fuiuCt1Sq=qVjt@~KAU!Ab34sQ{J+FddZfMFKWk8=XU22%sw z=$DaYLPG%m^p_y7({YoJ;#&jGMjW z*ok-z8(>Ek8+jwSRq=>J$a1Y8!Aig#%^V_?(JZNI)uBwIB*zR#RpIlhASmYjyM*T6 zFe~?~_*|0(RIl6fL>WD2^F~#ib{1sFiyuN$m#$4WH&@?;vY~&>L!0ZwMmWkwz!r57 z3F5#!){7=zb?doquIccuC$)|tEbe@%A=eqh!6crK1Ac?{+J>_`f}VGigNNWEoK}Dj z62m8FtPclgsS+9rf3PGsX8&5QR29)9+A-y6#WI(z^p=Fp6VPUO6txq4dSWQDg&k7| zU$-nCpr`0eNp15<5mA(oo|p6G&iDP)ksKx7lF;`Ioq^KXoAjFq#k6L~9B329#o-Me z#+9CK07WBZosRtWx9=S1_TDKx+jVm&a0cxbShxDKOE7;EqD>3BrpY_~faoJzf13Tt zDuQsTdU!{RFuCfo(u}Dn!u)c!JNKqDjN4E?@YpNN2?E2qzNdT{yIRB()*E0+zLpsF zu{UIQZQQ}#o|(g+dy=#h!MsA`C5?*D#z4WN3)(&y-P~C&i?A9_9=>pO1S>s7UzaDI zdTY6Fur*BkI=8K2MxrEqmpGj2V#V;8j&wiObakR2+!O+6!1OV;1Vbz6#B*VQ;J~Sr|*cU7sAS zR)!%ji2vqXV+5_1F=B@0I26Q9Nq64q!Z)Dk#x-ct5n^${SmnXbzWUU#H!;cfg52V5 zV#-?-jbSR{{p-I{mbg&&8O9=B6(sw+4H*RG&lyH#F(FYovAY>LJ^}DEh46XlQf1!1 zHX($1#gOz3E_+sN5-n}H6|h9w0f)=R#%jn>y3~BhBQY-uF{ce$dT#|^vnGM0&(+2_Nobk_}5t&yIvUu#gU>X_<3*Q zo=-R#c2uqzaTpcov?;Y6?uJ-36F)ah>GAJzhMrW?$U1ZD;OXlm;)8X6wOk9+{5?r3 zY%bXKuvYG9!OAX|EMJjbXg7~YptUp^nboD(j&Q1#o3AWKsJk3K(FWxO12O_`^YNGF za2j_8BbP^~>u)MzgxQ=`H(G_Q;_7@1RoAdG2~%odC2?z>#J?s(W!s@*`DXU~t;KT~ zlP9bsq`ASI(l1uIYY<;y$t6w9Kim4jJFLHEL?c{yF$hXt9Al?824c9{rC7Ei5k-ir z$h;k2PS>zj*Apf{y;K9W&z29=56|9ej*M&1+ohM9_BtqIi0EYJ@MpKJ_)M|Bs;Qu~U(Sh0wAo|N2+MjksUTAV;|4&XDXvP5knQ=lk4hTHczY#QuZ zTR`Wu+tO$SKWarp;#w^15t8j@GsLh46#zxDkIf$o8VH4P9`UqVH0PK)WV6b=?zZy3 zG`mXmEncWb-e;oG4ADqUuc^1DT;#vTvv92V2g$63*7GB^hXMW9<_O$S*b&V*Qc zYC&Ji8=2miPxuCVn$Co!&qwD9C!RyxcgPw|_FclzlU*gjjWO}mLu12l(?U7|oAB|6 z5Nsh4Fa6%vPeDF*#&wMHO zPdLgaHa}@&euoAY(27%G$sJcrpDEgDvf!G!?2PA};5nY4d%dvZLPtwB4W-1I2c>5J8+)ZGyg{`x&77?K6v+75) z41&O_WgI1vncV5_1DKPaKzpE->g?@#6ause9_2-!lm?jHe$oKS6ZBnSS5~*|=_F(N z6O;8~YoKgRv$S)gBq$@&rF!M+(Q>se%PVtvhY?V7#z8;uoshLu0_CpZP}3))bsKniq z@_bDGK>C+1_c!*x%U!>)8h3&9I|u*Ks{R(g9(wyw^!kM@`roj&Mh}q>HK|`noxAY= zt*3t}R1bL%b)#QAq`S5Bcf5a8q=&$ViTq!{?zz83sFmpV$onbzo_OE%htv8! gG0z9~@&88rk4aup1|H!j0ORg6a3_(P5&!)4f8yy2vj6}9 literal 0 HcmV?d00001 diff --git a/.yarn/cache/object.entries-npm-1.1.5-7a8fcbc43e-d658696f74.zip b/.yarn/cache/object.entries-npm-1.1.5-7a8fcbc43e-d658696f74.zip new file mode 100644 index 0000000000000000000000000000000000000000..716d52aa78fc8b98399fddc098bcd5b03ed2f4f5 GIT binary patch literal 13922 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 z zr7GyN>z3UUeQoEZzv{X!IC=OS6Rbbvhl9U4wwa%<_>foY6ML;tp$irf!PU{B4mQX^ zBb;6IzCgUWnEretJCpZBI?=n4Jk;>imCiimQqB;eqF9!^T+NhhYL-=P8DA*gOe3Rx zT%`U(baGJv)~;wKuEc0Uaytw^=?`oq!%E~XgKjf8) zq0)Q=M!MY+r}3M1P!$htrzh$=xzC>lSvV^&hqTsfh8TreY;jxmPobL%qwb06CbTE9#6@qztm07lvu$g=7Uhecy7e4Fl zi+QWAH>>Kpcg>(loX2N)Ujh?yC9SC|p&@of&5#1n6Y%rDkkoN;sODV-n{)O|&h0+E z&_G!Ai0*!V-kev2{k^hNUYK7-=z;ITWdQQ7)J+U=m7xvVPouHtHHperJp;07lRo9%JzyynhGTM$KvR;!Ti zD6tYTJ^<9VqQu)zy$NPoY0yTwjIHoHd<(qYxRj9U1c}}-&3vl~^J_`8cBsDD*<`Co zsg5d=$Mg}0P94u>1$OM_gndz)JB;wP#?W#bH>YeIR)msf3F^1e3p7E-pb|5uf@`9E zM+q)J*lmhJq;2bG-{{T9OCR0kGt5qkOT9n1{$!UfZMRpW*=w_W10%L+x!$J`iVbtn zuqfB}RulUB(2D9M=#{c&03~i&*cP_6>Z{y5UT8KZlWBgjH4;0w3i=`I9R1<-+*6fo zy)p5o8wV5_8YRopL+rJEE5r0X8o8PHUh}d6qw2=myiZVtwJYybd^fbIb#ItWxI2%5 z)$5*kB3|dP_zQ%S;e}uMl&#+ep1@()=}@b5MMfOFS?knxLJ&ip&_TpJz>U!foy)1j zQX6I#<~g37;WjCEw7!tFR0KV`!tv20OO2%Ty0F7Kkuj$2VlB~uS|3!!?Uqj7I&?%o zp=^eVV#>sW($nK!RJRS77n?}V`7YHQ=6<-}rRbR`s?3^6&L5EM#sKw5m{+C~XH)*U zO{pfyF+YWP(87bVr?h#$q*6Y0g_HA}Sqe`#2cnx~gcP{}qc{BuhlFKI0)ZF0__y~T z0?b(g!=D8Fd-&zhQe8-RKMc;DFdzfoYahb7q7ZM+u@-;5-l}96$em>Gan#~PB(@A3 zH*(L1@f~0|3^)s$BYsuQC~x%S^TlS39+rG7_|~qoG7?B(mDw}d@W5Uk9K~7t-UsPH zmRG?sDPM%UGf_}S#@$YO35Dp}H-_x_F@<~FSmrV@Y4RsNz)-6)5k2FPaX1B?bWy50 zJ})};jq}6&Y(XmZxBcWB?cGxM7}xuXFwzCICOM+vA{*8rO;ruY5RFVK+xsa*q4f2F z%JIyTqdD2ZZw+|@eQtsd5p?T*tb<$1ZaSaTMCkP?C_LfEG2k4s(A#Jeou^#yVf?v; zsUyr>sMj0PmBMyVJm#Fbh$b&`cprtaBRmg3at^k>BKi*-k{WrM(m6^Zfz7%!;;*LF+jej%qPl@+S-b>;2FP`hOaOdB6f61`_1pd0? zFO{&rfE>{O4*W|O`_jUfs@Gq@%-;b2k2v!s`la0S7aH`r{{EZuf2#0aYHVNfUn&}Z z@jadc_HX>Z^^GryFO`D7h|aJ6o%pB4f9VEaVqePaeqm$a{~h}uqPv&Sm!h{{P}S!j z7yX;}e@Wn80$)nXegP#B{~h>;^S=aUFTpPbO25F$$o~cSC;g{f=_U51(&86(=lOB% zzXjkwbr&zGFCX{*qSj&lJM}*w{=THXTzLMX`aGACf3x*}m!Yx}kWfFnp+EmEJYU(< H|9tm9`3y_& literal 0 HcmV?d00001 diff --git a/packages/js-drive/.env.example b/packages/js-drive/.env.example new file mode 100644 index 00000000000..c0afd31ff42 --- /dev/null +++ b/packages/js-drive/.env.example @@ -0,0 +1,71 @@ +# ABCI host and port to listen +ABCI_HOST=0.0.0.0 +ABCI_PORT=26658 + +DB_PATH=./db + +# GroveDB database file +GROVEDB_LATEST_FILE=${DB_PATH}/latest_state + +# Cache size for Data Contracts +DATA_CONTRACTS_GLOBAL_CACHE_SIZE=500 +DATA_CONTRACTS_BLOCK_CACHE_SIZE=200 + +# DashCore JSON-RPC host, port and credentials +# Read more: https://dashcore.readme.io/docs/core-api-ref-remote-procedure-calls +CORE_JSON_RPC_HOST=127.0.0.1 +CORE_JSON_RPC_PORT=9998 +CORE_JSON_RPC_USERNAME=dashrpc +CORE_JSON_RPC_PASSWORD=password + +# DashCore ZMQ host and port +CORE_ZMQ_HOST=127.0.0.1 +CORE_ZMQ_PORT=29998 +CORE_ZMQ_CONNECTION_RETRIES=16 + +NETWORK=testnet + +INITIAL_CORE_CHAINLOCKED_HEIGHT= + +# https://github.com/dashevo/dashcore-lib/blob/286c33a9d29d33f05d874c47a9b33764a0be0cf1/lib/constants/index.js#L42-L57 +VALIDATOR_SET_LLMQ_TYPE=100 + +# DPNS Contract + +DPNS_MASTER_PUBLIC_KEY= +DPNS_SECOND_PUBLIC_KEY= + +# Dashpay Contract + +DASHPAY_MASTER_PUBLIC_KEY= +DASHPAY_SECOND_PUBLIC_KEY= + +# Feature flags contract + +FEATURE_FLAGS_MASTER_PUBLIC_KEY= +FEATURE_FLAGS_SECOND_PUBLIC_KEY= + +# Masternode reward shares contract + +MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY= +MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY= + +# Withdrawals contract + +WITHDRAWALS_MASTER_PUBLIC_KEY= +WITHDRAWALS_SECOND_PUBLIC_KEY= + +# logging +LOG_STDOUT_LEVEL=info + +LOG_PRETTY_FILE_LEVEL=silent +LOG_PRETTY_FILE_PATH=/tmp/drive-pretty.log + +LOG_JSON_FILE_LEVEL=silent +LOG_JSON_FILE_PATH=/tmp/drive-json.log + +LOG_STATE_REPOSITORY=false + +NODE_ENV=production + +TENDERDASH_P2P_PORT=26656 diff --git a/packages/js-drive/.eslintrc b/packages/js-drive/.eslintrc new file mode 100644 index 00000000000..53708855109 --- /dev/null +++ b/packages/js-drive/.eslintrc @@ -0,0 +1,36 @@ +{ + "extends": "airbnb-base", + "env": { + "es2021": true, + "node": true + }, + "parser": "babel-eslint", + "rules": { + "no-plusplus": 0, + "eol-last": [ + "error", + "always" + ], + "no-continue": "off", + "class-methods-use-this": "off", + "no-await-in-loop": "off", + "no-restricted-syntax": [ + "error", + { + "selector": "LabeledStatement", + "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." + }, + { + "selector": "WithStatement", + "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." + } + ], + "curly": [ + "error", + "all" + ] + }, + "globals": { + "BigInt": true + } +} diff --git a/packages/js-drive/.mocharc.yml b/packages/js-drive/.mocharc.yml new file mode 100644 index 00000000000..4f6cc77a7d5 --- /dev/null +++ b/packages/js-drive/.mocharc.yml @@ -0,0 +1,4 @@ +exit: true +timeout: 5000 +file: + - ./lib/test/bootstrap.js diff --git a/packages/js-drive/CHANGELOG.md b/packages/js-drive/CHANGELOG.md new file mode 100644 index 00000000000..e0e0e75d9aa --- /dev/null +++ b/packages/js-drive/CHANGELOG.md @@ -0,0 +1,509 @@ +## [0.21.1](https://github.com/dashevo/js-drive/compare/v0.21.0...v0.21.1) (2021-10-28) + + +### Bug Fixes + +* getFeatureFlagForHeight must not try to fetch feature flags before they were created ([#575](https://github.com/dashevo/js-drive/issues/575)) + + + +# [0.21.0](https://github.com/dashevo/js-drive/compare/v0.20.0...v0.21.0) (2021-10-14) + + +### Features + +* support higher protocol version ([#571](https://github.com/dashevo/js-drive/issues/571)) +* set protocol version on `begin block` ([#558](https://github.com/dashevo/js-drive/issues/558)) +* comprehensive error codes ([#564](https://github.com/dashevo/js-drive/issues/564), [#572](https://github.com/dashevo/js-drive/issues/572)) +* multiproof for the identity non inclusion proof root tree ([#560](https://github.com/dashevo/js-drive/issues/560)) + + +### Bug Fixes + +* consensus logger wasn't set on error ([#567](https://github.com/dashevo/js-drive/issues/567)) +* previousRootTree not rebuilt on commit, resulting in a wrong proof ([#563](https://github.com/dashevo/js-drive/issues/563)) + + + +# [0.20.0](https://github.com/dashevo/js-drive/compare/v0.19.3...v0.20.0) (2021-07-22) + + +### Features + +* use latest version of Merk storage ([#546](https://github.com/dashevo/js-drive/issues/546)) +* remove chainlock SML verification in favor of more robust core verification ([#536](https://github.com/dashevo/js-drive/issues/536)) +* remove SML instant lock verification in favor of more robust core verification [#533](https://github.com/dashevo/js-drive/issues/533)) +* make compatible with Tenderdash v0.5 ([#527](https://github.com/dashevo/js-drive/issues/527)) +* add additional info to proofs ([#518](https://github.com/dashevo/js-drive/issues/518), [#523](https://github.com/dashevo/js-drive/issues/523), [#525](https://github.com/dashevo/js-drive/issues/525), [#540](https://github.com/dashevo/js-drive/issues/540), [#542](https://github.com/dashevo/js-drive/issues/542)) +* validator set rotation ([#446](https://github.com/dashevo/js-drive/issues/446), [#515](https://github.com/dashevo/js-drive/issues/515), [#517](https://github.com/dashevo/js-drive/issues/517), [#530](https://github.com/dashevo/js-drive/issues/530), [#531](https://github.com/dashevo/js-drive/issues/531), ) + + +### Bug Fixes + +* invalid instant lock if no blocks are produced ([#513](https://github.com/dashevo/js-drive/issues/513)) +* `getProofs` method was missing from `PublicKeyToIdentityIdStoreRootTreeLeaf` class ([#544](https://github.com/dashevo/js-drive/issues/544)) +* typo in trace output ([#516](https://github.com/dashevo/js-drive/issues/516)) + + +### BREAKING CHANGES + +* `document`, `dataContract`, `identity`, `identityIdsByPublicKeyHashes` and `identitiesByPublicKeyHashes` query handlers now returns Protobuf messages instead of cbor'ed data. data is not sent if a proof is requested +* `VALIDATOR_SET_LLMQ_TYPE` env is required +* due to changes in hashing algorithm `appHash` is no longer same and not reproducible for old blocks hence new nodes would not be able to sync +* removing SML IS lock verification make some previously invalid transactions valid +* not compatible with Tenderdash v0.4 +* new ABCI messages and types not compatible with previous ones + + + +## [0.19.3](https://github.com/dashevo/js-drive/compare/v0.19.2...v0.19.3) (2021-06-04) + + +### Bug Fixes + +* documents were deleted using wrong id ([#514](https://github.com/dashevo/js-drive/issues/514)) + + + +## [0.19.2](https://github.com/dashevo/js-drive/compare/v0.19.1...v0.19.2) (2021-05-25) + + +### Bug Fixes + +* InvalidQuery error due to feature flags ([#512](https://github.com/dashevo/js-drive/issues/512)) + + + +## [0.19.1](https://github.com/dashevo/js-drive/compare/v0.19.0...v0.19.1) (2021-05-13) + + +### Bug Fixes + +* feature flags contract height variable has had an invalid name ([#509](https://github.com/dashevo/js-drive/issues/509)) + + + +# [0.19.0](https://github.com/dashevo/js-drive/compare/v0.18.1...v0.19.0) (2021-05-05) + + +### Features + +* use Dash Core to verify chain locks ([#503](https://github.com/dashevo/js-drive/issues/503), [#505](https://github.com/dashevo/js-drive/issues/505), [#506](https://github.com/dashevo/js-drive/issues/506)) +* verify instant locks using Dash Core ([#499](https://github.com/dashevo/js-drive/issues/499), [#501](https://github.com/dashevo/js-drive/issues/501), [#492](https://github.com/dashevo/js-drive/issues/492), [#498](https://github.com/dashevo/js-drive/issues/498)) +* feature flags ([#491](https://github.com/dashevo/js-drive/issues/491), [#504](https://github.com/dashevo/js-drive/issues/504), [#485](https://github.com/dashevo/js-drive/issues/485)) +* output Core network on start ([#490](https://github.com/dashevo/js-drive/issues/490)) +* update js-dp-services-ctl to 0.19-dev ([#486](https://github.com/dashevo/js-drive/issues/486)) +* enable docker build npm cache ([#478](https://github.com/dashevo/js-drive/issues/478)) +* do not setup node if SKIP_TEST_SUITE option is set ([#480](https://github.com/dashevo/js-drive/issues/480)) +* remove regtest fallbacks ([#477](https://github.com/dashevo/js-drive/issues/477)) +* add `verifyInstantLock` in favor of `getSMLStore` method ([#474](https://github.com/dashevo/js-drive/issues/474)) + + +### Bug Fixes + +* error loading shared library libzmq.so.5 ([#483](https://github.com/dashevo/js-drive/issues/483)) +* blockExecutionContext header might be null ([#481](https://github.com/dashevo/js-drive/issues/481)) + + +### BREAKING CHANGES + +* running in standalone regtest mode is not supported anymore +* `fetchSMLStore` method has been removed +* See [DPP v0.19 breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.19.0) + + +# [0.18.1](https://github.com/dashevo/js-drive/compare/v0.18.0...v0.18.1) (2021-03-08) + + +### Documentation + +* polish changelog ([ac434e](https://github.com/dashevo/dapi/commit/ac434eea9e1588077445ac13a7f4c066a710a3ec)) + + + +# [0.18.0](https://github.com/dashevo/js-drive/compare/v0.17.14...v0.18.0) (2021-03-03) + + +### Bug Fixes + +* ABCI request length error still not parsing properly ([#476](https://github.com/dashevo/js-drive/issues/476)) + + +### Features + +* output ABCI connection error message ([ff3660a](https://github.com/dashevo/js-drive/commit/ff3660a638f0f4170ff3f7242f84c256e75fa4c6)) +* getProofs ABCI query endpoint ([#451](https://github.com/dashevo/js-drive/issues/451), [#462](https://github.com/dashevo/js-drive/issues/462)) + + + +## [0.17.14](https://github.com/dashevo/js-drive/compare/v0.18.0-dev.5...v0.17.14) (2021-02-20) + + +### Bug Fixes + +* can't parse ABCI request length error ([f55fac7](https://github.com/dashevo/js-drive/commit/f55fac7aced9334cf26e930cfaf23258cec66a9d)) + + + +## [0.17.13](https://github.com/dashevo/js-drive/compare/v0.17.12...v0.17.13) (2021-02-19) + + +### Features + +* reimplemented ABCI server for better reliability ([#475](https://github.com/dashevo/js-drive/issues/475)) + + + +## [0.17.12](https://github.com/dashevo/js-drive/compare/v0.17.11...v0.17.12) (2021-02-16) + + +### Features + +* better handle abci connection errors ([f4348e9](https://github.com/dashevo/js-drive/commit/f4348e944825dc9b554eec8dcf7752e972081b2a)) + + + +## [0.17.11](https://github.com/dashevo/js-drive/compare/v0.17.8...v0.17.11) (2021-02-16) + + +### Bug Fixes + +* stack overflow due to write on write error ([cb3e0ac](https://github.com/dashevo/js-drive/commit/cb3e0ac4212d95372c2b402496125afdf5e69cea)) + + + +## [0.17.10](https://github.com/dashevo/js-drive/compare/v0.17.8...v0.17.10) (2021-02-16) + + +### Bug Fixes + +* abci connection error writes to closed stream ([41a891a](https://github.com/dashevo/js-drive/commit/41a891a922bf2f924c543410dd6d19b3a3ba03d0)) + + + +## [0.17.9](https://github.com/dashevo/js-drive/compare/v0.17.8...v0.17.9) (2021-02-15) + + +### Features + +* robust error handling ([#473](https://github.com/dashevo/js-drive/issues/473)) +* use a different handler for ABCI connection error ([#465](https://github.com/dashevo/js-drive/issues/465), [b9d452a](https://github.com/dashevo/js-drive/commit/b9d452a20bdf75699fa532eb69af7500fc985045)) + + + +## [0.17.8](https://github.com/dashevo/js-drive/compare/v0.17.7...v0.17.8) (2021-02-11) + + +### Bug Fixes + +* could not resolve `previousBlockExecutionStoreTransactions` on query ([#470](https://github.com/dashevo/js-drive/issues/470)) + + +### Features + +* add `driveVersion` to every log output ([#469](https://github.com/dashevo/js-drive/issues/469)) +* await Node logger stream to be ended ([#471](https://github.com/dashevo/js-drive/issues/471)) +* distinguishing log data ([#472](https://github.com/dashevo/js-drive/issues/472)) + + + +## [0.17.7](https://github.com/dashevo/js-drive/compare/v0.17.6...v0.17.7) (2021-02-04) + + +### Features + +* disable state repository and merk logging by default ([#467](https://github.com/dashevo/js-drive/issues/467)) + + + +## [0.17.6](https://github.com/dashevo/js-drive/compare/v0.17.5...v0.17.6) (2021-01-26) + + +### Bug Fixes + +* only info log level is present in log streams ([#463](https://github.com/dashevo/js-drive/issues/463)) + + + +## [0.17.5](https://github.com/dashevo/js-drive/compare/v0.17.4...v0.17.5) (2021-01-21) + + +### Features + +* different logging levels ([#461](https://github.com/dashevo/js-drive/issues/461)) + + +### BREAKING CHANGES + +* `LOGGING_LEVEL` is ignored. Use `LOG_STDOUT_LEVEL`. + + + +## [0.17.4](https://github.com/dashevo/js-drive/compare/v0.17.3...v0.17.4) (2021-01-20) + + +### Bug Fixes + +* logger with context is not used in some cases ([#458](https://github.com/dashevo/js-drive/issues/458)) +* tx counters and logger were not reset ([#460](https://github.com/dashevo/js-drive/issues/460)) + + +### Features + +* log to human-readable and json files ([#459](https://github.com/dashevo/js-drive/issues/459)) + + + +## [0.17.3](https://github.com/dashevo/js-drive/compare/v0.17.2...v0.17.3) (2021-01-20) + + +### Features + +* better logging ([#456](https://github.com/dashevo/js-drive/issues/456)) + + + +## [0.17.2](https://github.com/dashevo/js-drive/compare/v0.17.1...v0.17.2) (2021-01-19) + + +### Bug Fixes + +* could not resolve 'previousBlockExecutionStoreTransactions' ([5a9dbff](https://github.com/dashevo/js-drive/commit/5a9dbffb05cfb85e6e394ed79538d979eb4a73a7)) +* ST isolation leads to non-deterministic results ([#455](https://github.com/dashevo/js-drive/issues/455)) +* handle rawChainLockMessage parsing errors ([#454](https://github.com/dashevo/js-drive/issues/454)) + + + +## [0.17.1](https://github.com/dashevo/js-drive/compare/v0.17.0...v0.17.1) (2021-01-12) + + +### Bug Fixes + +* duplicate MongoDB index name ([#453](https://github.com/dashevo/js-drive/issues/453)) + + + +# [0.17.0](https://github.com/dashevo/js-drive/compare/v0.16.1...v0.17.0) (2020-12-30) + + +### Features + +* introduce `DriveStateRepository#fetchSMLStore` ([#444](https://github.com/dashevo/js-drive/issues/444), [#445](https://github.com/dashevo/js-drive/issues/445)) +* update `dashcore-lib` ([#411](https://github.com/dashevo/js-drive/issues/411), [#442](https://github.com/dashevo/js-drive/issues/442), [#443](https://github.com/dashevo/js-drive/issues/443)) +* add old zmq client from DAPI ([#439](https://github.com/dashevo/js-drive/issues/439)) +* dashpay contract support ([#441](https://github.com/dashevo/js-drive/issues/441)) +* change merk to @dashevo/merk +* gracefull shutdown on SIGINT, SIGTERM, SIGQUIT and unhandled errors ([#427](https://github.com/dashevo/js-drive/issues/427)) +* handle core chain locked height ([#428](https://github.com/dashevo/js-drive/issues/428)) +* implement verify chainlock query handler ([#402](https://github.com/dashevo/js-drive/issues/402)) +* intermediate merk tree for the current block ([#429](https://github.com/dashevo/js-drive/issues/429)) +* pass latestCoreChainLock on block end ([#434](https://github.com/dashevo/js-drive/issues/434)) +* provide proofs for getIdentitiesByPublicKeyHashes endpoint ([#422](https://github.com/dashevo/js-drive/issues/422)) +* provide proofs for getIdentitiyIdsByPublicKeyHashes endpoint ([#419](https://github.com/dashevo/js-drive/issues/419)) +* provide proofs in ABCI query and DAPI getIdentity ([#415](https://github.com/dashevo/js-drive/issues/415)) +* set IDENTITY_SKIP_ASSET_LOCK_CONFIRMATION_VALIDATION to false ([#437](https://github.com/dashevo/js-drive/issues/437)) +* sort keys for MerkDB ([#413](https://github.com/dashevo/js-drive/issues/413)) +* store ChainInfo in MerkDb ([#404](https://github.com/dashevo/js-drive/issues/404)) +* store Data Contracts in merk tree ([#405](https://github.com/dashevo/js-drive/issues/405)) +* store documents in MerkDb ([#410](https://github.com/dashevo/js-drive/issues/410)) +* store height in externalStorage instead of merkDB ([#433](https://github.com/dashevo/js-drive/issues/433)) +* store identities in merk tree ([#400](https://github.com/dashevo/js-drive/issues/400)) +* store Public Key to Identity ID in MerkDb ([#409](https://github.com/dashevo/js-drive/issues/409)) +* update `dpp` to include asset lock verification logic ([#432](https://github.com/dashevo/js-drive/issues/432)) +* introduce merkle forest ([#401](https://github.com/dashevo/js-drive/issues/401)) +* move block execution context out of blockchain state ([#403](https://github.com/dashevo/js-drive/issues/403)) +* add abstraction for MerkDb ([#407](https://github.com/dashevo/js-drive/issues/407)) + + +### Bug Fixes + +* hash was used as a Buffer where it should be hex string ([#440](https://github.com/dashevo/js-drive/issues/440)) +* documents DB transaction is already started error ([#417](https://github.com/dashevo/js-drive/issues/417)) +* e.getErrors is not a function error ([#418](https://github.com/dashevo/js-drive/issues/418)) +* missing nested indexed fields and transaction ([#426](https://github.com/dashevo/js-drive/issues/426)) + + +### BREAKING CHANGES + +* AppHash is not equal to nils anymore. +* data created with 0.16 and lower versions of Drive is not compatible anymore +* ABCI query responses are changed + + + +## [0.16.1](https://github.com/dashevo/js-drive/compare/v0.16.0...v0.16.1) (2020-10-29) + + +### Bug Fixes + +* `header` is not present in `RequestEndBlock` ([#399](https://github.com/dashevo/js-drive/issues/399)) + + + +# [0.16.0](https://github.com/dashevo/js-drive/compare/v0.15.0...v0.16.0) (2020-10-28) + + +### Bug Fixes + +* incorrect deliver state transition hash logging ([#396](https://github.com/dashevo/js-drive/issues/396)) + + +### Features + +* verify DPNS contract existence ([#397](https://github.com/dashevo/js-drive/issues/397)) +* add `LoggedStateRepositoryDecorator` ([#393](https://github.com/dashevo/js-drive/issues/393)) +* debug mode to respond internal error with message and stack ([#383](https://github.com/dashevo/js-drive/issues/383)) +* implement `fetchIdentityIdsByPublicKeys` method ([#385](https://github.com/dashevo/js-drive/issues/385)) +* implement `storeIdentityPublicKeyHashes` method ([#387](https://github.com/dashevo/js-drive/issues/387)) +* implement getting identities by multiple public keys hashes ([#388](https://github.com/dashevo/js-drive/issues/388), [#395](https://github.com/dashevo/js-drive/issues/395), [#386](https://github.com/dashevo/js-drive/issues/386)) +* update DPP to 0.16.0 ([#392](https://github.com/dashevo/js-drive/issues/392)) + + +### Refactoring + +* remove unnecessary InvalidDocumentTypeError handling ([#384](https://github.com/dashevo/js-drive/issues/384)) + + +### BREAKING CHANGES + +* If `DPNS_CONTRACT_ID` is set it requires `DPNS_CONTRACT_BLOCK_HEIGHT` to be set too. +* See [DPP v0.16 breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.16.0) + + + +# [0.15.0](https://github.com/dashevo/js-drive/compare/v0.14.0...v0.15.0) (2020-09-04) + + +### Bug Fixes + +* internal errors are not logged ([#380](https://github.com/dashevo/js-drive/issues/380)) +* unique index throws duplicate key error (#378) + + +### Features + +* handle protocol and software versions ([#377](https://github.com/dashevo/js-drive/issues/377)) +* handle user-defined binary fields ([#373](https://github.com/dashevo/js-drive/issues/373), [#381](https://github.com/dashevo/js-drive/issues/381)) + + +### BREAKING CHANGES + +* protocol version (`AppVersion`) is required in a Tendermint block header +* the previous state is not compatible due to new DPP serialization format +* See [DPP breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.15.0) + + + +# [0.14.0](https://github.com/dashevo/drive/compare/v0.13.2...v0.14.0) (2020-07-23) + + +### Features + +* increase MongoDB query allowed field length ([#366](https://github.com/dashevo/drive/issues/366)) +* logging of block execution process ([#365](https://github.com/dashevo/drive/issues/365)) +* use test suite to run functional and e2e tests ([#362](https://github.com/dashevo/drive/issues/362)) +* update to DPP v0.14 with timestamps ([#363](https://github.com/dashevo/drive/issues/363)) + + +### BREAKING CHANGES + +* See [DPP v0.14 breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.14.0) + + + +## [0.13.2](https://github.com/dashevo/drive/compare/v0.13.0-dev.2...v0.13.2) (2020-06-12) + + +### Bug Fixes + +* internal errors lead to inability to fix bugs as it leads to a state inconsistency ([#360](https://github.com/dashevo/drive/issues/360)) + + + +## [0.13.1](https://github.com/dashevo/drive/compare/v0.13.0...v0.13.1) (2020-06-12) + + +### Bug Fixes + +* document repository not created properly due to missing `await` ([#358](https://github.com/dashevo/drive/issues/358)) + + + +# [0.13.0](https://github.com/dashevo/drive/compare/v0.12.1...v0.13.0) (2020-06-08) + + +### Features + +* update to DPP 0.13 ([#336](https://github.com/dashevo/drive/issues/336), [#338](https://github.com/dashevo/drive/issues/338), [#340](https://github.com/dashevo/drive/issues/340), [#344](https://github.com/dashevo/drive/issues/344), [#346](https://github.com/dashevo/drive/issues/346), [#348](https://github.com/dashevo/drive/issues/348), [#354](https://github.com/dashevo/drive/issues/354), [#357](https://github.com/dashevo/drive/issues/357)) +* wait mongoDB replica set initialization ([#349](https://github.com/dashevo/drive/issues/349)) +* wait for Core to be synced before starting ([#345](https://github.com/dashevo/drive/issues/345), [#353](https://github.com/dashevo/drive/issues/353), [#356](https://github.com/dashevo/drive/issues/356)) +* get identity by public key endpoints ([#341](https://github.com/dashevo/drive/issues/341)) +* store identity id with identity's public key as a DB key ([#337](https://github.com/dashevo/drive/issues/337), [#339](https://github.com/dashevo/drive/issues/339)) + + +### Code Refactoring + +* use async function with cache to connect and get `MongoClient` ([#350](https://github.com/dashevo/drive/issues/350)) + + +### BREAKING CHANGES + +* see [DPP breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.13.0) + + + +## [0.12.2](https://github.com/dashevo/drive/compare/v0.12.1...v0.12.2) (2020-05-21) + + +### Bug Fixes + +* validateFee error handling expects only BalanceIsNotEnoughError ([#343](https://github.com/dashevo/drive/issues/343)) + + + +## [0.12.1](https://github.com/dashevo/drive/compare/v0.12.0...v0.12.1) (2020-04-22) + + +### Features + +* update `dpp` version to `0.12.1` ([#335](https://github.com/dashevo/drive/issues/335)) + + +# [0.12.0](https://github.com/dashevo/drive/compare/v0.11.1...v0.12.0) (2020-04-18) + +### Features + +* publish docker image with tag for every Semver segment ([#332](https://github.com/dashevo/drive/issues/332)) +* introduce ABCI and Machine logic, remove API and upgrade to DPP 0.12 ([#328](https://github.com/dashevo/drive/issues/328)) +* validate fee, reduce balance and move fees to distribution pool ([#329](https://github.com/dashevo/drive/issues/329)) + +### BREAKING CHANGES + +* JSON RPC and gRPC endpoints are removed. Use Tendermint ABCI query endpoint in order to fetch data +* see [DPP breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.12.0) + + +## [0.11.1](https://github.com/dashevo/drive/compare/v0.11.0...v0.11.1) (2020-03-17) + +### Bug Fixes + +* do not validate ST second time in `applyStateTransition` ([d296608](https://github.com/dashevo/drive/commit/d29660886deb7e5556c5346da54506aebc005bfa)) +* check for MongoDb replica set on start ([286074f](https://github.com/dashevo/drive/commit/286074fe297bb693ffe7492523e560aeb2512330)) + +# [0.11.0](https://github.com/dashevo/drive/compare/v0.7.0...v0.11.0) (2020-03-09) + +### Bug Fixes + +* prevent to update dependencies with major version `0` to minor versions ([9f1dd95](https://github.com/dashevo/drive/commit/9f1dd95fe2294de2d0a3157807eec9598d0f0db7)) + +### Features + +* upgrade DPP to v0.11 ([9797e51](https://github.com/dashevo/drive/commit/9797e51bee6899c07aabcf733fa54650037c42cd)) + +### Chore + +* update gRPC errors ([1d31326](https://github.com/dashevo/drive/commit/1d31326977b2b5f1537426d9d31d89f459aaace6)) + +### BREAKING CHANGES + +* see [DPP breaking changes](https://github.com/dashevo/js-dpp/releases/tag/v0.11.0) diff --git a/packages/js-drive/Dockerfile b/packages/js-drive/Dockerfile new file mode 100644 index 00000000000..0dfb7f69180 --- /dev/null +++ b/packages/js-drive/Dockerfile @@ -0,0 +1,114 @@ +# syntax = docker/dockerfile:1.3 +FROM node:16-alpine3.16 as builder + +ARG NODE_ENV=production +ENV NODE_ENV ${NODE_ENV} + +ARG CARGO_BUILD_PROFILE=debug +ENV CARGO_BUILD_PROFILE ${CARGO_BUILD_PROFILE} + +RUN apk update && \ + apk --no-cache upgrade && \ + apk add --no-cache git \ + openssh-client \ + linux-headers \ + python3 \ + alpine-sdk \ + cmake \ + zeromq-dev \ + ca-certificates \ + gcc \ + clang \ + libc-dev \ + binutils \ + bash + +# Install Rust +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=stable + +RUN set -eux; \ + apkArch="$(apk --print-arch)"; \ + case "$apkArch" in \ + x86_64) rustArch='x86_64-unknown-linux-musl'; rustupSha256='bdf022eb7cba403d0285bb62cbc47211f610caec24589a72af70e1e900663be9' ;; \ + aarch64) rustArch='aarch64-unknown-linux-musl'; rustupSha256='89ce657fe41e83186f5a6cdca4e0fd40edab4fd41b0f9161ac6241d49fbdbbbe' ;; \ + *) echo >&2 "unsupported architecture: $apkArch"; exit 1 ;; \ + esac; \ + url="https://static.rust-lang.org/rustup/archive/1.24.3/${rustArch}/rustup-init"; \ + wget "$url"; \ + echo "${rustupSha256} *rustup-init" | sha256sum -c -; \ + chmod +x rustup-init; \ + ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \ + rm rustup-init; \ + chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ + rustup --version; \ + cargo --version; \ + rustc --version; + +# Enable corepack https://github.com/nodejs/corepack +RUN corepack enable + +# Print build output +RUN yarn config set enableInlineBuilds true + +WORKDIR /platform + +# Copy yarn and Cargo files +COPY .yarn /platform/.yarn +COPY .cargo /platform/.cargo +COPY package.json yarn.lock .yarnrc.yml .pnp.* Cargo.toml Cargo.lock rust-toolchain.toml ./ + +# Copy only necessary packages from monorepo +COPY packages/js-drive packages/js-drive +COPY packages/rs-dpp packages/rs-dpp +COPY packages/wasm-dpp packages/wasm-dpp +COPY packages/rs-drive packages/rs-drive +COPY packages/rs-drive-abci packages/rs-drive-abci +COPY packages/rs-platform-value packages/rs-platform-value +COPY packages/rs-drive-nodejs packages/rs-drive-nodejs +COPY packages/dapi-grpc packages/dapi-grpc +COPY packages/js-dpp packages/js-dpp +COPY packages/js-grpc-common packages/js-grpc-common +COPY packages/dashpay-contract packages/dashpay-contract +COPY packages/dpns-contract packages/dpns-contract +COPY packages/feature-flags-contract packages/feature-flags-contract +COPY packages/masternode-reward-shares-contract packages/masternode-reward-shares-contract +COPY packages/withdrawals-contract packages/withdrawals-contract +COPY packages/data-contracts packages/data-contracts + +# Build RS Drive Node.JS binding +RUN --mount=type=cache,target=target \ + --mount=type=cache,target=$CARGO_HOME/git \ + --mount=type=cache,target=$CARGO_HOME/registry \ + yarn workspace @dashevo/rs-drive build + +# Install Drive-specific dependencies using previous +# node_modules directory to reuse built binaries +RUN --mount=type=cache,target=/tmp/unplugged \ + cp -R /tmp/unplugged /platform/.yarn/ && \ + yarn workspaces focus --production @dashevo/drive && \ + cp -R /platform/.yarn/unplugged /tmp/ + +FROM node:16-alpine3.16 + +ARG NODE_ENV=production +ENV NODE_ENV ${NODE_ENV} + +LABEL maintainer="Dash Developers " +LABEL description="Drive Node.JS" + +# Install ZMQ shared library +RUN apk update && apk add --no-cache zeromq-dev + +WORKDIR /platform + +COPY --from=builder /platform /platform + +# Remove Rust sources +RUN rm -rf packages/rs-drive packages/rs-drive-abci packages/rs-dpp packages/rs-platform-value + +RUN cp /platform/packages/js-drive/.env.example /platform/packages/js-drive/.env + +EXPOSE 26658 diff --git a/packages/js-drive/LICENSE b/packages/js-drive/LICENSE new file mode 100644 index 00000000000..54f58e74bf7 --- /dev/null +++ b/packages/js-drive/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017-2023 Dash Core Group, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/js-drive/README.md b/packages/js-drive/README.md new file mode 100644 index 00000000000..23763e50481 --- /dev/null +++ b/packages/js-drive/README.md @@ -0,0 +1,55 @@ +# Drive + +[![Latest Release](https://img.shields.io/github/v/release/dashpay/platform)](https://github.com/dashpay/platform/releases/latest) +[![Build Status](https://github.com/dashpay/platform/actions/workflows/release.yml/badge.svg)](https://github.com/dashpay/platform/actions/workflows/release.yml) +[![Release Date](https://img.shields.io/github/release-date/dashpay/platform)](https://github.com/dashpay/platform/releases/latest) +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen)](https://github.com/RichardLitt/standard-readme) + +Replicated state machine for Dash Platform + +Drive is the storage component of Dash Platform, allowing developers to store and secure their application data through Dash's masternode network. Application data structures are defined by a data contract, which is stored on Drive and used to verify/validate updates to your application data. + +## Table of Contents +- [Install](#install) +- [Usage](#usage) +- [Configuration](#configuration) +- [Tests](#tests) +- [Maintainer](#maintainer) +- [Contributing](#contributing) +- [License](#license) + +## Install + +1. [Install Node.JS 12 or higher](https://nodejs.org/en/download/) +2. Copy `.env.example` to `.env` file +3. Install npm dependencies: `npm install` + +## Usage + +```bash +npm run abci +``` + +## Configuration + +Drive uses environment variables for configuration. +Variables are read from `.env` file and can be overwritten by variables +defined in env or directly passed to the process. + +See all available settings in [.env.example](.env.example). + +## Tests + +[Read](test/) about tests in `test/` folder. + +## Maintainer + +[@shumkov](https://github.com/shumkov) + +## Contributing + +Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs. + +## License + +[MIT](LICENSE) © Dash Core Group, Inc. diff --git a/packages/js-drive/db/.gitignore b/packages/js-drive/db/.gitignore new file mode 100644 index 00000000000..593bcf0e80e --- /dev/null +++ b/packages/js-drive/db/.gitignore @@ -0,0 +1,2 @@ +!.gitignore +* diff --git a/packages/js-drive/lib/abci/closeAbciServerFactory.js b/packages/js-drive/lib/abci/closeAbciServerFactory.js new file mode 100644 index 00000000000..141cdeb9b3b --- /dev/null +++ b/packages/js-drive/lib/abci/closeAbciServerFactory.js @@ -0,0 +1,25 @@ +const { promisify } = require('util'); + +/** + * + * @param {net.Server} abciServer + * @return {closeAbciServer} + */ +function closeAbciServerFactory(abciServer) { + /** + * @typedef {closeAbciServer} + * @return {Promise} + */ + async function closeAbciServer() { + if (!abciServer.listening) { + return; + } + + const close = promisify(abciServer.close.bind(abciServer)); + await close(); + } + + return closeAbciServer; +} + +module.exports = closeAbciServerFactory; diff --git a/packages/js-drive/lib/abci/errors/AbstractAbciError.js b/packages/js-drive/lib/abci/errors/AbstractAbciError.js new file mode 100644 index 00000000000..ae054bc477d --- /dev/null +++ b/packages/js-drive/lib/abci/errors/AbstractAbciError.js @@ -0,0 +1,68 @@ +const cbor = require('cbor'); + +const DriveError = require('../../errors/DriveError'); + +/** + * @abstract + */ +class AbstractAbciError extends DriveError { + /** + * + * @param {number} code + * @param {string} message + * @param {Object} data + */ + constructor(code, message, data) { + super(message); + + this.code = code; + this.data = data; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } + + /** + * Get error code + * + * @returns {number} + */ + getCode() { + return this.code; + } + + /** + * Get error data + * + * @returns {Object} + */ + getData() { + return this.data; + } + + /** + * @returns {{code: number, info: string}} + */ + getAbciResponse() { + const info = { + message: this.getMessage(), + }; + + const data = this.getData(); + + if (Object.keys(data).length > 0) { + info.data = data; + } + + return { + code: this.getCode(), + info: cbor.encode(info).toString('base64'), + }; + } +} + +module.exports = AbstractAbciError; diff --git a/packages/js-drive/lib/abci/errors/DPPValidationAbciError.js b/packages/js-drive/lib/abci/errors/DPPValidationAbciError.js new file mode 100644 index 00000000000..7fcf434a5c0 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/DPPValidationAbciError.js @@ -0,0 +1,44 @@ +const cbor = require('cbor'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class DPPValidationAbciError extends AbstractAbciError { + /** + * + * @param {string} message + * @param {AbstractConsensusError} consensusError + */ + constructor(message, consensusError) { + const args = consensusError.getConstructorArguments(); + + const data = { }; + if (args.length > 0) { + data.arguments = args; + } + + super(consensusError.getCode(), message, data); + } + + /** + * @returns {{code: number, info: string}} + */ + getAbciResponse() { + const info = { }; + + const data = this.getData(); + + let encodedInfo; + if (Object.keys(data).length > 0) { + info.data = data; + + encodedInfo = cbor.encode(info).toString('base64'); + } + + return { + code: this.getCode(), + info: encodedInfo, + }; + } +} + +module.exports = DPPValidationAbciError; diff --git a/packages/js-drive/lib/abci/errors/InternalAbciError.js b/packages/js-drive/lib/abci/errors/InternalAbciError.js new file mode 100644 index 00000000000..bc5ac65b1c4 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/InternalAbciError.js @@ -0,0 +1,25 @@ +const grpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class InternalAbciError extends AbstractAbciError { + /** + * + * @param {Error} error + * @param {Object} [data] + */ + constructor(error, data = {}) { + super(grpcErrorCodes.INTERNAL, 'Internal error', data); + + this.error = error; + } + + /** + * @returns {Error} + */ + getError() { + return this.error; + } +} + +module.exports = InternalAbciError; diff --git a/packages/js-drive/lib/abci/errors/InvalidArgumentAbciError.js b/packages/js-drive/lib/abci/errors/InvalidArgumentAbciError.js new file mode 100644 index 00000000000..dc66740da1f --- /dev/null +++ b/packages/js-drive/lib/abci/errors/InvalidArgumentAbciError.js @@ -0,0 +1,16 @@ +const grpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class InvalidArgumentAbciError extends AbstractAbciError { + /** + * + * @param {string} message + * @param {Object} [data] + */ + constructor(message, data = {}) { + super(grpcErrorCodes.INVALID_ARGUMENT, message, data); + } +} + +module.exports = InvalidArgumentAbciError; diff --git a/packages/js-drive/lib/abci/errors/NotFoundAbciError.js b/packages/js-drive/lib/abci/errors/NotFoundAbciError.js new file mode 100644 index 00000000000..1daeb9325c1 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/NotFoundAbciError.js @@ -0,0 +1,16 @@ +const grpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class NotFoundAbciError extends AbstractAbciError { + /** + * + * @param {string} message + * @param {Object} [data] + */ + constructor(message, data = {}) { + super(grpcErrorCodes.NOT_FOUND, message, data); + } +} + +module.exports = NotFoundAbciError; diff --git a/packages/js-drive/lib/abci/errors/UnavailableAbciError.js b/packages/js-drive/lib/abci/errors/UnavailableAbciError.js new file mode 100644 index 00000000000..227aff45fe4 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/UnavailableAbciError.js @@ -0,0 +1,16 @@ +const grpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class UnavailableAbciError extends AbstractAbciError { + /** + * + * @param {string} message + * @param {Object} [data] + */ + constructor(message, data = {}) { + super(grpcErrorCodes.UNAVAILABLE, message, data); + } +} + +module.exports = UnavailableAbciError; diff --git a/packages/js-drive/lib/abci/errors/UnimplementedAbciError.js b/packages/js-drive/lib/abci/errors/UnimplementedAbciError.js new file mode 100644 index 00000000000..81e279f2039 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/UnimplementedAbciError.js @@ -0,0 +1,16 @@ +const grpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const AbstractAbciError = require('./AbstractAbciError'); + +class UnimplementedAbciError extends AbstractAbciError { + /** + * + * @param {string} message + * @param {Object} [data] + */ + constructor(message, data = {}) { + super(grpcErrorCodes.UNIMPLEMENTED, message, data); + } +} + +module.exports = UnimplementedAbciError; diff --git a/packages/js-drive/lib/abci/errors/VerboseInternalAbciError.js b/packages/js-drive/lib/abci/errors/VerboseInternalAbciError.js new file mode 100644 index 00000000000..fe13d71c83c --- /dev/null +++ b/packages/js-drive/lib/abci/errors/VerboseInternalAbciError.js @@ -0,0 +1,30 @@ +const InternalAbciError = require('./InternalAbciError'); + +class VerboseInternalAbciError extends InternalAbciError { + /** + * + * @param {InternalAbciError} error + */ + constructor(error) { + const originalError = error.getError(); + let [, errorPath] = originalError.stack.toString().split(/\r\n|\n/); + + if (!errorPath) { + errorPath = originalError.stack; + } + + const message = `${originalError.message} ${errorPath.trim()}`; + + const data = error.getData() || {}; + data.stack = originalError.stack; + + super( + originalError, + data, + ); + + this.message = message; + } +} + +module.exports = VerboseInternalAbciError; diff --git a/packages/js-drive/lib/abci/errors/createContextLoggerFactory.js b/packages/js-drive/lib/abci/errors/createContextLoggerFactory.js new file mode 100644 index 00000000000..4e9b52354f6 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/createContextLoggerFactory.js @@ -0,0 +1,24 @@ +/** + * + * @param {AsyncLocalStorage} abciAsyncLocalStorage + * @return {createContextLogger} + */ +function createContextLoggerFactory(abciAsyncLocalStorage) { + /** + * @typedef {createContextLogger} + * @param {Logger} logger + * @param {Object} context + * @return {Logger} + */ + function createContextLogger(logger, context) { + const contextLogger = logger.child(context); + + abciAsyncLocalStorage.getStore().set('logger', contextLogger); + + return contextLogger; + } + + return createContextLogger; +} + +module.exports = createContextLoggerFactory; diff --git a/packages/js-drive/lib/abci/errors/enrichErrorWithContextLoggerFactory.js b/packages/js-drive/lib/abci/errors/enrichErrorWithContextLoggerFactory.js new file mode 100644 index 00000000000..2493c6d7c92 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/enrichErrorWithContextLoggerFactory.js @@ -0,0 +1,39 @@ +/** + * Add consensus logger to an error (factory) + * + * @param {AsyncLocalStorage} abciAsyncLocalStorage + * @return {enrichErrorWithContextLogger} + */ +function enrichErrorWithContextLoggerFactory(abciAsyncLocalStorage) { + /** + * Add consensus logger to an error + * + * @typedef enrichErrorWithContextLogger + * + * @param {Function} method + * + * @return {Function} + */ + function enrichErrorWithContextLogger(method) { + /** + * @param {*[]} args + */ + async function methodHandler(...args) { + return abciAsyncLocalStorage.run( + new Map(), + () => method(...args).catch((error) => { + // eslint-disable-next-line no-param-reassign + error.contextLogger = abciAsyncLocalStorage.getStore().get('logger'); + + return Promise.reject(error); + }), + ); + } + + return methodHandler; + } + + return enrichErrorWithContextLogger; +} + +module.exports = enrichErrorWithContextLoggerFactory; diff --git a/packages/js-drive/lib/abci/errors/wrapInErrorHandlerFactory.js b/packages/js-drive/lib/abci/errors/wrapInErrorHandlerFactory.js new file mode 100644 index 00000000000..8445605f767 --- /dev/null +++ b/packages/js-drive/lib/abci/errors/wrapInErrorHandlerFactory.js @@ -0,0 +1,63 @@ +const AbstractAbciError = require('./AbstractAbciError'); +const InternalAbciError = require('./InternalAbciError'); +const VerboseInternalAbciError = require('./VerboseInternalAbciError'); + +/** + * @param {BaseLogger} logger + * @param {boolean} isProductionEnvironment + * + * @return wrapInErrorHandler + */ +function wrapInErrorHandlerFactory(logger, isProductionEnvironment) { + /** + * Wrap ABCI methods in error handler + * + * @typedef wrapInErrorHandler + * + * @param {Function} method + * + * @return {Function} + */ + function wrapInErrorHandler(method) { + /** + * @param {*[]} args + */ + async function methodErrorHandler(...args) { + try { + return await method(...args); + } catch (e) { + let abciError = e; + + // Wrap all non ABCI errors to an internal ABCI error + if (!(e instanceof AbstractAbciError)) { + abciError = new InternalAbciError(e); + } + + // Log only internal ABCI errors + if (abciError instanceof InternalAbciError) { + const originalError = abciError.getError(); + + const preferredLogger = originalError.contextLogger || logger; + delete originalError.contextLogger; + + preferredLogger.error( + { err: originalError }, + originalError.message, + ); + + if (!isProductionEnvironment) { + abciError = new VerboseInternalAbciError(abciError); + } + } + + return abciError.getAbciResponse(); + } + } + + return methodErrorHandler; + } + + return wrapInErrorHandler; +} + +module.exports = wrapInErrorHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/checkTxHandlerFactory.js b/packages/js-drive/lib/abci/handlers/checkTxHandlerFactory.js new file mode 100644 index 00000000000..05057f6a239 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/checkTxHandlerFactory.js @@ -0,0 +1,44 @@ +const { + tendermint: { + abci: { + ResponseCheckTx, + }, + }, +} = require('@dashevo/abci/types'); + +/** + * @param {unserializeStateTransition} unserializeStateTransition + * @param {AsyncLocalStorage} unserializeStateTransition + * @param {createContextLogger} createContextLogger + * @param {Logger} logger + * + * @returns {checkTxHandler} + */ +function checkTxHandlerFactory( + unserializeStateTransition, + createContextLogger, + logger, +) { + /** + * CheckTx ABCI Handler + * + * @typedef checkTxHandler + * + * @param {abci.RequestCheckTx} request + * + * @returns {Promise} + */ + async function checkTxHandler({ tx: stateTransitionByteArray }) { + createContextLogger(logger, { + abciMethod: 'checkTx', + }); + + await unserializeStateTransition(stateTransitionByteArray); + + return new ResponseCheckTx(); + } + + return checkTxHandler; +} + +module.exports = checkTxHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/errors/NetworkProtocolVersionIsNotSetError.js b/packages/js-drive/lib/abci/handlers/errors/NetworkProtocolVersionIsNotSetError.js new file mode 100644 index 00000000000..39a8ca62828 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/errors/NetworkProtocolVersionIsNotSetError.js @@ -0,0 +1,9 @@ +const DriveError = require('../../../errors/DriveError'); + +class NetworkProtocolVersionIsNotSetError extends DriveError { + constructor() { + super('Network protocol version is not set'); + } +} + +module.exports = NetworkProtocolVersionIsNotSetError; diff --git a/packages/js-drive/lib/abci/handlers/errors/NotSupportedNetworkProtocolVersionError.js b/packages/js-drive/lib/abci/handlers/errors/NotSupportedNetworkProtocolVersionError.js new file mode 100644 index 00000000000..5422637b6b2 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/errors/NotSupportedNetworkProtocolVersionError.js @@ -0,0 +1,30 @@ +const DriveError = require('../../../errors/DriveError'); + +class NotSupportedNetworkProtocolVersionError extends DriveError { + /** + * @param {Long} networkProtocolVersion + * @param {Long} latestProtocolVersion + */ + constructor(networkProtocolVersion, latestProtocolVersion) { + super(`Block protocol version ${networkProtocolVersion} not supported. Expected to be less or equal to ${latestProtocolVersion}.`); + + this.networkProtocolVersion = networkProtocolVersion; + this.latestProtocolVersion = latestProtocolVersion; + } + + /** + * @returns {Long} + */ + getNetworkProtocolVersion() { + return this.networkProtocolVersion; + } + + /** + * @returns {Long} + */ + getLatestProtocolVersion() { + return this.latestProtocolVersion; + } +} + +module.exports = NotSupportedNetworkProtocolVersionError; diff --git a/packages/js-drive/lib/abci/handlers/extendVoteHandlerFactory.js b/packages/js-drive/lib/abci/handlers/extendVoteHandlerFactory.js new file mode 100644 index 00000000000..9d0e1ef786c --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/extendVoteHandlerFactory.js @@ -0,0 +1,67 @@ +const { + tendermint: { + abci: { + ResponseExtendVote, + }, + types: { + VoteExtensionType, + }, + }, +} = require('@dashevo/abci/types'); + +/** + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {createContextLogger} createContextLogger + * + * @return {extendVoteHandler} + */ +function extendVoteHandlerFactory(proposalBlockExecutionContext, createContextLogger) { + /** + * @typedef extendVoteHandler + * @return {Promise} + */ + async function extendVoteHandler() { + const contextLogger = createContextLogger(proposalBlockExecutionContext.getContextLogger(), { + abciMethod: 'extendVote', + }); + + contextLogger.debug('ExtendVote ABCI method requested'); + + const unsignedWithdrawalTransactionsMap = proposalBlockExecutionContext + .getWithdrawalTransactionsMap(); + + const voteExtensions = Object.keys(unsignedWithdrawalTransactionsMap) + .sort() + .map((txHashHex) => ({ + type: VoteExtensionType.THRESHOLD_RECOVER, + extension: Buffer.from(txHashHex, 'hex'), + })); + + const voteExtensionTypeName = { + [VoteExtensionType.DEFAULT]: 'default', + [VoteExtensionType.THRESHOLD_RECOVER]: 'threshold recovery', + }; + + voteExtensions.forEach(({ extension, type }) => { + const extensionString = extension.toString('hex'); + + const extensionTruncatedString = extensionString.substring( + 0, + Math.min(30, extensionString.length), + ); + + contextLogger.debug({ + type, + extension: extensionString, + }, `Vote extended to obtain ${voteExtensionTypeName} signature for ${extensionTruncatedString}... payload`); + }); + + return new ResponseExtendVote({ + voteExtensions, + }); + } + + return extendVoteHandler; +} + +module.exports = extendVoteHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/finalizeBlockHandlerFactory.js b/packages/js-drive/lib/abci/handlers/finalizeBlockHandlerFactory.js new file mode 100644 index 00000000000..1d3dfdaf4fb --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/finalizeBlockHandlerFactory.js @@ -0,0 +1,145 @@ +const { + tendermint: { + abci: { + ResponseFinalizeBlock, + RequestProcessProposal, + }, + }, +} = require('@dashevo/abci/types'); + +const lodashCloneDeep = require('lodash/cloneDeep'); + +/** + * + * @return {finalizeBlockHandler} + * @param {GroveDBStore} groveDBStore + * @param {BlockExecutionContextRepository} blockExecutionContextRepository + * @param {BaseLogger} logger + * @param {ExecutionTimer} executionTimer + * @param {BlockExecutionContext} latestBlockExecutionContext + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {processProposal} processProposal + * @param {broadcastWithdrawalTransactions} broadcastWithdrawalTransactions + * @param {createContextLogger} createContextLogger + * + */ +function finalizeBlockHandlerFactory( + groveDBStore, + blockExecutionContextRepository, + logger, + executionTimer, + latestBlockExecutionContext, + proposalBlockExecutionContext, + processProposal, + broadcastWithdrawalTransactions, + createContextLogger, +) { + /** + * @typedef finalizeBlockHandler + * + * @param {abci.RequestFinalizeBlock} request + * @return {Promise} + */ + async function finalizeBlockHandler(request) { + const { + commit: commitInfo, + height, + round, + } = request; + + const contextLogger = createContextLogger(logger, { + height: height.toString(), + round, + abciMethod: 'finalizeBlock', + }); + + const requestToLog = lodashCloneDeep(request); + delete requestToLog.block.data; + + contextLogger.debug('FinalizeBlock ABCI method requested'); + contextLogger.trace({ abciRequest: requestToLog }); + + const lastProcessedRound = proposalBlockExecutionContext.getRound(); + + if (lastProcessedRound !== round) { + contextLogger.warn({ + lastProcessedRound, + round, + }, `Finalizing previously executed round ${round} instead of the last known ${lastProcessedRound}`); + + const { + block: { + header: { + time, + version, + proposerProTxHash, + proposedAppVersion, + coreChainLockedHeight, + }, + data: { + txs, + }, + }, + } = request; + + const processProposalRequest = new RequestProcessProposal({ + height, + txs, + coreChainLockedHeight, + version, + proposedLastCommit: commitInfo, + time, + proposerProTxHash, + proposedAppVersion, + round, + }); + + await processProposal(processProposalRequest, contextLogger); + } + + // Send withdrawal transactions to Core + const unsignedWithdrawalTransactionsMap = proposalBlockExecutionContext + .getWithdrawalTransactionsMap(); + + const { thresholdVoteExtensions } = commitInfo; + + await broadcastWithdrawalTransactions( + proposalBlockExecutionContext, + thresholdVoteExtensions, + unsignedWithdrawalTransactionsMap, + ); + + proposalBlockExecutionContext.setLastCommitInfo(commitInfo); + + // Store proposal block execution context + await blockExecutionContextRepository.store( + proposalBlockExecutionContext, + { + useTransaction: true, + }, + ); + + // Commit the current block db transactions into storage + await groveDBStore.commitTransaction(); + + // Update last block execution context with proposal data + latestBlockExecutionContext.populate(proposalBlockExecutionContext); + + proposalBlockExecutionContext.reset(); + + const blockExecutionTimings = executionTimer.stopTimer('blockExecution'); + + contextLogger.info( + { + timings: blockExecutionTimings, + }, + `Block #${height} finalized in ${round + 1} round(s) and ${blockExecutionTimings} seconds`, + ); + + return new ResponseFinalizeBlock(); + } + + return finalizeBlockHandler; +} + +module.exports = finalizeBlockHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/infoHandlerFactory.js b/packages/js-drive/lib/abci/handlers/infoHandlerFactory.js new file mode 100644 index 00000000000..0d056c005d0 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/infoHandlerFactory.js @@ -0,0 +1,94 @@ +const { + tendermint: { + abci: { + ResponseInfo, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const { version: driveVersion } = require('../../../package.json'); + +/** + * @param {BlockExecutionContext} latestBlockExecutionContext + * @param {BlockExecutionContextRepository} blockExecutionContextRepository + * @param {Long} latestProtocolVersion + * @param {updateSimplifiedMasternodeList} updateSimplifiedMasternodeList + * @param {BaseLogger} logger + * @param {GroveDBStore} groveDBStore + * @param {createContextLogger} createContextLogger + * @return {infoHandler} + */ +function infoHandlerFactory( + latestBlockExecutionContext, + blockExecutionContextRepository, + latestProtocolVersion, + updateSimplifiedMasternodeList, + logger, + groveDBStore, + createContextLogger, +) { + /** + * Info ABCI handler + * + * @typedef infoHandler + * + * @param {abci.RequestInfo} request + * @return {Promise} + */ + async function infoHandler(request) { + let contextLogger = createContextLogger(logger, { + abciMethod: 'info', + }); + + contextLogger.debug('Info ABCI method requested'); + contextLogger.trace({ abciRequest: request }); + + // Initialize current heights + + let lastHeight = Long.fromNumber(0); + let lastCoreChainLockedHeight = 0; + + // Initialize latest Block Execution Context + const persistedBlockExecutionContext = await blockExecutionContextRepository.fetch(); + if (!persistedBlockExecutionContext.isEmpty()) { + latestBlockExecutionContext.populate(persistedBlockExecutionContext); + + lastHeight = latestBlockExecutionContext.getHeight(); + lastCoreChainLockedHeight = latestBlockExecutionContext.getCoreChainLockedHeight(); + + contextLogger = createContextLogger(contextLogger, { + height: lastHeight.toString(), + }); + + // Update SML store to latest saved core chain lock to make sure + // that verify chain lock handler has updated SML Store to verify signatures + await updateSimplifiedMasternodeList(lastCoreChainLockedHeight, { + logger: contextLogger, + }); + } + + const appHash = await groveDBStore.getRootHash(); + + contextLogger.info( + { + lastHeight: lastHeight.toString(), + appHash: appHash.toString('hex').toUpperCase(), + latestProtocolVersion: latestProtocolVersion.toString(), + }, + `Start processing from block #${lastHeight} with appHash ${appHash.toString('hex').toUpperCase()}`, + ); + + return new ResponseInfo({ + version: driveVersion, + appVersion: latestProtocolVersion, + lastBlockHeight: lastHeight, + lastBlockAppHash: appHash, + }); + } + + return infoHandler; +} + +module.exports = infoHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/initChainHandlerFactory.js b/packages/js-drive/lib/abci/handlers/initChainHandlerFactory.js new file mode 100644 index 00000000000..51f1b73aa0c --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/initChainHandlerFactory.js @@ -0,0 +1,150 @@ +const { + tendermint: { + abci: { + ResponseInitChain, + }, + }, +} = require('@dashevo/abci/types'); + +const BlockInfo = require('../../blockExecution/BlockInfo'); +const protoTimestampToMillis = require('../../util/protoTimestampToMillis'); + +/** + * Init Chain ABCI handler + * + * @param {updateSimplifiedMasternodeList} updateSimplifiedMasternodeList + * @param {number} initialCoreChainLockedHeight + * @param {ValidatorSet} validatorSet + * @param {createValidatorSetUpdate} createValidatorSetUpdate + * @param {synchronizeMasternodeIdentities} synchronizeMasternodeIdentities + * @param {BaseLogger} logger + * @param {GroveDBStore} groveDBStore + * @param {RSAbci} rsAbci + * @param {createCoreChainLockUpdate} createCoreChainLockUpdate + * @param {createContextLogger} createContextLogger + * @param {SystemIdentityPublicKeys} systemIdentityPublicKeys + * @return {initChainHandler} + */ +function initChainHandlerFactory( + updateSimplifiedMasternodeList, + initialCoreChainLockedHeight, + validatorSet, + createValidatorSetUpdate, + synchronizeMasternodeIdentities, + logger, + groveDBStore, + rsAbci, + createCoreChainLockUpdate, + createContextLogger, + systemIdentityPublicKeys, +) { + /** + * @typedef initChainHandler + * + * @param {abci.RequestInitChain} request + * @return {Promise} + */ + async function initChainHandler(request) { + const { time } = request; + const timeMs = protoTimestampToMillis(time); + + const contextLogger = createContextLogger(logger, { + height: request.initialHeight.toString(), + abciMethod: 'initChain', + }); + + contextLogger.debug('InitChain ABCI method requested'); + contextLogger.trace({ abciRequest: request }); + + await updateSimplifiedMasternodeList( + initialCoreChainLockedHeight, { + logger: contextLogger, + }, + ); + + // Create initial state + + await groveDBStore.startTransaction(); + + // Call RS ABCI + + const initChainRequest = { + genesisTimeMs: timeMs, + systemIdentityPublicKeys, + }; + + logger.debug('Request RS Drive\'s InitChain method'); + logger.trace(initChainRequest); + + await rsAbci.initChain(initChainRequest, true); + + const blockInfo = new BlockInfo( + 0, + 0, + timeMs, + ); + + const synchronizeMasternodeIdentitiesResult = await synchronizeMasternodeIdentities( + initialCoreChainLockedHeight, + blockInfo, + ); + + const { + createdEntities, updatedEntities, removedEntities, fromHeight, toHeight, + } = synchronizeMasternodeIdentitiesResult; + + contextLogger.info( + `Masternode identities are synced for heights from ${fromHeight} to ${toHeight}: ${createdEntities.length} created, ${updatedEntities.length} updated, ${removedEntities.length} removed`, + ); + + contextLogger.trace( + { + createdEntities: createdEntities.map((item) => item.toJSON()), + updatedEntities: updatedEntities.map((item) => item.toJSON()), + removedEntities: removedEntities.map((item) => item.toJSON()), + }, + 'Synchronized masternode identities', + ); + + // Set initial validator set + + await validatorSet.initialize(initialCoreChainLockedHeight); + + const { quorumHash } = validatorSet.getQuorum(); + + const validatorSetUpdate = createValidatorSetUpdate(validatorSet); + + const coreChainLockUpdate = await createCoreChainLockUpdate( + initialCoreChainLockedHeight, + 0, + contextLogger, + ); + + const appHash = await groveDBStore.getRootHash({ useTransaction: true }); + + await groveDBStore.commitTransaction(); + + contextLogger.trace(validatorSetUpdate, `Validator set initialized with ${quorumHash} quorum`); + + contextLogger.info( + { + chainId: request.chainId, + appHash: appHash.toString('hex').toUpperCase(), + initialHeight: request.initialHeight.toString(), + initialCoreHeight: initialCoreChainLockedHeight, + }, + `Init ${request.chainId} chain on block #${request.initialHeight.toString()} with app hash ${appHash.toString('hex').toUpperCase()}`, + ); + + return new ResponseInitChain({ + appHash, + validatorSetUpdate, + initialCoreHeight: initialCoreChainLockedHeight, + nextCoreChainLockUpdate: coreChainLockUpdate, + }); + } + + return initChainHandler; +} + +module.exports = initChainHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/prepareProposalHandlerFactory.js b/packages/js-drive/lib/abci/handlers/prepareProposalHandlerFactory.js new file mode 100644 index 00000000000..9f36462f661 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/prepareProposalHandlerFactory.js @@ -0,0 +1,189 @@ +const { + tendermint: { + abci: { + ResponsePrepareProposal, + }, + }, +} = require('@dashevo/abci/types'); + +const lodashCloneDeep = require('lodash/cloneDeep'); +const addStateTransitionFeesToBlockFees = require('./proposal/fees/addStateTransitionFeesToBlockFees'); + +const txAction = { + UNKNOWN: 0, // Unknown action + UNMODIFIED: 1, // The Application did not modify this transaction. + ADDED: 2, // The Application added this transaction. + REMOVED: 3, // The Application wants this transaction removed from the proposal and the mempool. +}; + +/** + * @param {deliverTx} wrappedDeliverTx + * @param {BaseLogger} logger + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {beginBlock} beginBlock + * @param {endBlock} endBlock + * @param {createCoreChainLockUpdate} createCoreChainLockUpdate + * @param {ExecutionTimer} executionTimer + * @param {createContextLogger} createContextLogger + * @return {prepareProposalHandler} + */ +function prepareProposalHandlerFactory( + wrappedDeliverTx, + logger, + proposalBlockExecutionContext, + beginBlock, + endBlock, + createCoreChainLockUpdate, + executionTimer, + createContextLogger, +) { + /** + * @typedef prepareProposalHandler + * @param {abci.RequestPrepareProposal} request + * @return {Promise} + */ + async function prepareProposalHandler(request) { + const { + height, + maxTxBytes, + txs, + coreChainLockedHeight, + version, + localLastCommit: lastCommitInfo, + time, + proposerProTxHash, + proposedAppVersion, + round, + quorumHash, + } = request; + + const contextLogger = createContextLogger(logger, { + height: height.toString(), + round, + abciMethod: 'prepareProposal', + }); + + const requestToLog = lodashCloneDeep(request); + delete requestToLog.txs; + + contextLogger.debug('PrepareProposal ABCI method requested'); + contextLogger.trace({ abciRequest: requestToLog }); + + contextLogger.info(`Preparing a block proposal for height #${height} round #${round}`); + + await beginBlock( + { + lastCommitInfo, + height, + coreChainLockedHeight, + version, + time, + proposerProTxHash: Buffer.from(proposerProTxHash), + proposedAppVersion, + round, + quorumHash, + }, + contextLogger, + ); + + let totalSizeBytes = 0; + + const txRecords = []; + const txResults = []; + const blockFees = { + storageFee: 0, + processingFee: 0, + refundsPerEpoch: {}, + }; + + let validTxCount = 0; + let invalidTxCount = 0; + + for (const tx of txs) { + totalSizeBytes += tx.length; + + if (totalSizeBytes > maxTxBytes) { + break; + } + + txRecords.push({ + tx, + action: txAction.UNMODIFIED, + }); + + const { + code, + info, + fees, + } = await wrappedDeliverTx(tx, round, contextLogger); + + if (code === 0) { + validTxCount += 1; + // TODO We probably should calculate fees for invalid transitions as well + addStateTransitionFeesToBlockFees(blockFees, fees); + } else { + invalidTxCount += 1; + } + + const txResult = { code }; + + if (info) { + txResult.info = info; + } + + txResults.push(txResult); + } + + const coreChainLockUpdate = await createCoreChainLockUpdate( + coreChainLockedHeight, + round, + contextLogger, + ); + + const { + consensusParamUpdates, + validatorSetUpdate, + appHash, + } = await endBlock({ + height, + round, + fees: blockFees, + coreChainLockedHeight, + }, contextLogger); + + const roundExecutionTime = executionTimer.getTimer('roundExecution', true); + + const mempoolTxCount = txs.length - validTxCount - invalidTxCount; + + contextLogger.info( + { + roundExecutionTime, + validTxCount, + invalidTxCount, + mempoolTxCount, + }, + `Prepared block proposal for height #${height} with appHash ${appHash.toString('hex').toUpperCase()}` + + ` in ${roundExecutionTime} seconds (valid txs = ${validTxCount}, invalid txs = ${invalidTxCount}, mempool txs = ${mempoolTxCount})`, + ); + + proposalBlockExecutionContext.setPrepareProposalResult({ + appHash, + txResults, + consensusParamUpdates, + validatorSetUpdate, + }); + + return new ResponsePrepareProposal({ + appHash, + txResults, + consensusParamUpdates, + validatorSetUpdate, + coreChainLockUpdate, + txRecords, + }); + } + + return prepareProposalHandler; +} + +module.exports = prepareProposalHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/processProposalHandlerFactory.js b/packages/js-drive/lib/abci/handlers/processProposalHandlerFactory.js new file mode 100644 index 00000000000..ee942a773be --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/processProposalHandlerFactory.js @@ -0,0 +1,99 @@ +const { + tendermint: { + abci: { + ResponseProcessProposal, + }, + }, +} = require('@dashevo/abci/types'); + +const lodashCloneDeep = require('lodash/cloneDeep'); +const statuses = require('./proposal/statuses'); + +/** + * @param {BaseLogger} logger + * @param {verifyChainLock} verifyChainLock + * @param {processProposal} processProposal + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {createContextLogger} createContextLogger + * @return {processProposalHandler} + */ +function processProposalHandlerFactory( + logger, + verifyChainLock, + processProposal, + proposalBlockExecutionContext, + createContextLogger, +) { + /** + * @typedef processProposalHandler + * @param {abci.RequestProcessProposal} request + * @return {Promise} + */ + async function processProposalHandler(request) { + const { + height, + coreChainLockUpdate, + round, + } = request; + + const contextLogger = createContextLogger(logger, { + height: height.toString(), + round, + abciMethod: 'processProposal', + }); + + const requestToLog = lodashCloneDeep(request); + delete requestToLog.txs; + + contextLogger.debug('ProcessProposal ABCI method requested'); + contextLogger.trace({ abciRequest: requestToLog }); + + // Skip process proposal if it was already prepared for this height and round + const prepareProposalResult = proposalBlockExecutionContext.getPrepareProposalResult(); + + if (prepareProposalResult + && proposalBlockExecutionContext.getHeight().toNumber() === height.toNumber() + && proposalBlockExecutionContext.getRound() === round) { + contextLogger.debug('Skip processing proposal and return prepared result'); + + const { + appHash, + txResults, + consensusParamUpdates, + validatorSetUpdate, + } = prepareProposalResult; + + return new ResponseProcessProposal({ + status: statuses.ACCEPT, + appHash, + txResults, + consensusParamUpdates, + validatorSetUpdate, + }); + } + + if (coreChainLockUpdate) { + const chainLockIsValid = await verifyChainLock(coreChainLockUpdate); + + if (!chainLockIsValid) { + contextLogger.warn({ + coreChainLockUpdate, + }, `Block proposal #${height} round #${round} rejected due to invalid core chain locked height update`); + + return new ResponseProcessProposal({ + status: statuses.REJECT, + }); + } + + logger.debug({ + coreChainLockUpdate, + }, `ChainLock is valid for height ${coreChainLockUpdate.coreBlockHeight}`); + } + + return processProposal(request, contextLogger); + } + + return processProposalHandler; +} + +module.exports = processProposalHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/beginBlockFactory.js b/packages/js-drive/lib/abci/handlers/proposal/beginBlockFactory.js new file mode 100644 index 00000000000..a159db141e4 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/beginBlockFactory.js @@ -0,0 +1,224 @@ +const { hash } = require('@dashevo/dpp/lib/util/hash'); + +const NotSupportedNetworkProtocolVersionError = require('../errors/NotSupportedNetworkProtocolVersionError'); +const NetworkProtocolVersionIsNotSetError = require('../errors/NetworkProtocolVersionIsNotSetError'); + +const BlockInfo = require('../../../blockExecution/BlockInfo'); +const protoTimestampToMillis = require('../../../util/protoTimestampToMillis'); + +/** + * Begin Block + * + * @param {GroveDBStore} groveDBStore + * @param {BlockExecutionContext} latestBlockExecutionContext + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {Long} latestProtocolVersion + * @param {DashPlatformProtocol} dpp + * @param {DashPlatformProtocol} transactionalDpp + * @param {updateSimplifiedMasternodeList} updateSimplifiedMasternodeList + * @param {waitForChainLockedHeight} waitForChainLockedHeight + * @param {synchronizeMasternodeIdentities} synchronizeMasternodeIdentities + * @param {RSAbci} rsAbci + * @param {ExecutionTimer} executionTimer + * @param {LastSyncedCoreHeightRepository} lastSyncedCoreHeightRepository + * @param {SimplifiedMasternodeList} simplifiedMasternodeList + * + * @return {beginBlock} + */ +function beginBlockFactory( + groveDBStore, + latestBlockExecutionContext, + proposalBlockExecutionContext, + latestProtocolVersion, + dpp, + transactionalDpp, + updateSimplifiedMasternodeList, + waitForChainLockedHeight, + synchronizeMasternodeIdentities, + rsAbci, + executionTimer, + lastSyncedCoreHeightRepository, + simplifiedMasternodeList, +) { + /** + * @typedef beginBlock + * @param {Object} request + * @param {ILastCommitInfo} request.lastCommitInfo + * @param {Long} request.height + * @param {number} request.coreChainLockedHeight + * @param {IConsensus} request.version + * @param {Long} request.proposedAppVersion + * @param {ITimestamp} request.time + * @param {Buffer} request.proposerProTxHash + * @param {Buffer} request.quorumHash + * @param {BaseLogger} contextLogger + * + * @return {Promise} + */ + async function beginBlock(request, contextLogger) { + const { + lastCommitInfo, + height, + coreChainLockedHeight, + version, + time, + proposerProTxHash, + proposedAppVersion, + round, + quorumHash, + } = request; + + if (proposalBlockExecutionContext.isEmpty()) { + executionTimer.clearTimer('blockExecution'); + executionTimer.startTimer('blockExecution'); + } + + executionTimer.clearTimer('roundExecution'); + executionTimer.startTimer('roundExecution'); + + // Validate protocol version + + if (version.app.eq(0)) { + throw new NetworkProtocolVersionIsNotSetError(); + } + + if (version.app.gt(latestProtocolVersion)) { + throw new NotSupportedNetworkProtocolVersionError( + version.app, + latestProtocolVersion, + ); + } + + // Make sure Core has the same height as the network + + await waitForChainLockedHeight(coreChainLockedHeight); + + // Reset block execution context + proposalBlockExecutionContext.reset(); + + proposalBlockExecutionContext.setContextLogger(contextLogger); + proposalBlockExecutionContext.setHeight(height); + proposalBlockExecutionContext.setVersion(version); + proposalBlockExecutionContext.setProposedAppVersion(proposedAppVersion); + proposalBlockExecutionContext.setRound(round); + proposalBlockExecutionContext.setTimeMs(protoTimestampToMillis(time)); + proposalBlockExecutionContext.setCoreChainLockedHeight(coreChainLockedHeight); + proposalBlockExecutionContext.setLastCommitInfo(lastCommitInfo); + + // Update SML + const isSimplifiedMasternodeListUpdated = await updateSimplifiedMasternodeList( + coreChainLockedHeight, + { + logger: contextLogger, + }, + ); + + // Set protocol version to DPP + dpp.setProtocolVersion(version.app.toNumber()); + transactionalDpp.setProtocolVersion(version.app.toNumber()); + + // Restart transaction if already started + if (await groveDBStore.isTransactionStarted()) { + await groveDBStore.abortTransaction(); + } + + await groveDBStore.startTransaction(); + + const lastSyncedHeightResult = await lastSyncedCoreHeightRepository.fetch({ + useTransaction: true, + }); + + const lastSyncedCoreHeight = lastSyncedHeightResult.getValue() || 0; + + // Call RS ABCI + + /** + * @type {BlockBeginRequest} + */ + const rsRequest = { + blockHeight: height.toNumber(), + blockTimeMs: proposalBlockExecutionContext.getTimeMs(), + proposerProTxHash, + validatorSetQuorumHash: quorumHash, + coreChainLockedHeight, + lastSyncedCoreHeight, + // TODO: Since we don't have HPMNs now and every masternode can be a validator, + // we pass the whole list + totalHpmns: simplifiedMasternodeList.getStore() + .getCurrentSML() + .getValidMasternodesList() + .length, + proposedAppVersion: proposedAppVersion.toNumber(), + }; + + if (!latestBlockExecutionContext.isEmpty()) { + rsRequest.previousBlockTimeMs = latestBlockExecutionContext.getTimeMs(); + } + + contextLogger.debug(rsRequest, 'Request RS Drive\'s BlockBegin method'); + + const rsResponse = await rsAbci.blockBegin(rsRequest, true); + + const withdrawalTransactionsMap = (rsResponse.unsignedWithdrawalTransactions || []).reduce( + (map, transactionBytes) => ({ + ...map, + [hash(transactionBytes).toString('hex')]: transactionBytes, + }), + {}, + ); + + proposalBlockExecutionContext.setWithdrawalTransactionsMap(withdrawalTransactionsMap); + proposalBlockExecutionContext.setEpochInfo(rsResponse.epochInfo); + + const { currentEpochIndex, isEpochChange } = rsResponse; + + if (isEpochChange) { + const debugData = { + currentEpochIndex, + blockTime: proposalBlockExecutionContext.getTimeMs(), + }; + + if (rsRequest.previousBlockTimeMs) { + debugData.previousBlockTimeMs = rsRequest.previousBlockTimeMs; + } + + const blockTimeFormatted = new Date(proposalBlockExecutionContext.getTimeMs()).toUTCString(); + + contextLogger.info(debugData, `Epoch #${currentEpochIndex} started on block #${height} at ${blockTimeFormatted}`); + } + + // Synchronize masternode identities + + if (isSimplifiedMasternodeListUpdated) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(proposalBlockExecutionContext); + + const synchronizeMasternodeIdentitiesResult = await synchronizeMasternodeIdentities( + coreChainLockedHeight, + blockInfo, + ); + + const { + createdEntities, updatedEntities, removedEntities, fromHeight, toHeight, + } = synchronizeMasternodeIdentitiesResult; + + contextLogger.info( + `Masternode identities are synced for heights from ${fromHeight} to ${toHeight}: ${createdEntities.length} created, ${updatedEntities.length} updated, ${removedEntities.length} removed`, + ); + + if (createdEntities.length > 0 || updatedEntities.length > 0 || removedEntities.length > 0) { + contextLogger.trace( + { + createdEntities: createdEntities.map((item) => item.toJSON()), + updatedEntities: updatedEntities.map((item) => item.toJSON()), + removedEntities: removedEntities.map((item) => item.toJSON()), + }, + 'Synchronized masternode identities', + ); + } + } + } + + return beginBlock; +} + +module.exports = beginBlockFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.js b/packages/js-drive/lib/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.js new file mode 100644 index 00000000000..3bc4818b684 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.js @@ -0,0 +1,64 @@ +const BlockInfo = require('../../../blockExecution/BlockInfo'); + +/** + * @param {CoreRpcClient} coreRpcClient + * @param {updateWithdrawalTransactionIdAndStatus} updateWithdrawalTransactionIdAndStatus + * + * @return {broadcastWithdrawalTransactions} + */ +function broadcastWithdrawalTransactionsFactory( + coreRpcClient, + updateWithdrawalTransactionIdAndStatus, +) { + /** + * @typedef broadcastWithdrawalTransactions + * + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {{{ extension: Buffer, signature: Buffer }}[]} thresholdVoteExtensions + * @param {Object} unsignedWithdrawalTransactionsMap + * + * @return {Promise} + */ + async function broadcastWithdrawalTransactions( + proposalBlockExecutionContext, + thresholdVoteExtensions, + unsignedWithdrawalTransactionsMap, + ) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(proposalBlockExecutionContext); + + const transactionIdMap = {}; + + for (const { extension, signature } of (thresholdVoteExtensions || [])) { + const withdrawalTransactionHash = extension.toString('hex'); + + const unsignedWithdrawalTransactionBytes = unsignedWithdrawalTransactionsMap[ + withdrawalTransactionHash + ]; + + if (unsignedWithdrawalTransactionBytes) { + const transactionBytes = Buffer.concat([ + unsignedWithdrawalTransactionBytes, + signature, + ]); + + transactionIdMap[unsignedWithdrawalTransactionBytes.toString('hex')] = transactionBytes; + + // TODO: think about Core error handling + await coreRpcClient.sendRawTransaction(transactionBytes.toString('hex')); + } + } + + await updateWithdrawalTransactionIdAndStatus( + blockInfo, + proposalBlockExecutionContext.getCoreChainLockedHeight(), + transactionIdMap, + { + useTransaction: true, + }, + ); + } + + return broadcastWithdrawalTransactions; +} + +module.exports = broadcastWithdrawalTransactionsFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/createConsensusParamUpdateFactory.js b/packages/js-drive/lib/abci/handlers/proposal/createConsensusParamUpdateFactory.js new file mode 100644 index 00000000000..4ac31ad0377 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/createConsensusParamUpdateFactory.js @@ -0,0 +1,69 @@ +const { + tendermint: { + types: { + ConsensusParams, + }, + }, +} = require('@dashevo/abci/types'); + +const featureFlagTypes = require('@dashevo/feature-flags-contract/lib/featureFlagTypes'); + +/** + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {getFeatureFlagForHeight} getFeatureFlagForHeight + * @return {createConsensusParamUpdate} + */ +function createConsensusParamUpdateFactory( + proposalBlockExecutionContext, + getFeatureFlagForHeight, +) { + /** + * @typedef createConsensusParamUpdate + * @param {number} height + * @param {number} round + * @param {BaseLogger} contextLogger + * @return {Promise} + */ + async function createConsensusParamUpdate(height, round, contextLogger) { + const contextVersion = proposalBlockExecutionContext.getVersion(); + + // Update consensus params feature flag + + const updateConsensusParamsFeatureFlag = await getFeatureFlagForHeight( + featureFlagTypes.UPDATE_CONSENSUS_PARAMS, + height, + true, + ); + + let consensusParamUpdates; + if (updateConsensusParamsFeatureFlag) { + // Use previous version if we aren't going to update it + let version = { + appVersion: contextVersion.app, + }; + + if (updateConsensusParamsFeatureFlag.get('version')) { + version = updateConsensusParamsFeatureFlag.get('version'); + } + + consensusParamUpdates = new ConsensusParams({ + block: updateConsensusParamsFeatureFlag.get('block'), + evidence: updateConsensusParamsFeatureFlag.get('evidence'), + version, + }); + + contextLogger.info( + { + consensusParamUpdates, + }, + 'Update consensus params', + ); + } + + return consensusParamUpdates; + } + + return createConsensusParamUpdate; +} + +module.exports = createConsensusParamUpdateFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/createCoreChainLockUpdateFactory.js b/packages/js-drive/lib/abci/handlers/proposal/createCoreChainLockUpdateFactory.js new file mode 100644 index 00000000000..8a712216696 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/createCoreChainLockUpdateFactory.js @@ -0,0 +1,49 @@ +const { + tendermint: { + types: { + CoreChainLock, + }, + }, +} = require('@dashevo/abci/types'); +/** + * + * @param {LatestCoreChainLock} latestCoreChainLock + * @return {createCoreChainLockUpdate} + */ +function createCoreChainLockUpdateFactory( + latestCoreChainLock, +) { + /** + * @typedef createCoreChainLockUpdate + * @param {number} contextCoreChainLockedHeight + * @param {number} round + * @param {BaseLogger} contextLogger + * @return {Promise} + */ + async function createCoreChainLockUpdate(contextCoreChainLockedHeight, round, contextLogger) { + // Update Core Chain Locks + const coreChainLock = latestCoreChainLock.getChainLock(); + + let coreChainLockUpdate; + if (coreChainLock && coreChainLock.height > contextCoreChainLockedHeight) { + coreChainLockUpdate = new CoreChainLock({ + coreBlockHeight: coreChainLock.height, + coreBlockHash: coreChainLock.blockHash, + signature: coreChainLock.signature, + }); + + contextLogger.debug( + { + nextCoreChainLockHeight: coreChainLock.height, + }, + `Provide next chain lock for Core height ${coreChainLock.height}`, + ); + } + + return coreChainLockUpdate; + } + + return createCoreChainLockUpdate; +} + +module.exports = createCoreChainLockUpdateFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/deliverTxFactory.js b/packages/js-drive/lib/abci/handlers/proposal/deliverTxFactory.js new file mode 100644 index 00000000000..760c9e5e99b --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/deliverTxFactory.js @@ -0,0 +1,281 @@ +const crypto = require('crypto'); +const { FeeResult } = require('@dashevo/rs-drive'); + +const stateTransitionTypes = require('@dashevo/dpp/lib/stateTransition/stateTransitionTypes'); +const AbstractDocumentTransition = require( + '@dashevo/dpp/lib/document/stateTransition/DocumentsBatchTransition/documentTransition/AbstractDocumentTransition', +); + +const DPPValidationAbciError = require('../../errors/DPPValidationAbciError'); + +const DOCUMENT_ACTION_DESCRIPTIONS = { + [AbstractDocumentTransition.ACTIONS.CREATE]: 'Create', + [AbstractDocumentTransition.ACTIONS.REPLACE]: 'Replace', + [AbstractDocumentTransition.ACTIONS.DELETE]: 'Delete', +}; + +const DATA_CONTRACT_ACTION_DESCRIPTIONS = { + [stateTransitionTypes.DATA_CONTRACT_CREATE]: 'Create', + [stateTransitionTypes.DATA_CONTRACT_UPDATE]: 'Update', +}; + +const TIMERS = require('../timers'); + +/** + * @param {unserializeStateTransition} transactionalUnserializeStateTransition + * @param {DashPlatformProtocol} transactionalDpp + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {ExecutionTimer} executionTimer + * @param {IdentityBalanceStoreRepository} identityBalanceRepository + * @param {calculateStateTransitionFee} calculateStateTransitionFee + * @param {calculateStateTransitionFeeFromOperations} calculateStateTransitionFeeFromOperations + * @param {createContextLogger} createContextLogger + * + * @return {deliverTx} + */ +function deliverTxFactory( + transactionalUnserializeStateTransition, + transactionalDpp, + proposalBlockExecutionContext, + executionTimer, + identityBalanceRepository, + calculateStateTransitionFee, + calculateStateTransitionFeeFromOperations, + createContextLogger, +) { + /** + * @typedef deliverTx + * + * @param {Buffer} stateTransitionByteArray + * @param {number} round + * @param {BaseLogger} contextLogger + * @return {Promise<{ + * code: number, + * fees: BlockFees}>} + */ + async function deliverTx(stateTransitionByteArray, round, contextLogger) { + const blockHeight = proposalBlockExecutionContext.getHeight(); + + // Start execution timer + + executionTimer.clearTimer(TIMERS.DELIVER_TX.OVERALL); + executionTimer.clearTimer(TIMERS.DELIVER_TX.VALIDATE_BASIC); + executionTimer.clearTimer(TIMERS.DELIVER_TX.VALIDATE_FEE); + executionTimer.clearTimer(TIMERS.DELIVER_TX.VALIDATE_SIGNATURE); + executionTimer.clearTimer(TIMERS.DELIVER_TX.VALIDATE_STATE); + executionTimer.clearTimer(TIMERS.DELIVER_TX.APPLY); + + executionTimer.startTimer(TIMERS.DELIVER_TX.OVERALL); + + const stHash = crypto + .createHash('sha256') + .update(stateTransitionByteArray) + .digest() + .toString('hex') + .toUpperCase(); + + const txContextLogger = createContextLogger(contextLogger, { + txId: stHash, + }); + + txContextLogger.info(`Deliver state transition ${stHash} from block #${blockHeight}`); + + const stateTransition = await transactionalUnserializeStateTransition( + stateTransitionByteArray, + { + logger: txContextLogger, + executionTimer, + }, + ); + + // Logging + /* istanbul ignore next */ + switch (stateTransition.getType()) { + case stateTransitionTypes.DATA_CONTRACT_UPDATE: + case stateTransitionTypes.DATA_CONTRACT_CREATE: { + const dataContract = stateTransition.getDataContract(); + + // Save data contracts in order to create databases for documents on block commit + proposalBlockExecutionContext.addDataContract(dataContract); + + const description = DATA_CONTRACT_ACTION_DESCRIPTIONS[stateTransition.getType()]; + + txContextLogger.info( + { + dataContractId: dataContract.getId().toString(), + }, + `${description} Data Contract with id: ${dataContract.getId()}`, + ); + + break; + } + case stateTransitionTypes.IDENTITY_CREATE: { + const identityId = stateTransition.getIdentityId(); + + txContextLogger.info( + { + identityId: identityId.toString(), + }, + `Create Identity with id: ${identityId}`, + ); + + break; + } + case stateTransitionTypes.IDENTITY_TOP_UP: { + const identityId = stateTransition.getIdentityId(); + + txContextLogger.info( + { + identityId: identityId.toString(), + }, + `Top up Identity with id: ${identityId}`, + ); + + break; + } + case stateTransitionTypes.IDENTITY_UPDATE: { + const identityId = stateTransition.getIdentityId(); + + txContextLogger.info( + { + identityId: identityId.toString(), + }, + `Update Identity with id: ${identityId}`, + ); + break; + } + case stateTransitionTypes.DOCUMENTS_BATCH: { + stateTransition.getTransitions().forEach((transition) => { + const description = DOCUMENT_ACTION_DESCRIPTIONS[transition.getAction()]; + + txContextLogger.info( + { + documentId: transition.getId().toString(), + }, + `${description} Document with id: ${transition.getId()}`, + ); + }); + + break; + } + default: + break; + } + + // Remove dry run operations from state transition execution context + + const stateTransitionExecutionContext = stateTransition.getExecutionContext(); + + const predictedStateTransitionOperations = stateTransitionExecutionContext.getOperations(); + const predictedStateTransitionFees = calculateStateTransitionFeeFromOperations( + predictedStateTransitionOperations, + stateTransition.getOwnerId(), + ); + + stateTransitionExecutionContext.clearDryOperations(); + + // Validate against state + + executionTimer.startTimer(TIMERS.DELIVER_TX.VALIDATE_STATE); + + const result = await transactionalDpp.stateTransition.validateState(stateTransition); + + if (!result.isValid()) { + const consensusError = result.getFirstError(); + const message = 'State transition is invalid against the state'; + + txContextLogger.info(message); + txContextLogger.debug({ + consensusError, + }); + + throw new DPPValidationAbciError(message, result.getFirstError()); + } + + executionTimer.stopTimer(TIMERS.DELIVER_TX.VALIDATE_STATE, true); + + executionTimer.startTimer(TIMERS.DELIVER_TX.APPLY); + + // Apply state transition to the state + + await transactionalDpp.stateTransition.apply(stateTransition); + + executionTimer.stopTimer(TIMERS.DELIVER_TX.APPLY, true); + + // Update identity balance + + const actualStateTransitionFees = calculateStateTransitionFee(stateTransition); + + const actualStateTransitionOperations = stateTransition.getExecutionContext().getOperations(); + + if (actualStateTransitionFees.desiredAmount > predictedStateTransitionFees.desiredAmount) { + txContextLogger.warn({ + predictedFee: predictedStateTransitionFees.desiredAmount, + actualFee: actualStateTransitionFees.desiredAmount, + }, `Actual fees are greater than predicted for ${actualStateTransitionFees.desiredAmount - predictedStateTransitionFees.desiredAmount} credits`); + } + + const feeResult = FeeResult.create( + actualStateTransitionFees.storageFee, + actualStateTransitionFees.processingFee, + actualStateTransitionFees.feeRefunds, + ); + + const applyFeesToBalanceResult = await identityBalanceRepository.applyFees( + stateTransition.getOwnerId(), + feeResult, + { useTransaction: true }, + ); + + const transactionFees = applyFeesToBalanceResult.getValue(); + + const deliverTxTiming = executionTimer.stopTimer(TIMERS.DELIVER_TX.OVERALL); + + txContextLogger.trace( + { + timings: { + overall: deliverTxTiming, + validateBasic: executionTimer.getTimer(TIMERS.DELIVER_TX.VALIDATE_BASIC, true), + validateFee: executionTimer.getTimer(TIMERS.DELIVER_TX.VALIDATE_FEE, true), + validateSignature: executionTimer.getTimer(TIMERS.DELIVER_TX.VALIDATE_SIGNATURE, true), + validateState: executionTimer.getTimer(TIMERS.DELIVER_TX.VALIDATE_STATE, true), + apply: executionTimer.getTimer(TIMERS.DELIVER_TX.APPLY, true), + }, + fees: { + predicted: { + storage: predictedStateTransitionFees.storageFee, + processing: predictedStateTransitionFees.processingFee, + refunds: predictedStateTransitionFees.totalRefunds, + requiredAmount: predictedStateTransitionFees.requiredAmount, + desiredAmount: predictedStateTransitionFees.desiredAmount, + operations: predictedStateTransitionOperations.map((operation) => operation.toJSON()), + }, + actual: { + storage: actualStateTransitionFees.storageFee, + processing: actualStateTransitionFees.processingFee, + refunds: actualStateTransitionFees.totalRefunds, + requiredAmount: actualStateTransitionFees.requiredAmount, + desiredAmount: actualStateTransitionFees.desiredAmount, + operations: actualStateTransitionOperations.map((operation) => operation.toJSON()), + }, + debt: actualStateTransitionFees.desiredAmount - transactionFees.processingFee, + }, + txType: stateTransition.getType(), + }, + `${stateTransition.constructor.name} execution took ${deliverTxTiming} seconds and cost ${actualStateTransitionFees.desiredAmount} credits`, + ); + + return { + code: 0, + fees: { + storageFee: transactionFees.storageFee, + processingFee: transactionFees.processingFee, + refundsPerEpoch: transactionFees.sumFeeRefundsPerEpoch(), + }, + }; + } + + return deliverTx; +} + +module.exports = deliverTxFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/endBlockFactory.js b/packages/js-drive/lib/abci/handlers/proposal/endBlockFactory.js new file mode 100644 index 00000000000..307e50fd95b --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/endBlockFactory.js @@ -0,0 +1,125 @@ +/** + * Begin block ABCI + * + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {ValidatorSet} validatorSet + * @param {createValidatorSetUpdate} createValidatorSetUpdate + * @param {getFeatureFlagForHeight} getFeatureFlagForHeight + * @param {RSAbci} rsAbci + * @param {createConsensusParamUpdate} createConsensusParamUpdate + * @param {rotateAndCreateValidatorSetUpdate} rotateAndCreateValidatorSetUpdate + * @param {GroveDBStore} groveDBStore + * @param {ExecutionTimer} executionTimer + * + * @return {endBlock} + */ +function endBlockFactory( + proposalBlockExecutionContext, + validatorSet, + createValidatorSetUpdate, + getFeatureFlagForHeight, + createConsensusParamUpdate, + rotateAndCreateValidatorSetUpdate, + rsAbci, + groveDBStore, + executionTimer, +) { + /** + * @typedef endBlock + * + * @param {Object} request + * @param {number} request.height + * @param {number} request.round + * @param { + * storageFee: number, + * processingFee: number, + * feeRefunds: Object, + * feeRefundsSum: number + * } request.fees + * @param {number} request.coreChainLockedHeight + * @param {BaseLogger} contextLogger + * @return {Promise<{ + * consensusParamUpdates: ConsensusParams, + * validatorSetUpdate: ValidatorSetUpdate, + * nextCoreChainLockUpdate: CoreChainLock, + * }>} + */ + async function endBlock( + request, + contextLogger, + ) { + const { + height, + round, + fees, + coreChainLockedHeight, + } = request; + + // Call RS ABCI + + const rsRequest = { + fees, + }; + + contextLogger.debug(rsRequest, 'Request RS Drive\'s BlockEnd method'); + + const rsResponse = await rsAbci.blockEnd(rsRequest, true); + + contextLogger.debug(rsResponse, 'RS Drive\'s BlockEnd method response'); + + const { currentEpochIndex } = proposalBlockExecutionContext.getEpochInfo(); + + const { + processingFee, + storageFee, + feeRefundsSum, + } = fees; + + if (processingFee > 0 || storageFee > 0) { + contextLogger.debug({ + currentEpochIndex, + processingFee, + storageFee, + feeRefundsSum, + }, `${processingFee} processing fees added to epoch #${currentEpochIndex}. ${storageFee} storage fees added to distribution pool. ${feeRefundsSum} credits refunded to identities`); + } + + if (rsResponse.proposersPaidCount) { + contextLogger.debug({ + currentEpochIndex, + proposersPaidCount: rsResponse.proposersPaidCount, + paidEpochIndex: rsResponse.paidEpochIndex, + }, `${rsResponse.proposersPaidCount} masternodes were paid for epoch #${rsResponse.paidEpochIndex}`); + } + + if (rsResponse.refundedEpochsCount) { + contextLogger.debug({ + currentEpochIndex, + refundedEpochsCount: rsResponse.refundedEpochsCount, + }, `${rsResponse.refundedEpochsCount} epochs were refunded`); + } + + const consensusParamUpdates = await createConsensusParamUpdate(height, round, contextLogger); + + const validatorSetUpdate = await rotateAndCreateValidatorSetUpdate( + height, + coreChainLockedHeight, + round, + contextLogger, + ); + + const appHash = await groveDBStore.getRootHash({ useTransaction: true }); + + executionTimer.stopTimer('roundExecution', true); + + return { + consensusParamUpdates, + validatorSetUpdate, + appHash, + }; + } + + return endBlock; +} + +module.exports = endBlockFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/fees/addStateTransitionFeesToBlockFees.js b/packages/js-drive/lib/abci/handlers/proposal/fees/addStateTransitionFeesToBlockFees.js new file mode 100644 index 00000000000..2e81f9c2ad5 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/fees/addStateTransitionFeesToBlockFees.js @@ -0,0 +1,21 @@ +/** + * @param {BlockFees} blockFees + * @param {BlockFees} stFees + */ +function addStateTransitionFeesToBlockFees(blockFees, stFees) { + /* eslint-disable no-param-reassign */ + blockFees.storageFee += stFees.storageFee; + blockFees.processingFee += stFees.processingFee; + + for (const [epochIndex, credits] of Object.entries(stFees.refundsPerEpoch)) { + if (!blockFees.refundsPerEpoch[epochIndex]) { + blockFees.refundsPerEpoch[epochIndex] = 0; + } + + blockFees.refundsPerEpoch[epochIndex] += credits; + } + + return blockFees; +} + +module.exports = addStateTransitionFeesToBlockFees; diff --git a/packages/js-drive/lib/abci/handlers/proposal/processProposalFactory.js b/packages/js-drive/lib/abci/handlers/proposal/processProposalFactory.js new file mode 100644 index 00000000000..85907f69401 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/processProposalFactory.js @@ -0,0 +1,139 @@ +const { + tendermint: { + abci: { + ResponseProcessProposal, + }, + }, +} = require('@dashevo/abci/types'); + +const statuses = require('./statuses'); + +const addStateTransitionFeesToBlockFees = require('./fees/addStateTransitionFeesToBlockFees'); + +/** + * + * @param {deliverTx} wrappedDeliverTx + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {beginBlock} beginBlock + * @param {endBlock} endBlock + * @param {ExecutionTimer} executionTimer + * + * @return {processProposal} + */ +function processProposalFactory( + wrappedDeliverTx, + proposalBlockExecutionContext, + beginBlock, + endBlock, + executionTimer, +) { + /** + * @param {abci.RequestProcessProposal} request + * @param {BaseLogger} contextLogger + * + * @typedef processProposal + */ + async function processProposal(request, contextLogger) { + const { + height, + txs, + coreChainLockedHeight, + version, + proposedLastCommit: lastCommitInfo, + time, + proposerProTxHash, + proposedAppVersion, + round, + quorumHash, + } = request; + + contextLogger.info(`Processing a block proposal for height #${height} round #${round}`); + + await beginBlock( + { + lastCommitInfo, + height, + coreChainLockedHeight, + version, + time, + proposerProTxHash: Buffer.from(proposerProTxHash), + proposedAppVersion, + round, + quorumHash, + }, + contextLogger, + ); + + const txResults = []; + const blockFees = { + storageFee: 0, + processingFee: 0, + refundsPerEpoch: {}, + }; + + let validTxCount = 0; + let invalidTxCount = 0; + + for (const tx of txs) { + const { + code, + info, + fees, + } = await wrappedDeliverTx(tx, round, contextLogger); + + if (code === 0) { + validTxCount += 1; + // TODO We should calculate fees for invalid transitions as well + addStateTransitionFeesToBlockFees(blockFees, fees); + } else { + invalidTxCount += 1; + } + + const txResult = { code }; + + if (info) { + txResult.info = info; + } + + txResults.push(txResult); + } + + // Revert consensus logger after deliverTx + proposalBlockExecutionContext.setContextLogger(contextLogger); + + const { + consensusParamUpdates, + validatorSetUpdate, + appHash, + } = await endBlock({ + height, + round, + fees: blockFees, + coreChainLockedHeight, + }, contextLogger); + + const roundExecutionTime = executionTimer.getTimer('roundExecution', true); + + contextLogger.info( + { + roundExecutionTime, + validTxCount, + invalidTxCount, + }, + `Processed proposal #${height} with appHash ${appHash.toString('hex').toUpperCase()}` + + ` in ${roundExecutionTime} seconds (valid txs = ${validTxCount}, invalid txs = ${invalidTxCount})`, + ); + + return new ResponseProcessProposal({ + status: statuses.ACCEPT, + appHash, + txResults, + consensusParamUpdates, + validatorSetUpdate, + }); + } + + return processProposal; +} + +module.exports = processProposalFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.js b/packages/js-drive/lib/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.js new file mode 100644 index 00000000000..9307c5f05c2 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.js @@ -0,0 +1,51 @@ +/** + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @param {ValidatorSet} validatorSet + * @param {createValidatorSetUpdate} createValidatorSetUpdate + * @return {rotateAndCreateValidatorSetUpdate} + */ +function rotateAndCreateValidatorSetUpdateFactory( + proposalBlockExecutionContext, + validatorSet, + createValidatorSetUpdate, +) { + /** + * @typedef rotateAndCreateValidatorSetUpdate + * @param {number} height + * @param {number} coreChainLockedHeight + * @param {number} round + * @param {BaseLogger} contextLogger + * @return {Promise} + */ + async function rotateAndCreateValidatorSetUpdate( + height, + coreChainLockedHeight, + round, + contextLogger, + ) { + const lastCommitInfo = proposalBlockExecutionContext.getLastCommitInfo(); + + // Rotate validators + + let validatorSetUpdate; + const rotationEntropy = Buffer.from(lastCommitInfo.blockSignature); + if (await validatorSet.rotate(height, coreChainLockedHeight, rotationEntropy)) { + validatorSetUpdate = createValidatorSetUpdate(validatorSet); + + const { quorumHash } = validatorSet.getQuorum(); + + contextLogger.debug( + { + quorumHash, + }, + `Validator set switched to ${quorumHash} quorum`, + ); + } + + return validatorSetUpdate; + } + + return rotateAndCreateValidatorSetUpdate; +} + +module.exports = rotateAndCreateValidatorSetUpdateFactory; diff --git a/packages/js-drive/lib/abci/handlers/proposal/statuses.js b/packages/js-drive/lib/abci/handlers/proposal/statuses.js new file mode 100644 index 00000000000..adb8785b6c5 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/statuses.js @@ -0,0 +1,5 @@ +module.exports = { + UNKNOWN: 0, // Unknown status. Returning this from the application is always an error. + ACCEPT: 1, // Status that signals that the application finds the proposal valid. + REJECT: 2, // Status that signals that the application finds the proposal invalid. +}; diff --git a/packages/js-drive/lib/abci/handlers/proposal/verifyChainLockFactory.js b/packages/js-drive/lib/abci/handlers/proposal/verifyChainLockFactory.js new file mode 100644 index 00000000000..3fb6100b860 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/proposal/verifyChainLockFactory.js @@ -0,0 +1,69 @@ +/** + * + * @param {RpcClient} coreRpcClient + * @param {BlockExecutionContext} latestBlockExecutionContext + * @param {BaseLogger} logger + * @return {verifyChainLock} + */ +function verifyChainLockFactory( + coreRpcClient, + latestBlockExecutionContext, + logger, +) { + /** + * @typedef verifyChainLock + * @param {ChainLockUpdate} coreChainLock + * @return {Promise} + */ + async function verifyChainLock(coreChainLock) { + const serializedCoreChainLock = { + height: coreChainLock.coreBlockHeight, + signature: Buffer.from(coreChainLock.signature).toString('hex'), + blockHash: Buffer.from(coreChainLock.coreBlockHash).toString('hex'), + }; + + const lastCoreChainLockedHeight = latestBlockExecutionContext.getCoreChainLockedHeight(); + if (coreChainLock.coreBlockHeight <= lastCoreChainLockedHeight) { + logger.debug( + { + chainLock: serializedCoreChainLock, + lastCoreChainLockedHeight, + }, + 'Chainlock verification failed: coreBlockHeight must be bigger than the latest core chain locked height', + ); + + return false; + } + + let isVerified; + try { + ({ result: isVerified } = await coreRpcClient.verifyChainLock( + serializedCoreChainLock.blockHash, + serializedCoreChainLock.signature, + serializedCoreChainLock.height, + )); + } catch (e) { + // Invalid signature format + // Parse error + if ([-8, -32700].includes(e.code)) { + logger.debug( + { + err: e, + chainLock: serializedCoreChainLock, + }, + `Chainlock verification failed using verifyChainLock method: ${e.message} ${e.code}`, + ); + + return false; + } + + throw e; + } + + return isVerified; + } + + return verifyChainLock; +} + +module.exports = verifyChainLockFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/dataContractQueryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/query/dataContractQueryHandlerFactory.js new file mode 100644 index 00000000000..2948bae4bcf --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/dataContractQueryHandlerFactory.js @@ -0,0 +1,74 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetDataContractResponse, + }, +} = require('@dashevo/dapi-grpc'); + +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentifierError = require('@dashevo/dpp/lib/identifier/errors/IdentifierError'); + +const NotFoundAbciError = require('../../errors/NotFoundAbciError'); +const InvalidArgumentAbciError = require('../../errors/InvalidArgumentAbciError'); + +/** + * + * @param {DataContractStoreRepository} dataContractRepository + * @param {createQueryResponse} createQueryResponse + * @return {dataContractQueryHandler} + */ +function dataContractQueryHandlerFactory( + dataContractRepository, + createQueryResponse, +) { + /** + * @typedef dataContractQueryHandler + * @param {Object} params + * @param {Object} data + * @param {Buffer} data.id + * @param {RequestQuery} request + * @return {Promise} + */ + async function dataContractQueryHandler(params, { id }, request) { + let contractIdIdentifier; + try { + contractIdIdentifier = new Identifier(id); + } catch (e) { + if (e instanceof IdentifierError) { + throw new InvalidArgumentAbciError('id must be a valid identifier (32 bytes long)'); + } + + throw e; + } + + const response = createQueryResponse(GetDataContractResponse, request.prove); + + if (request.prove) { + const proof = await dataContractRepository.prove(contractIdIdentifier); + + response.getProof().setMerkleProof(proof.getValue()); + } else { + const dataContract = await dataContractRepository.fetch(contractIdIdentifier); + if (dataContract.isNull()) { + throw new NotFoundAbciError('Data Contract not found'); + } + + response.setDataContract(dataContract.getValue().toBuffer()); + } + + return new ResponseQuery({ + value: response.serializeBinary(), + }); + } + + return dataContractQueryHandler; +} + +module.exports = dataContractQueryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/documentQueryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/query/documentQueryHandlerFactory.js new file mode 100644 index 00000000000..c78f6445dbb --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/documentQueryHandlerFactory.js @@ -0,0 +1,97 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetDocumentsResponse, + }, +} = require('@dashevo/dapi-grpc'); + +const InvalidArgumentAbciError = require('../../errors/InvalidArgumentAbciError'); +const InvalidQueryError = require('../../../document/errors/InvalidQueryError'); + +/** + * + * @param {fetchDocuments} fetchDocuments + * @param {proveDocuments} proveDocuments + * @param {createQueryResponse} createQueryResponse + * @return {documentQueryHandler} + */ +function documentQueryHandlerFactory( + fetchDocuments, + proveDocuments, + createQueryResponse, +) { + /** + * @typedef {documentQueryHandler} + * @param {Object} params + * @param {Object} data + * @param {Buffer} data.contractId + * @param {string} data.type + * @param {string} [data.where] + * @param {string} [data.orderBy] + * @param {string} [data.limit] + * @param {Buffer} [data.startAfter] + * @param {Buffer} [data.startAt] + * @param {RequestQuery} request + * @return {Promise} + */ + async function documentQueryHandler( + params, + { + contractId, + type, + where, + orderBy, + limit, + startAfter, + startAt, + }, + request, + ) { + const response = createQueryResponse(GetDocumentsResponse, request.prove); + + const options = { + where, + orderBy, + limit, + startAfter: startAfter ? Buffer.from(startAfter) : startAfter, + startAt: startAt ? Buffer.from(startAt) : startAt, + }; + + try { + if (request.prove) { + const proof = await proveDocuments(contractId, type, options); + + response.getProof().setMerkleProof(proof.getValue()); + } else { + const documentsResult = await fetchDocuments(contractId, type, options); + + const documents = documentsResult.getValue(); + + response.setDocumentsList( + documents.map((document) => document.toBuffer()), + ); + } + } catch (e) { + if (e instanceof InvalidQueryError) { + throw new InvalidArgumentAbciError(`Invalid query: ${e.message}`); + } + + throw e; + } + + return new ResponseQuery({ + value: response.serializeBinary(), + }); + } + + return documentQueryHandler; +} + +module.exports = documentQueryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/getProofsQueryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/query/getProofsQueryHandlerFactory.js new file mode 100644 index 00000000000..585fb8db9b4 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/getProofsQueryHandlerFactory.js @@ -0,0 +1,110 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const cbor = require('cbor'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); + +/** + * + * @param {BlockExecutionContext} latestBlockExecutionContext + * @param {IdentityStoreRepository} identityRepository + * @param {DataContractStoreRepository} dataContractRepository + * @param {DocumentRepository} documentRepository + * @return {getProofsQueryHandler} + */ +function getProofsQueryHandlerFactory( + latestBlockExecutionContext, + identityRepository, + dataContractRepository, + documentRepository, +) { + /** + * @typedef getProofsQueryHandler + * @param params + * @param callArguments + * @param {Buffer[]} callArguments.identityIds + * @param {Buffer[]} callArguments.dataContractIds + * @param {{dataContractId: Buffer, documentId: Buffer, type: string}[]} documents + * @return {Promise} + */ + async function getProofsQueryHandler(params, { + identityIds, + dataContractIds, + documents, + }) { + const blockHeight = latestBlockExecutionContext.getHeight(); + const coreChainLockedHeight = latestBlockExecutionContext.getCoreChainLockedHeight(); + const timeMs = latestBlockExecutionContext.getTimeMs(); + const version = latestBlockExecutionContext.getVersion(); + const { + quorumHash, + blockSignature: signature, + } = latestBlockExecutionContext.getLastCommitInfo(); + const round = latestBlockExecutionContext.getRound(); + + const response = { + documentsProof: null, + identitiesProof: null, + dataContractsProof: null, + metadata: { + height: blockHeight.toNumber(), + coreChainLockedHeight, + timeMs, + protocolVersion: version.app.toNumber(), + }, + }; + + if (documents && documents.length) { + const documentsProof = await documentRepository + .proveManyDocumentsFromDifferentContracts(documents); + + response.documentsProof = { + quorumHash, + signature, + merkleProof: documentsProof.getValue(), + round, + }; + } + + if (identityIds && identityIds.length) { + const identitiesProof = await identityRepository.proveMany( + identityIds.map((identityId) => Identifier.from(identityId)), + ); + + response.identitiesProof = { + quorumHash, + signature, + merkleProof: identitiesProof.getValue(), + round, + }; + } + + if (dataContractIds && dataContractIds.length) { + const dataContractsProof = await dataContractRepository.proveMany( + dataContractIds.map((dataContractId) => Identifier.from(dataContractId)), + ); + + response.dataContractsProof = { + quorumHash, + signature, + merkleProof: dataContractsProof.getValue(), + round, + }; + } + + return new ResponseQuery({ + value: await cbor.encodeAsync( + response, + ), + }); + } + + return getProofsQueryHandler; +} + +module.exports = getProofsQueryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.js new file mode 100644 index 00000000000..3b1706bda87 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.js @@ -0,0 +1,70 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetIdentitiesByPublicKeyHashesResponse, + }, +} = require('@dashevo/dapi-grpc'); + +const InvalidArgumentAbciError = require('../../errors/InvalidArgumentAbciError'); + +/** + * + * @param {IdentityStoreRepository} identityRepository + * @param {number} maxIdentitiesPerRequest + * @param {createQueryResponse} createQueryResponse + * @return {identitiesByPublicKeyHashesQueryHandler} + */ +function identitiesByPublicKeyHashesQueryHandlerFactory( + identityRepository, + maxIdentitiesPerRequest, + createQueryResponse, +) { + /** + * @typedef identitiesByPublicKeyHashesQueryHandler + * @param {Object} params + * @param {Object} data + * @param {Buffer[]} data.publicKeyHashes + * @param {RequestQuery} request + * @return {Promise} + */ + async function identitiesByPublicKeyHashesQueryHandler(params, { publicKeyHashes }, request) { + if (publicKeyHashes && publicKeyHashes.length > maxIdentitiesPerRequest) { + throw new InvalidArgumentAbciError( + `Maximum number of ${maxIdentitiesPerRequest} requested items exceeded.`, { + maxIdentitiesPerRequest, + }, + ); + } + + const response = createQueryResponse(GetIdentitiesByPublicKeyHashesResponse, request.prove); + + if (request.prove) { + const proof = await identityRepository.proveManyByPublicKeyHashes(publicKeyHashes); + + response.getProof().setMerkleProof(proof.getValue()); + } else { + const identitiesListResult = await identityRepository.fetchManyByPublicKeyHashes( + publicKeyHashes, + ); + + response.setIdentitiesList( + identitiesListResult.getValue().map((identity) => identity.toBuffer()), + ); + } + + return new ResponseQuery({ + value: response.serializeBinary(), + }); + } + + return identitiesByPublicKeyHashesQueryHandler; +} + +module.exports = identitiesByPublicKeyHashesQueryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/identityQueryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/query/identityQueryHandlerFactory.js new file mode 100644 index 00000000000..179002335b1 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/identityQueryHandlerFactory.js @@ -0,0 +1,75 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetIdentityResponse, + }, +} = require('@dashevo/dapi-grpc'); + +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentifierError = require('@dashevo/dpp/lib/identifier/errors/IdentifierError'); + +const NotFoundAbciError = require('../../errors/NotFoundAbciError'); +const InvalidArgumentAbciError = require('../../errors/InvalidArgumentAbciError'); + +/** + * + * @param {IdentityStoreRepository} identityRepository + * @param {createQueryResponse} createQueryResponse + * @return {identityQueryHandler} + */ +function identityQueryHandlerFactory( + identityRepository, + createQueryResponse, +) { + /** + * @typedef identityQueryHandler + * @param {Object} params + * @param {Object} options + * @param {Buffer} options.id + * @param {RequestQuery} request + * @return {Promise} + */ + async function identityQueryHandler(params, { id }, request) { + let identifier; + try { + identifier = new Identifier(id); + } catch (e) { + if (e instanceof IdentifierError) { + throw new InvalidArgumentAbciError('id must be a valid identifier (32 bytes long)'); + } + + throw e; + } + + const response = createQueryResponse(GetIdentityResponse, request.prove); + + if (request.prove) { + const proof = await identityRepository.prove(identifier); + + response.getProof().setMerkleProof(proof.getValue()); + } else { + const identityResult = await identityRepository.fetch(identifier); + + if (identityResult.isNull()) { + throw new NotFoundAbciError('Identity not found'); + } + + response.setIdentity(identityResult.getValue().toBuffer()); + } + + return new ResponseQuery({ + value: response.serializeBinary(), + }); + } + + return identityQueryHandler; +} + +module.exports = identityQueryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/query/response/createQueryResponseFactory.js b/packages/js-drive/lib/abci/handlers/query/response/createQueryResponseFactory.js new file mode 100644 index 00000000000..cf22cf73228 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/query/response/createQueryResponseFactory.js @@ -0,0 +1,65 @@ +const { + v0: { + Proof, + ResponseMetadata, + }, +} = require('@dashevo/dapi-grpc'); + +const UnavailableAbciError = require('../../../errors/UnavailableAbciError'); + +/** + * @param {BlockExecutionContext} latestBlockExecutionContext + * @return {createQueryResponse} + */ +function createQueryResponseFactory( + latestBlockExecutionContext, +) { + /** + * @typedef {createQueryResponse} + * @param {Function} ResponseClass + * @param {boolean} [prove=false] + */ + function createQueryResponse(ResponseClass, prove = false) { + if (latestBlockExecutionContext.isEmpty()) { + throw new UnavailableAbciError('data is not available'); + } + + const blockHeight = latestBlockExecutionContext.getHeight(); + const coreChainLockedHeight = latestBlockExecutionContext.getCoreChainLockedHeight(); + const timeMs = latestBlockExecutionContext.getTimeMs(); + const version = latestBlockExecutionContext.getVersion(); + + const response = new ResponseClass(); + + const metadata = new ResponseMetadata(); + metadata.setHeight(blockHeight); + metadata.setCoreChainLockedHeight(coreChainLockedHeight); + metadata.setTimeMs(timeMs); + metadata.setProtocolVersion(version.app); + + response.setMetadata(metadata); + + if (prove) { + const { + quorumHash, + blockSignature: signature, + } = latestBlockExecutionContext.getLastCommitInfo(); + + const round = latestBlockExecutionContext.getRound(); + + const proof = new Proof(); + + proof.setQuorumHash(quorumHash); + proof.setSignature(signature); + proof.setRound(round); + + response.setProof(proof); + } + + return response; + } + + return createQueryResponse; +} + +module.exports = createQueryResponseFactory; diff --git a/packages/js-drive/lib/abci/handlers/queryHandlerFactory.js b/packages/js-drive/lib/abci/handlers/queryHandlerFactory.js new file mode 100644 index 00000000000..338538af6a0 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/queryHandlerFactory.js @@ -0,0 +1,58 @@ +const cbor = require('cbor'); + +const InvalidArgumentAbciError = require('../errors/InvalidArgumentAbciError'); + +/** + * @param {Object} queryHandlerRouter + * @param {Function} sanitizeUrl + * @param {BaseLogger} logger + * @param {createContextLogger} createContextLogger + * @return {queryHandler} + */ +function queryHandlerFactory(queryHandlerRouter, sanitizeUrl, logger, createContextLogger) { + /** + * Query ABCI Handler + * + * @typedef queryHandler + * + * @param {RequestQuery} request + * @return {Promise} + */ + async function queryHandler(request) { + const { path, data } = request; + + createContextLogger(logger, { + abciMethod: 'query', + }); + + const route = queryHandlerRouter.find('GET', sanitizeUrl(path)); + + if (!route) { + throw new InvalidArgumentAbciError('Invalid path', { path }); + } + + const invalidDataMessage = 'Invalid data format: it should be cbor encoded object.'; + + let encodedData = {}; + + const decodeData = route.store && route.store.rawData === true; + + if (data.length > 0) { + try { + encodedData = decodeData ? Buffer.from(data) : cbor.decode(Buffer.from(data)); + } catch (e) { + throw new InvalidArgumentAbciError(invalidDataMessage); + } + + if (encodedData === null || typeof encodedData !== 'object') { + throw new InvalidArgumentAbciError(invalidDataMessage); + } + } + + return route.handler(route.params, encodedData, request); + } + + return queryHandler; +} + +module.exports = queryHandlerFactory; diff --git a/packages/js-drive/lib/abci/handlers/stateTransition/unserializeStateTransitionFactory.js b/packages/js-drive/lib/abci/handlers/stateTransition/unserializeStateTransitionFactory.js new file mode 100644 index 00000000000..91433eedbbd --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/stateTransition/unserializeStateTransitionFactory.js @@ -0,0 +1,120 @@ +const InvalidStateTransitionError = require('@dashevo/dpp/lib/stateTransition/errors/InvalidStateTransitionError'); +const InvalidArgumentAbciError = require('../../errors/InvalidArgumentAbciError'); + +const DPPValidationAbciError = require('../../errors/DPPValidationAbciError'); + +const TIMERS = require('../timers'); + +/** + * @param {DashPlatformProtocol} dpp + * @param {Object} noopLogger + * @return {unserializeStateTransition} + */ +function unserializeStateTransitionFactory(dpp, noopLogger) { + /** + * @typedef unserializeStateTransition + * @param {Uint8Array} stateTransitionByteArray + * @param {Object} [options] + * @param {BaseLogger} [options.logger] + * @param {ExecutionTimer} [options.executionTimer] + * @return {AbstractStateTransition} + */ + async function unserializeStateTransition(stateTransitionByteArray, options = {}) { + // either use a logger passed or use noop logger + const logger = (options.logger || noopLogger); + + // measure timing if timer is passed + const executionTimer = (options.executionTimer || { + startTimer: () => {}, + stopTimer: () => {}, + }); + + if (!stateTransitionByteArray) { + logger.warn('State transition is not specified'); + + throw new InvalidArgumentAbciError('State Transition is not specified'); + } + + const stateTransitionSerialized = Buffer.from(stateTransitionByteArray); + + executionTimer.startTimer(TIMERS.DELIVER_TX.VALIDATE_BASIC); + + let stateTransition; + try { + stateTransition = await dpp + .stateTransition + .createFromBuffer(stateTransitionSerialized); + } catch (e) { + if (e instanceof InvalidStateTransitionError) { + const consensusError = e.getErrors()[0]; + const message = 'Invalid state transition'; + + logger.info(message); + logger.debug({ + consensusError, + }); + + throw new DPPValidationAbciError(message, consensusError); + } + + throw e; + } + + executionTimer.stopTimer(TIMERS.DELIVER_TX.VALIDATE_BASIC, true); + + executionTimer.startTimer(TIMERS.DELIVER_TX.VALIDATE_SIGNATURE); + + let result = await dpp.stateTransition.validateSignature(stateTransition); + + if (!result.isValid()) { + const consensusError = result.getFirstError(); + const message = 'Invalid state transition signature'; + + logger.info(message); + + logger.debug({ + consensusError, + }); + + throw new DPPValidationAbciError(message, consensusError); + } + + executionTimer.stopTimer(TIMERS.DELIVER_TX.VALIDATE_SIGNATURE, true); + + executionTimer.startTimer(TIMERS.DELIVER_TX.VALIDATE_FEE); + + const executionContext = stateTransition.getExecutionContext(); + + // Pre-calculate fee for validateState and state transition apply + // with worst case costs to validate the whole state transition execution cost + executionContext.enableDryRun(); + + await dpp.stateTransition.validateState(stateTransition); + await dpp.stateTransition.apply(stateTransition); + + executionContext.disableDryRun(); + + result = await dpp.stateTransition.validateFee(stateTransition); + + if (!result.isValid()) { + const consensusError = result.getFirstError(); + const message = 'Insufficient funds to process state transition'; + + logger.info(message); + + logger.debug({ + consensusError, + }); + + throw new DPPValidationAbciError(message, consensusError); + } + + executionTimer.stopTimer(TIMERS.DELIVER_TX.VALIDATE_FEE, true); + + return stateTransition; + } + + return unserializeStateTransition; +} + +module.exports = unserializeStateTransitionFactory; diff --git a/packages/js-drive/lib/abci/handlers/timers.js b/packages/js-drive/lib/abci/handlers/timers.js new file mode 100644 index 00000000000..8aa29e8c4cf --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/timers.js @@ -0,0 +1,10 @@ +module.exports = { + DELIVER_TX: { + OVERALL: 'deliverTx:overall', + VALIDATE_BASIC: 'deliverTx:validate:basic', + VALIDATE_FEE: 'deliverTx:validate:fee', + VALIDATE_SIGNATURE: 'deliverTx:validate:signature', + VALIDATE_STATE: 'deliverTx:validate:state', + APPLY: 'deliverTx:apply', + }, +}; diff --git a/packages/js-drive/lib/abci/handlers/validator/createValidatorSetUpdate.js b/packages/js-drive/lib/abci/handlers/validator/createValidatorSetUpdate.js new file mode 100644 index 00000000000..60287f54b5e --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/validator/createValidatorSetUpdate.js @@ -0,0 +1,49 @@ +const { + tendermint: { + abci: { + ValidatorUpdate, + ValidatorSetUpdate, + }, + crypto: { + PublicKey, + }, + }, +} = require('@dashevo/abci/types'); + +/** + * @typedef {createValidatorSetUpdate} + * @param {ValidatorSet} validatorSet + * @return {ValidatorSetUpdate} + */ +function createValidatorSetUpdate(validatorSet) { + const validatorUpdates = validatorSet.getValidators() + .map((validator) => { + const networkInfo = validator.getNetworkInfo(); + + const validatorUpdate = new ValidatorUpdate({ + power: validator.getVotingPower(), + proTxHash: validator.getProTxHash(), + nodeAddress: `tcp://${networkInfo.getHost()}:${networkInfo.getPort()}`, + }); + + if (validator.getPublicKeyShare()) { + validatorUpdate.pubKey = new PublicKey({ + bls12381: validator.getPublicKeyShare(), + }); + } + + return validatorUpdate; + }); + + const { quorumPublicKey, quorumHash } = validatorSet.getQuorum(); + + return new ValidatorSetUpdate({ + validatorUpdates, + thresholdPublicKey: new PublicKey({ + bls12381: Buffer.from(quorumPublicKey, 'hex'), + }), + quorumHash: Buffer.from(quorumHash, 'hex'), + }); +} + +module.exports = createValidatorSetUpdate; diff --git a/packages/js-drive/lib/abci/handlers/verifyVoteExtensionHandlerFactory.js b/packages/js-drive/lib/abci/handlers/verifyVoteExtensionHandlerFactory.js new file mode 100644 index 00000000000..c414ef3e4a7 --- /dev/null +++ b/packages/js-drive/lib/abci/handlers/verifyVoteExtensionHandlerFactory.js @@ -0,0 +1,92 @@ +const { + tendermint: { + abci: { + ResponseVerifyVoteExtension, + }, + types: { + VoteExtensionType, + }, + }, +} = require('@dashevo/abci/types'); + +const verifyStatus = { + UNKNOWN: 0, // Unknown status. Returning this from the application is always an error. + ACCEPT: 1, // Status that signals that the application finds the vote extension valid. + REJECT: 2, // Status that signals that the application finds the vote extension invalid. +}; + +/** + * @param {BlockExecutionContext} proposalBlockExecutionContext + * @return {verifyVoteExtensionHandler} + */ +function verifyVoteExtensionHandlerFactory(proposalBlockExecutionContext) { + /** + * @typedef verifyVoteExtensionHandler + * + * @param {abci.RequestVerifyVoteExtension} request + * + * @return {Promise} + */ + async function verifyVoteExtensionHandler(request) { + const { + voteExtensions, + } = request; + + const contextLogger = proposalBlockExecutionContext.getContextLogger() + .child({ + abciMethod: 'verifyVoteExtension', + }); + + contextLogger.debug('VerifyVote ABCI method requested'); + contextLogger.trace({ request }); + + const unsignedWithdrawalTransactionsMap = proposalBlockExecutionContext + .getWithdrawalTransactionsMap(); + + const voteExtensionsToCheck = Object.keys(unsignedWithdrawalTransactionsMap || {}) + .sort() + .map((txHashHex) => ({ + type: VoteExtensionType.THRESHOLD_RECOVER, + extension: Buffer.from(txHashHex, 'hex'), + })); + + const numberOfVoteExtensionsMatch = ( + voteExtensionsToCheck.length === (voteExtensions || []).length + ); + + const allVoteExtensionsPresent = voteExtensionsToCheck.reduce((result, nextExtension) => { + const searchedVoteExtension = (voteExtensions || []).find((voteExtension) => ( + voteExtension.type === nextExtension.type + && Buffer.compare(voteExtension.extension, nextExtension.extension) + )); + + if (!searchedVoteExtension) { + const extensionString = nextExtension.extension.toString('hex'); + + const extensionTruncatedString = extensionString.substring( + 0, + Math.min(30, extensionString.length), + ); + + contextLogger.warn({ + type: nextExtension.type, + extension: extensionString, + }, `${nextExtension.type} vote extension ${extensionTruncatedString}... was not found in verify request`); + } + + return result && (searchedVoteExtension !== undefined); + }, true); + + const status = (numberOfVoteExtensionsMatch && allVoteExtensionsPresent) + ? verifyStatus.ACCEPT + : verifyStatus.REJECT; + + return new ResponseVerifyVoteExtension({ + status, + }); + } + + return verifyVoteExtensionHandler; +} + +module.exports = verifyVoteExtensionHandlerFactory; diff --git a/packages/js-drive/lib/blockExecution/BlockExecutionContext.js b/packages/js-drive/lib/blockExecution/BlockExecutionContext.js new file mode 100644 index 00000000000..52df4c12f93 --- /dev/null +++ b/packages/js-drive/lib/blockExecution/BlockExecutionContext.js @@ -0,0 +1,383 @@ +const DataContract = require('@dashevo/dpp/lib/dataContract/DataContract'); + +const { + tendermint: { + abci: { + CommitInfo, + }, + version: { + Consensus, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +class BlockExecutionContext { + constructor() { + this.reset(); + } + + /** + * Add Data Contract + * + * @param {DataContract|null} dataContract + */ + addDataContract(dataContract) { + this.dataContracts.push(dataContract); + } + + /** + * Check is data contract with specific ID is persistent in the context + * + * @param {Identifier} dataContractId + * @return {boolean} + */ + hasDataContract(dataContractId) { + const index = this.dataContracts + .findIndex((dataContract) => dataContractId.equals(dataContract.getId())); + + return index !== -1; + } + + /** + * Get Data Contracts + * + * @returns {DataContract[]} + */ + getDataContracts() { + return this.dataContracts; + } + + /** + * + * @param {number} coreChainLockedHeight + * @return {BlockExecutionContext} + */ + setCoreChainLockedHeight(coreChainLockedHeight) { + this.coreChainLockedHeight = coreChainLockedHeight; + + return this; + } + + /** + * + * @return {number} + */ + getCoreChainLockedHeight() { + return this.coreChainLockedHeight; + } + + /** + * @param {Long} height + * @return {BlockExecutionContext} + */ + setHeight(height) { + this.height = height; + + return this; + } + + /** + * + * @return {Long} + */ + getHeight() { + return this.height; + } + + /** + * + * @param {IConsensus} version + * @return {BlockExecutionContext} + */ + setVersion(version) { + this.version = version; + + return this; + } + + /** + * + * @return {IConsensus} + */ + getVersion() { + return this.version; + } + + /** + * + * @param {Long} version + * @return {BlockExecutionContext} + */ + setProposedAppVersion(version) { + this.proposedAppVersion = version; + + return this; + } + + /** + * + * @return {Long} + */ + getProposedAppVersion() { + return this.proposedAppVersion; + } + + /** + * @param {number} timeMs + */ + setTimeMs(timeMs) { + this.timeMs = timeMs; + } + + /** + * @returns {number} + */ + getTimeMs() { + return this.timeMs; + } + + /** + * Set current block lastCommitInfo + * @param {ILastCommitInfo} lastCommitInfo + * @return {BlockExecutionContext} + */ + setLastCommitInfo(lastCommitInfo) { + this.lastCommitInfo = lastCommitInfo; + + return this; + } + + /** + * Get block lastCommitInfo + * + * @return {ILastCommitInfo|null} + */ + getLastCommitInfo() { + return this.lastCommitInfo; + } + + /** + * Set context logger + * + * @param {BaseLogger} logger + */ + setContextLogger(logger) { + this.contextLogger = logger; + } + + /** + * Get context logger + * + * @return {BaseLogger} + */ + getContextLogger() { + if (!this.contextLogger) { + throw new Error('Consensus logger has not been set'); + } + + return this.contextLogger; + } + + /** + * @param {EpochInfo} epochInfo + */ + setEpochInfo(epochInfo) { + this.epochInfo = epochInfo; + } + + /** + * @returns {EpochInfo} + */ + getEpochInfo() { + return this.epochInfo; + } + + /** + * Set withdrawal transactions hash map + * + * @param {Object} withdrawalTransactionsMap + * + * @returns {BlockExecutionContext} + */ + setWithdrawalTransactionsMap(withdrawalTransactionsMap) { + this.withdrawalTransactionsMap = withdrawalTransactionsMap; + + return this; + } + + /** + * Get withdrawal transactions hash map + * + * @return {Object} + */ + getWithdrawalTransactionsMap() { + return this.withdrawalTransactionsMap; + } + + /** + * Set committed round + * + * @param {number} round + * + * @returns {BlockExecutionContext} + */ + setRound(round) { + this.round = round; + + return this; + } + + /** + * Get committed round + * + * @return {number} + */ + getRound() { + return this.round; + } + + /** + * Set PrepareProposal Result + * + * @param {Object} prepareProposalResult + * + * @returns {BlockExecutionContext} + */ + setPrepareProposalResult(prepareProposalResult) { + this.prepareProposalResult = prepareProposalResult; + + return this; + } + + /** + * Get PrepareProposal Result + * + * @return {Object} + */ + getPrepareProposalResult() { + return this.prepareProposalResult; + } + + /** + * Reset state + */ + reset() { + this.dataContracts = []; + this.coreChainLockedHeight = null; + this.height = null; + this.version = null; + this.time = null; + this.lastCommitInfo = null; + this.contextLogger = null; + this.withdrawalTransactionsMap = {}; + this.round = null; + this.epochInfo = null; + this.timeMs = null; + this.prepareProposalResult = null; + this.proposedAppVersion = null; + } + + /** + * Check is the context is not set + * + * @return {boolean} + */ + isEmpty() { + return this.height === null; + } + + /** + * Populate the current instance with data from another instance + * + * @param {BlockExecutionContext} blockExecutionContext + */ + populate(blockExecutionContext) { + this.dataContracts = blockExecutionContext.dataContracts; + this.lastCommitInfo = blockExecutionContext.lastCommitInfo; + this.time = blockExecutionContext.time; + this.height = blockExecutionContext.height; + this.coreChainLockedHeight = blockExecutionContext.coreChainLockedHeight; + this.version = blockExecutionContext.version; + this.contextLogger = blockExecutionContext.contextLogger || null; + this.withdrawalTransactionsMap = blockExecutionContext.withdrawalTransactionsMap; + this.round = blockExecutionContext.round; + this.epochInfo = blockExecutionContext.epochInfo; + this.timeMs = blockExecutionContext.timeMs; + this.prepareProposalResult = blockExecutionContext.prepareProposalResult || null; + this.proposedAppVersion = blockExecutionContext.proposedAppVersion; + } + + /** + * Populate the current instance with data + * + * @param {Object} object + */ + fromObject(object) { + this.dataContracts = object.dataContracts + .map((rawDataContract) => new DataContract(rawDataContract)); + this.lastCommitInfo = CommitInfo.fromObject(object.lastCommitInfo); + this.contextLogger = object.contextLogger; + this.epochInfo = object.epochInfo; + this.timeMs = object.timeMs; + this.height = Long.fromNumber(object.height); + this.coreChainLockedHeight = object.coreChainLockedHeight; + this.version = Consensus.fromObject(object.version); + this.withdrawalTransactionsMap = object.withdrawalTransactionsMap; + this.round = object.round; + this.prepareProposalResult = object.prepareProposalResult; + this.proposedAppVersion = Long.fromNumber(object.proposedAppVersion); + } + + /** + * @param {Object} options + * @param {boolean} [options.skipContextLogger=false] + * @param {boolean} [options.skipPrepareProposalResult=false] + * @return {{ + * dataContracts: Object[], + * height: number, + * version: Object, + * timeMs: number, + * coreChainLockedHeight: number, + * lastCommitInfo: number, + * epochInfo: EpochInfo, + * withdrawalTransactionsMap: Object, + * round: number, + * proposedAppVersion: number, + * }} + */ + toObject(options = {}) { + let time = null; + + if (this.time) { + time = this.time.toJSON(); + time.seconds = Number(time.seconds); + } + + const object = { + dataContracts: this.dataContracts.map((dataContract) => dataContract.toObject()), + timeMs: this.timeMs, + height: this.height ? this.height.toNumber() : null, + version: this.version ? this.version.toJSON() : null, + coreChainLockedHeight: this.coreChainLockedHeight, + lastCommitInfo: this.lastCommitInfo ? CommitInfo.toObject(this.lastCommitInfo) : null, + withdrawalTransactionsMap: this.withdrawalTransactionsMap, + round: this.round, + epochInfo: this.epochInfo, + proposedAppVersion: this.proposedAppVersion ? this.proposedAppVersion.toNumber() : null, + }; + + if (!options.skipContextLogger) { + object.contextLogger = this.contextLogger; + } + + if (!options.skipPrepareProposalResult) { + object.prepareProposalResult = this.prepareProposalResult; + } + + return object; + } +} + +module.exports = BlockExecutionContext; diff --git a/packages/js-drive/lib/blockExecution/BlockExecutionContextRepository.js b/packages/js-drive/lib/blockExecution/BlockExecutionContextRepository.js new file mode 100644 index 00000000000..e7e2800ca81 --- /dev/null +++ b/packages/js-drive/lib/blockExecution/BlockExecutionContextRepository.js @@ -0,0 +1,69 @@ +const cbor = require('cbor'); + +const BlockExecutionContext = require('./BlockExecutionContext'); + +class BlockExecutionContextRepository { + /** + * + * @param {GroveDBStore} groveDBStore + */ + constructor(groveDBStore) { + this.db = groveDBStore; + } + + /** + * Store block execution context + * + * @param {BlockExecutionContext} blockExecutionContext + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {this} + */ + async store(blockExecutionContext, options = {}) { + await this.db.putAux( + BlockExecutionContextRepository.EXTERNAL_STORE_KEY_NAME, + await cbor.encodeAsync(blockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })), + options, + ); + + return this; + } + + /** + * Fetch block execution stack + * + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {BlockExecutionContext} + */ + async fetch(options = {}) { + const blockExecutionContextEncodedResult = await this.db.getAux( + BlockExecutionContextRepository.EXTERNAL_STORE_KEY_NAME, + options, + ); + + const blockExecutionContextEncoded = blockExecutionContextEncodedResult.getValue(); + + const blockExecutionContext = new BlockExecutionContext(); + + if (!blockExecutionContextEncoded) { + return blockExecutionContext; + } + + const rawBlockExecutionContext = cbor.decode(blockExecutionContextEncoded); + + const context = new BlockExecutionContext(); + + context.fromObject(rawBlockExecutionContext); + + return context; + } +} + +BlockExecutionContextRepository.EXTERNAL_STORE_KEY_NAME = Buffer.from('blockExecutionContext'); + +module.exports = BlockExecutionContextRepository; diff --git a/packages/js-drive/lib/blockExecution/BlockInfo.js b/packages/js-drive/lib/blockExecution/BlockInfo.js new file mode 100644 index 00000000000..75c5f0b553e --- /dev/null +++ b/packages/js-drive/lib/blockExecution/BlockInfo.js @@ -0,0 +1,54 @@ +class BlockInfo { + /** + * @type {number} + */ + height; + + /** + * @type {number} + */ + epoch; + + /** + * @type {number} + */ + timeMs; + + /** + * @param {number} height + * @param {number} epoch + * @param {number} timeMs + */ + constructor(height, epoch, timeMs) { + this.height = height; + this.epoch = epoch; + this.timeMs = timeMs; + } + + /** + * @returns {RawBlockInfo} + */ + toObject() { + return { + height: this.height, + epoch: this.epoch, + timeMs: this.timeMs, + }; + } + + /** + * @param {BlockExecutionContext} blockExecutionContext + * @returns {BlockInfo} + */ + static createFromBlockExecutionContext(blockExecutionContext) { + const epochInfo = blockExecutionContext.getEpochInfo(); + + return new BlockInfo( + blockExecutionContext.getHeight().toNumber(), + epochInfo.currentEpochIndex, + blockExecutionContext.getTimeMs(), + ); + } +} + +module.exports = BlockInfo; diff --git a/packages/js-drive/lib/blockExecution/errors/BlockExecutionContextNotFoundError.js b/packages/js-drive/lib/blockExecution/errors/BlockExecutionContextNotFoundError.js new file mode 100644 index 00000000000..6c7a0d13698 --- /dev/null +++ b/packages/js-drive/lib/blockExecution/errors/BlockExecutionContextNotFoundError.js @@ -0,0 +1,23 @@ +const DriveError = require('../../errors/DriveError'); + +class BlockExecutionContextNotFoundError extends DriveError { + /** + * + * @param {number} round + */ + constructor(round) { + super(`BlockExecutionContext for round ${round} not found`); + + this.round = round; + } + + /** + * + * @return {number} + */ + getRound() { + return this.round; + } +} + +module.exports = BlockExecutionContextNotFoundError; diff --git a/packages/js-drive/lib/blockExecution/errors/ContextsAreMoreThanStackMaxSizeError.js b/packages/js-drive/lib/blockExecution/errors/ContextsAreMoreThanStackMaxSizeError.js new file mode 100644 index 00000000000..a5594fd4e5b --- /dev/null +++ b/packages/js-drive/lib/blockExecution/errors/ContextsAreMoreThanStackMaxSizeError.js @@ -0,0 +1,9 @@ +const DriveError = require('../../errors/DriveError'); + +class ContextsAreMoreThanStackMaxSizeError extends DriveError { + constructor() { + super('Number of contexts is more than stack max size'); + } +} + +module.exports = ContextsAreMoreThanStackMaxSizeError; diff --git a/packages/js-drive/lib/core/LatestCoreChainLock.js b/packages/js-drive/lib/core/LatestCoreChainLock.js new file mode 100644 index 00000000000..1dee1a2a642 --- /dev/null +++ b/packages/js-drive/lib/core/LatestCoreChainLock.js @@ -0,0 +1,42 @@ +const EventEmitter = require('events'); + +class LatestCoreChainLock extends EventEmitter { + /** + * + * @param {ChainLock} [chainLock] + */ + constructor(chainLock = undefined) { + super(); + + this.chainLock = chainLock; + } + + /** + * Update latest chainlock + * + * @param {ChainLock} chainLock + * @return {LatestCoreChainLock} + */ + update(chainLock) { + this.chainLock = chainLock; + + this.emit(LatestCoreChainLock.EVENTS.update, this.chainLock); + this.emit(`${LatestCoreChainLock.EVENTS.update}:${this.chainLock.height}`, this.chainLock); + + return this; + } + + /** + * + * @return {ChainLock} + */ + getChainLock() { + return this.chainLock; + } +} + +LatestCoreChainLock.EVENTS = { + update: 'update', +}; + +module.exports = LatestCoreChainLock; diff --git a/packages/js-drive/lib/core/SimplifiedMasternodeList.js b/packages/js-drive/lib/core/SimplifiedMasternodeList.js new file mode 100644 index 00000000000..804d55707b8 --- /dev/null +++ b/packages/js-drive/lib/core/SimplifiedMasternodeList.js @@ -0,0 +1,47 @@ +const SimplifiedMNListStore = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListStore'); + +class SimplifiedMasternodeList { + constructor(options) { + this.options = { + maxListsLimit: options.smlMaxListsLimit, + }; + + this.store = undefined; + } + + /** + * @param {SimplifiedMNListDiff[]} smlDiffs + * + * @return SimplifiedMasternodeList + */ + applyDiffs(smlDiffs) { + if (!this.store) { + this.store = new SimplifiedMNListStore([...smlDiffs], this.options); + } else { + smlDiffs.forEach((diff) => { + this.store.addDiff(diff); + }); + } + + return this; + } + + /** + * + * @return {SimplifiedMNListStore|undefined} + */ + getStore() { + return this.store; + } + + /** + * Reset the SML store + * + * @return {void} + */ + reset() { + this.store = undefined; + } +} + +module.exports = SimplifiedMasternodeList; diff --git a/packages/js-drive/lib/core/ZmqClient.js b/packages/js-drive/lib/core/ZmqClient.js new file mode 100644 index 00000000000..e3450af613d --- /dev/null +++ b/packages/js-drive/lib/core/ZmqClient.js @@ -0,0 +1,129 @@ +const { EventEmitter } = require('events'); +const zeromq = require('zeromq'); + +const ZMQ_TOPICS = { + hashtx: 'hashtx', + hashtxlock: 'hashtxlock', + hashblock: 'hashblock', + rawblock: 'rawblock', + rawtx: 'rawtx', + rawtxlock: 'rawtxlock', + rawtxlocksig: 'rawtxlocksig', + rawchainlock: 'rawchainlock', + rawchainlocksig: 'rawchainlocksig', +}; + +const defaultOptions = { topics: ZMQ_TOPICS, maxRetryCount: 20 }; + +class ZmqClient extends EventEmitter { + constructor(host, port, options = defaultOptions) { + super(); + this.subscriberSocket = zeromq.socket('sub'); + this.connectionString = `tcp://${host}:${port}`; + this.topics = options.topics || []; + this.maxRetryCount = options.maxRetryCount; + this.isConnected = false; + this.resetConnectionFailuresCount(); + } + + resetConnectionFailuresCount() { + this.connectionFailuresCount = 0; + } + + /** + * Starts listening to zmq messages + * @returns {Promise} + */ + start() { + return new Promise((resolve) => { + this.subscriberSocket.once('connect', () => resolve()); + this.subscriberSocket.once('connect', () => { + this.emit(ZmqClient.events.CONNECTED); + }); + this.subscriberSocket.on('connect', () => { + this.resetConnectionFailuresCount(); + }); + + this.initErrorHandlers(); + this.initMessageHandlers(); + this.startMonitor(); + this.subscriberSocket.connect(this.connectionString); + this.isConnected = true; + }); + } + + /** + * @private + * Starts connection monitor to monitor connection status + */ + startMonitor() { + this.subscriberSocket.monitor(500, 0); + } + + /** + * @private + */ + incrementErrorCount() { + this.connectionFailuresCount += 1; + if (this.connectionFailuresCount >= this.maxRetryCount) { + this.emit(ZmqClient.events.MAX_RETRIES_REACHED, `Failed to connect to ZMQ after ${this.maxRetryCount} tries`); + } + } + + /** + * Init connection error handlers. Requires connection monitor to be started + */ + initErrorHandlers() { + this.subscriberSocket.on('connect_delay', () => { + this.emit(ZmqClient.events.CONNECTION_DELAY, 'Dashcore ZMQ connection delay'); + this.incrementErrorCount(); + }); + this.subscriberSocket.on('disconnect', () => { + this.emit(ZmqClient.events.DISCONNECTED, 'Dashcore ZMQ connection is lost'); + this.incrementErrorCount(); + }); + this.subscriberSocket.on('monitor_error', (error) => { + this.emit(ZmqClient.events.MONITOR_ERROR, error); + this.incrementErrorCount(); + setTimeout(() => this.startMonitor(), 1000); + }); + } + + /** + * Subscribes to zmq messages + */ + initMessageHandlers() { + Object.keys(this.topics).forEach((key) => this.subscriberSocket.subscribe(this.topics[key])); + this.subscriberSocket.on('message', this.emit.bind(this)); + } + + subscribe(topicName, callback) { + const isAlreadySubscribed = Object.keys(this.topics).includes(topicName); + + if (!this.isConnected) { + throw new Error('Socket not connected. Wait until .start() resolves'); + } + + if (!isAlreadySubscribed) { + this.topics[topicName] = topicName; + this.subscriberSocket.subscribe(topicName); + } + + if (callback) { + this.on(topicName, callback); + } + } +} + +ZmqClient.events = { + CONNECTION_DELAY: 'CONNECTION_DELAY', + DISCONNECTED: 'DISCONNECTED', + MONITOR_ERROR: 'MONITOR_ERROR', + ERROR: 'ERROR', + MAX_RETRIES_REACHED: 'MAX_RETRIES_REACHED', + CONNECTED: 'CONNECTED', +}; + +ZmqClient.TOPICS = ZMQ_TOPICS; + +module.exports = ZmqClient; diff --git a/packages/js-drive/lib/core/ensureBlock.js b/packages/js-drive/lib/core/ensureBlock.js new file mode 100644 index 00000000000..4b3091dcde1 --- /dev/null +++ b/packages/js-drive/lib/core/ensureBlock.js @@ -0,0 +1,35 @@ +const ZMQClient = require('./ZmqClient'); + +/** + * + * @param {ZMQClient} zmqClient + * @param {RpcClient} rpcClient + * @param {string} hash + * @return {Promise} + */ +async function ensureBlock(zmqClient, rpcClient, hash) { + const eventPromise = new Promise((resolve) => { + const onHashBlock = (response) => { + if (hash.toString('hex') === response.toString('hex')) { + zmqClient.removeListener(ZMQClient.TOPICS.hashblock, onHashBlock); + + resolve(response); + } + }; + + zmqClient.on(ZMQClient.TOPICS.hashblock, onHashBlock); + }); + + try { + await rpcClient.getBlock(hash.toString('hex')); + } catch (e) { + // Block not found + if (e.code === -5) { + await eventPromise; + } else { + throw e; + } + } +} + +module.exports = ensureBlock; diff --git a/packages/js-drive/lib/core/errors/MissingChainLockError.js b/packages/js-drive/lib/core/errors/MissingChainLockError.js new file mode 100644 index 00000000000..ab3a2960e45 --- /dev/null +++ b/packages/js-drive/lib/core/errors/MissingChainLockError.js @@ -0,0 +1,11 @@ +class MissingChainLockError extends Error { + constructor() { + super('ChainLock is required to obtain SML'); + + this.name = this.constructor.name; + + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = MissingChainLockError; diff --git a/packages/js-drive/lib/core/errors/NotEnoughBlocksForValidSMLError.js b/packages/js-drive/lib/core/errors/NotEnoughBlocksForValidSMLError.js new file mode 100644 index 00000000000..9021506a9ef --- /dev/null +++ b/packages/js-drive/lib/core/errors/NotEnoughBlocksForValidSMLError.js @@ -0,0 +1,23 @@ +const DriveError = require('../../errors/DriveError'); + +class NotEnoughBlocksForValidSMLError extends DriveError { + /** + * @param {number} blockHeight + */ + constructor(blockHeight) { + super(`${blockHeight} blocks are not enough to obtain comprehensive SML. Needs 16 block diffs minimum`); + + this.blockHeight = blockHeight; + } + + /** + * Get block height + * + * @return {number} + */ + getBlockHeight() { + return this.blockHeight; + } +} + +module.exports = NotEnoughBlocksForValidSMLError; diff --git a/packages/js-drive/lib/core/errors/QuorumsNotFoundError.js b/packages/js-drive/lib/core/errors/QuorumsNotFoundError.js new file mode 100644 index 00000000000..6b3e12d0ead --- /dev/null +++ b/packages/js-drive/lib/core/errors/QuorumsNotFoundError.js @@ -0,0 +1,31 @@ +const DriveError = require('../../errors/DriveError'); + +class QuorumsNotFoundError extends DriveError { + /** + * @param {SimplifiedMNList} simplifiedMNList + * @param {number} quorumType + */ + constructor(simplifiedMNList, quorumType) { + let message; + if (simplifiedMNList.quorumList.length === 0) { + message = `SML at block ${simplifiedMNList.blockHash} contains no quorums of any type`; + } else { + const otherQuorumTypes = [...new Set(simplifiedMNList.quorumList.map((quorumEntry) => quorumEntry.llmqType))].join(','); + message = `SML at block ${simplifiedMNList.blockHash} contains no quorums of type ${quorumType}, but contains entries for types ${otherQuorumTypes}. Please check the Drive configuration`; + } + super(message); + + this.simplifiedMNList = simplifiedMNList; + } + + /** + * Get block height + * + * @return {SimplifiedMNList} + */ + getSimplifiedMNList() { + return this.simplifiedMNList; + } +} + +module.exports = QuorumsNotFoundError; diff --git a/packages/js-drive/lib/core/fetchQuorumMembersFactory.js b/packages/js-drive/lib/core/fetchQuorumMembersFactory.js new file mode 100644 index 00000000000..bd175746c5d --- /dev/null +++ b/packages/js-drive/lib/core/fetchQuorumMembersFactory.js @@ -0,0 +1,38 @@ +/** + * @param {RpcClient} coreRpcClient + * @return {fetchQuorumMembers} + */ +function fetchQuorumMembersFactory(coreRpcClient) { + /** + * @typedef {fetchQuorumMembers} + * @param {number} quorumType + * @param {string} quorumHash + * @return {Promise} + */ + async function fetchQuorumMembers(quorumType, quorumHash) { + try { + const { + result: { + members: validators, + }, + } = await coreRpcClient.quorum( + 'info', + quorumType, + quorumHash, + ); + + return validators; + } catch (e) { + // RPC_INVALID_PARAMETER: quorum not found + if (e.code === -8) { + throw new Error(`The quorum of type ${quorumType} and quorumHash ${quorumHash} doesn't exist`); + } + + throw e; + } + } + + return fetchQuorumMembers; +} + +module.exports = fetchQuorumMembersFactory; diff --git a/packages/js-drive/lib/core/fetchSimplifiedMNListFactory.js b/packages/js-drive/lib/core/fetchSimplifiedMNListFactory.js new file mode 100644 index 00000000000..52d9e360a8f --- /dev/null +++ b/packages/js-drive/lib/core/fetchSimplifiedMNListFactory.js @@ -0,0 +1,23 @@ +const { SimplifiedMNList } = require('@dashevo/dashcore-lib'); + +/** + * @param {RpcClient} coreRpcClient + * @returns {fetchSimplifiedMNList} + */ +function fetchSimplifiedMNListFactory(coreRpcClient) { + /** + * @typedef fetchSimplifiedMNList + * @param {number} fromBlockHeight + * @param {number} toBlockHeight + * @returns {Promise} + */ + async function fetchSimplifiedMNList(fromBlockHeight, toBlockHeight) { + const { result: rawDiff } = await coreRpcClient.protx('diff', fromBlockHeight, toBlockHeight, true); + + return new SimplifiedMNList(rawDiff); + } + + return fetchSimplifiedMNList; +} + +module.exports = fetchSimplifiedMNListFactory; diff --git a/packages/js-drive/lib/core/fetchTransactionFactory.js b/packages/js-drive/lib/core/fetchTransactionFactory.js new file mode 100644 index 00000000000..2a770150429 --- /dev/null +++ b/packages/js-drive/lib/core/fetchTransactionFactory.js @@ -0,0 +1,33 @@ +const { Transaction } = require('@dashevo/dashcore-lib'); + +/** + * @param {RpcClient} coreRpcClient + * @returns {fetchTransaction} + */ +function fetchTransactionFactory(coreRpcClient) { + /** + * @typedef {fetchTransaction} + * @param {string} id + * @returns {Transaction} + */ + async function fetchTransaction(id) { + let rawTransaction; + + try { + ({ result: rawTransaction } = await coreRpcClient.getRawTransaction(id, 1)); + } catch (e) { + // Invalid address or key error + if (e.code === -5) { + return null; + } + + throw e; + } + + return new Transaction(rawTransaction.hex); + } + + return fetchTransaction; +} + +module.exports = fetchTransactionFactory; diff --git a/packages/js-drive/lib/core/getRandomQuorumFactory.js b/packages/js-drive/lib/core/getRandomQuorumFactory.js new file mode 100644 index 00000000000..94af18f98eb --- /dev/null +++ b/packages/js-drive/lib/core/getRandomQuorumFactory.js @@ -0,0 +1,113 @@ +const BufferWriter = require('@dashevo/dashcore-lib/lib/encoding/bufferwriter'); +const Hash = require('@dashevo/dashcore-lib/lib/crypto/hash'); +const { LLMQ_TYPES } = require('@dashevo/dashcore-lib/lib/constants'); +const QuorumsNotFoundError = require('./errors/QuorumsNotFoundError'); +const ValidatorSet = require('../validator/ValidatorSet'); + +const MIN_QUORUM_VALID_MEMBERS = 90; + +const LLMQ_TYPE_TO_NAME = Object + .fromEntries(Object + .entries(LLMQ_TYPES) + .map(([key, value]) => [value, key.toLowerCase().replace('type_', '')])); + +/** + * Calculates scores for validator quorum selection + * it calculates sha256(hash, modifier) per quorumHash + * Please note that this is not a double-sha256 but a single-sha256 + * + * @param {Buffer[]} quorumHashes + * @param {Buffer} modifier + * @return {Object[]} scores + */ +function calculateQuorumHashScores(quorumHashes, modifier) { + return quorumHashes.map((hash) => { + const bufferWriter = new BufferWriter(); + + bufferWriter.write(hash); + bufferWriter.write(modifier); + + return { score: Hash.sha256(bufferWriter.toBuffer()), hash }; + }); +} + +/** + * + * @param {RpcClient} coreRpcClient + * @return {getRandomQuorum} + */ +function getRandomQuorumFactory(coreRpcClient) { + /** + * Gets the current validator set quorum hash for a particular core height + * + * @typedef {getRandomQuorum} + * @param {SimplifiedMNList} sml + * @param {number} quorumType + * @param {Buffer} entropy - the entropy to select the quorum + * @param {number} coreHeight + * @return return {Promise} - the current validator set's quorumHash + */ + async function getRandomQuorum(sml, quorumType, entropy, coreHeight) { + const validatorQuorums = sml.getQuorumsOfType(quorumType); + + if (validatorQuorums.length === 0) { + throw new QuorumsNotFoundError(sml, quorumType); + } + + const { result: allValidatorQuorumsExtendedInfo } = await coreRpcClient.quorum('listextended', coreHeight); + + // convert to object + const validatorQuorumsInfo = allValidatorQuorumsExtendedInfo[LLMQ_TYPE_TO_NAME[quorumType]] + .reduce( + (obj, item) => ({ + ...obj, + ...item, + }), + {}, + ); + + const numberOfQuorums = validatorQuorums.length; + const minTtl = ValidatorSet.ROTATION_BLOCK_INTERVAL * 3; + const dkgInterval = 24; + + // filter quorum by the number of valid members to choose the most vital ones + let filteredValidatorQuorums = validatorQuorums + .filter( + (validatorQuorum) => validatorQuorum.validMembersCount >= MIN_QUORUM_VALID_MEMBERS, + ) + .filter((validatorQuorum) => { + const validatorQuorumInfo = validatorQuorumsInfo[validatorQuorum.quorumHash]; + + if (!validatorQuorumInfo) { + return false; + } + + const quorumRemoveHeight = validatorQuorumInfo.creationHeight + + (dkgInterval * numberOfQuorums); + const howMuchInRest = quorumRemoveHeight - coreHeight; + const quorumTtl = howMuchInRest * 2.5; + + return quorumTtl > minTtl; + }); + + if (filteredValidatorQuorums.length === 0) { + // if there is no "vital" quorums, we choose among others with default min quorum size + filteredValidatorQuorums = validatorQuorums; + } + + const validatorQuorumHashes = filteredValidatorQuorums + .map((quorum) => Buffer.from(quorum.quorumHash, 'hex')); + + const scoredHashes = calculateQuorumHashScores(validatorQuorumHashes, entropy); + + scoredHashes.sort((a, b) => Buffer.compare(a.score, b.score)); + + const quorumHash = scoredHashes[0].hash.toString('hex'); + + return sml.getQuorum(quorumType, quorumHash); + } + + return getRandomQuorum; +} + +module.exports = getRandomQuorumFactory; diff --git a/packages/js-drive/lib/core/updateSimplifiedMasternodeListFactory.js b/packages/js-drive/lib/core/updateSimplifiedMasternodeListFactory.js new file mode 100644 index 00000000000..e8f6d16073a --- /dev/null +++ b/packages/js-drive/lib/core/updateSimplifiedMasternodeListFactory.js @@ -0,0 +1,113 @@ +const SimplifiedMNListDiff = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListDiff'); + +const NotEnoughBlocksForValidSMLError = require('./errors/NotEnoughBlocksForValidSMLError'); + +/** + * Check that core is synced (factory) + * + * @param {RpcClient} coreRpcClient + * @param {SimplifiedMasternodeList} simplifiedMasternodeList + * @param {number} smlMaxListsLimit + * @param {string} network + * @param {BaseLogger} logger + * + * @returns {updateSimplifiedMasternodeList} + */ +function updateSimplifiedMasternodeListFactory( + coreRpcClient, + simplifiedMasternodeList, + smlMaxListsLimit, + network, + logger, +) { + // 1 means first block + let latestRequestedHeight = 1; + + /** + * @param {number} fromHeight + * @param {number} toHeight + * @return {Promise} + */ + async function fetchDiffsPerBlock(fromHeight, toHeight) { + const diffs = []; + + for (let height = fromHeight; height < toHeight; height += 1) { + const { result: rawDiff } = await coreRpcClient.protx('diff', height, height + 1, true); + + const diff = new SimplifiedMNListDiff(rawDiff, network); + + diffs.push(diff); + } + + return diffs; + } + + /** + * Check that core is synced + * + * @typedef updateSimplifiedMasternodeList + * @param {number} coreHeight + * @param {Object} [options] + * @param {BaseLogger} [options.logger] + * + * @returns {Promise} + */ + async function updateSimplifiedMasternodeList(coreHeight, options = {}) { + // either use a logger passed or use standard logger + const contextLogger = (options.logger || logger); + + // Should be enough to get 16 diffs + if (coreHeight < smlMaxListsLimit + 1) { + throw new NotEnoughBlocksForValidSMLError(coreHeight); + } + + // When we got more than 16 blocks of difference between last requested height + // and core height, we take only last 16 of them. + if (coreHeight - latestRequestedHeight > smlMaxListsLimit) { + latestRequestedHeight = 1; + simplifiedMasternodeList.reset(); + } + + if (latestRequestedHeight === 1) { + // Initialize SML with 16 diffs to have enough quorum information + // to be able to verify signatures + + const startHeight = coreHeight - smlMaxListsLimit; + + const { result: rawDiff } = await coreRpcClient.protx('diff', latestRequestedHeight, startHeight, true); + + const initialSmlDiffs = [ + new SimplifiedMNListDiff(rawDiff, network), + ...await fetchDiffsPerBlock(startHeight, coreHeight), + ]; + + simplifiedMasternodeList.applyDiffs(initialSmlDiffs); + + latestRequestedHeight = coreHeight; + + contextLogger.debug(`SML is initialized for core heights ${startHeight} to ${coreHeight}`); + + return true; + } + + if (latestRequestedHeight < coreHeight) { + // Update SML + + const smlDiffs = await fetchDiffsPerBlock(latestRequestedHeight, coreHeight); + + simplifiedMasternodeList.applyDiffs(smlDiffs); + + contextLogger.debug(`SML is updated for core heights ${latestRequestedHeight} to ${coreHeight}`); + + latestRequestedHeight = coreHeight; + + return true; + } + + return false; + } + + return updateSimplifiedMasternodeList; +} + +module.exports = updateSimplifiedMasternodeListFactory; diff --git a/packages/js-drive/lib/core/waitForChainLockedHeightFactory.js b/packages/js-drive/lib/core/waitForChainLockedHeightFactory.js new file mode 100644 index 00000000000..8d01c2fdfcc --- /dev/null +++ b/packages/js-drive/lib/core/waitForChainLockedHeightFactory.js @@ -0,0 +1,49 @@ +const MissingChainLockError = require('./errors/MissingChainLockError'); +const LatestCoreChainLock = require('./LatestCoreChainLock'); + +/** + * + * @param {LatestCoreChainLock} latestCoreChainLock + + * @return {waitForChainLockedHeight} + */ +function waitForChainLockedHeightFactory( + latestCoreChainLock, +) { + /** + * @typedef waitForChainLockedHeight + * @param {number} coreHeight + * + * @return {Promise} + */ + async function waitForChainLockedHeight(coreHeight) { + // ChainLock is required to get finalized SML that won't be reorged + const existingChainLock = latestCoreChainLock.getChainLock(); + + if (!existingChainLock) { + throw new MissingChainLockError(); + } + + // Wait for core to be synced up to coreHeight + if (coreHeight > existingChainLock.height) { + await new Promise((resolve) => { + const listener = (chainLock) => { + // Skip if core height still not reached + if (coreHeight > chainLock.height) { + return; + } + + latestCoreChainLock.removeListener(LatestCoreChainLock.EVENTS.update, listener); + + resolve(); + }; + + latestCoreChainLock.on(LatestCoreChainLock.EVENTS.update, listener); + }); + } + } + + return waitForChainLockedHeight; +} + +module.exports = waitForChainLockedHeightFactory; diff --git a/packages/js-drive/lib/core/waitForCoreChainLockSyncFactory.js b/packages/js-drive/lib/core/waitForCoreChainLockSyncFactory.js new file mode 100644 index 00000000000..d2f05f26760 --- /dev/null +++ b/packages/js-drive/lib/core/waitForCoreChainLockSyncFactory.js @@ -0,0 +1,103 @@ +const { ChainLock } = require('@dashevo/dashcore-lib'); + +const ChainLockSigMessage = require('@dashevo/dashcore-lib/lib/zmqMessages/ChainLockSigMessage'); +const ZMQClient = require('./ZmqClient'); + +const ensureBlock = require('./ensureBlock'); + +/** + * Wait and ensure that core chain lock stays synced (factory) + * + * @param {ZMQClient} coreZMQClient + * @param {RpcClient} coreRpcClient + * @param {LatestCoreChainLock} latestCoreChainLock + * @param {BaseLogger} logger + * + * @returns {waitForCoreSync} + */ +function waitForCoreChainLockSyncFactory( + coreZMQClient, + coreRpcClient, + latestCoreChainLock, + logger, +) { + /** + * Wait and ensure that core chain lock stays synced. + * On new ChainLock received, will also ensure that its block has been processed. + * + * @typedef waitForCoreChainLockSync + * + * @returns {Promise} + */ + async function waitForCoreChainLockSync() { + coreZMQClient.subscribe(ZMQClient.TOPICS.rawchainlocksig); + + let resolveFirstChainLockFromZMQPromise; + const firstChainLockFromZMQPromise = new Promise((resolve) => { + resolveFirstChainLockFromZMQPromise = resolve; + }); + + coreZMQClient.on(ZMQClient.TOPICS.rawchainlocksig, async (rawChainLockMessage) => { + let chainLock; + + try { + ({ chainLock } = new ChainLockSigMessage(rawChainLockMessage)); + } catch (e) { + logger.error( + { + err: e, + rawChainLockMessage: rawChainLockMessage.toString('hex'), + }, + 'Error on creating ChainLockSigMessage', + ); + + return; + } + + latestCoreChainLock.update(chainLock); + + logger.trace( + { + chainLock, + }, + `Updated latestCoreChainLock for core height ${chainLock.height}`, + ); + + if (resolveFirstChainLockFromZMQPromise) { + resolveFirstChainLockFromZMQPromise(); + resolveFirstChainLockFromZMQPromise = null; + } + }); + + // Because a ChainLock may happen before its block, we also subscribe to rawblock + coreZMQClient.subscribe(ZMQClient.TOPICS.hashblock); + + // We need to retrieve latest ChainLock from our fully synced Core instance + let rpcBestChainLockResponse; + try { + rpcBestChainLockResponse = await coreRpcClient.getBestChainLock(); + } catch (e) { + // Unable to find any ChainLock + if (e.code === -32603) { + logger.debug('There are no ChainLocks currently. Waiting for the first one...'); + + // We need to wait for a new ChainLock from ZMQ socket + await firstChainLockFromZMQPromise; + } else { + throw e; + } + } + + if (rpcBestChainLockResponse) { + const chainLock = new ChainLock(rpcBestChainLockResponse.result); + + await ensureBlock(coreZMQClient, coreRpcClient, chainLock.blockHash); + + latestCoreChainLock.update(chainLock); + } + } + + return waitForCoreChainLockSync; +} + +module.exports = waitForCoreChainLockSyncFactory; diff --git a/packages/js-drive/lib/core/waitForCoreSyncFactory.js b/packages/js-drive/lib/core/waitForCoreSyncFactory.js new file mode 100644 index 00000000000..9a4b612101a --- /dev/null +++ b/packages/js-drive/lib/core/waitForCoreSyncFactory.js @@ -0,0 +1,47 @@ +const wait = require('../util/wait'); + +/** + * Check that core is synced (factory) + * + * @param {RpcClient} coreRpcClient + * + * @returns {waitForCoreSync} + */ +function waitForCoreSyncFactory(coreRpcClient) { + /** + * Check that core is synced + * + * @typedef waitForCoreSync + * + * @param {function(number, number)} progressCallback + * + * @returns {Promise} + */ + async function waitForCoreSync(progressCallback) { + let isBlockchainSynced = false; + while (!isBlockchainSynced) { + ({ + result: { + IsBlockchainSynced: isBlockchainSynced, + }, + } = await coreRpcClient.mnsync('status')); + + if (!isBlockchainSynced) { + const { + result: { + blocks: currentBlockHeight, + headers: currentHeadersNumber, + }, + } = await coreRpcClient.getBlockchainInfo(); + + progressCallback(currentBlockHeight, currentHeadersNumber); + + await wait(10000); + } + } + } + + return waitForCoreSync; +} + +module.exports = waitForCoreSyncFactory; diff --git a/packages/js-drive/lib/createDIContainer.js b/packages/js-drive/lib/createDIContainer.js new file mode 100644 index 00000000000..d87029bb822 --- /dev/null +++ b/packages/js-drive/lib/createDIContainer.js @@ -0,0 +1,894 @@ +const { + createContainer: createAwilixContainer, + InjectionMode, + asClass, + asFunction, + asValue, +} = require('awilix'); + +const fs = require('fs'); + +const { AsyncLocalStorage } = require('node:async_hooks'); + +const Long = require('long'); + +const RSDrive = require('@dashevo/rs-drive'); + +const RpcClient = require('@dashevo/dashd-rpc/promise'); + +const { PublicKey } = require('@dashevo/dashcore-lib'); + +const DashPlatformProtocol = require('@dashevo/dpp'); + +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); + +const findMyWay = require('find-my-way'); + +const pino = require('pino'); +const pinoMultistream = require('pino-multi-stream'); + +const createABCIServer = require('@dashevo/abci'); + +const protocolVersion = require('@dashevo/dpp/lib/version/protocolVersion'); + +const calculateOperationFees = require('@dashevo/dpp/lib/stateTransition/fee/calculateOperationFees'); +const calculateStateTransitionFeeFactory = require('@dashevo/dpp/lib/stateTransition/fee/calculateStateTransitionFeeFactory'); + +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); +const featureFlagsSystemIds = require('@dashevo/feature-flags-contract/lib/systemIds'); + +const featureFlagsDocuments = require('@dashevo/feature-flags-contract/schema/feature-flags-documents.json'); +const dpnsSystemIds = require('@dashevo/dpns-contract/lib/systemIds'); + +const dpnsDocuments = require('@dashevo/dpns-contract/schema/dpns-contract-documents.json'); +const masternodeRewardsSystemIds = require('@dashevo/masternode-reward-shares-contract/lib/systemIds'); + +const masternodeRewardsDocuments = require('@dashevo/masternode-reward-shares-contract/schema/masternode-reward-shares-documents.json'); +const dashpaySystemIds = require('@dashevo/dashpay-contract/lib/systemIds'); + +const dashpayDocuments = require('@dashevo/dashpay-contract/schema/dashpay.schema.json'); + +const withdrawalsSystemIds = require('@dashevo/withdrawals-contract/lib/systemIds'); +const withdrawalsDocuments = require('@dashevo/withdrawals-contract/schema/withdrawals-documents.json'); +const calculateStateTransitionFeeFromOperationsFactory = require('@dashevo/dpp/lib/stateTransition/fee/calculateStateTransitionFeeFromOperationsFactory'); + +const packageJSON = require('../package.json'); + +const ZMQClient = require('./core/ZmqClient'); + +const sanitizeUrl = require('./util/sanitizeUrl'); +const LatestCoreChainLock = require('./core/LatestCoreChainLock'); + +const GroveDBStore = require('./storage/GroveDBStore'); + +const IdentityStoreRepository = require('./identity/IdentityStoreRepository'); + +const IdentityPublicKeyStoreRepository = require( + './identity/IdentityPublicKeyStoreRepository', +); +const DataContractStoreRepository = require('./dataContract/DataContractStoreRepository'); +const fetchDocumentsFactory = require('./document/fetchDocumentsFactory'); +const proveDocumentsFactory = require('./document/proveDocumentsFactory'); + +const fetchDataContractFactory = require('./document/fetchDataContractFactory'); +const BlockExecutionContext = require('./blockExecution/BlockExecutionContext'); + +const unserializeStateTransitionFactory = require( + './abci/handlers/stateTransition/unserializeStateTransitionFactory', +); +const DriveStateRepository = require('./dpp/DriveStateRepository'); +const CachedStateRepositoryDecorator = require('./dpp/CachedStateRepositoryDecorator'); +const LoggedStateRepositoryDecorator = require('./dpp/LoggedStateRepositoryDecorator'); +const dataContractQueryHandlerFactory = require('./abci/handlers/query/dataContractQueryHandlerFactory'); +const identityQueryHandlerFactory = require('./abci/handlers/query/identityQueryHandlerFactory'); + +const documentQueryHandlerFactory = require('./abci/handlers/query/documentQueryHandlerFactory'); + +const identitiesByPublicKeyHashesQueryHandlerFactory = require('./abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory'); +const getProofsQueryHandlerFactory = require('./abci/handlers/query/getProofsQueryHandlerFactory'); +const wrapInErrorHandlerFactory = require('./abci/errors/wrapInErrorHandlerFactory'); +const errorHandlerFactory = require('./errorHandlerFactory'); +const checkTxHandlerFactory = require('./abci/handlers/checkTxHandlerFactory'); +const initChainHandlerFactory = require('./abci/handlers/initChainHandlerFactory'); +const infoHandlerFactory = require('./abci/handlers/infoHandlerFactory'); +const extendVoteHandlerFactory = require('./abci/handlers/extendVoteHandlerFactory'); +const finalizeBlockHandlerFactory = require('./abci/handlers/finalizeBlockHandlerFactory'); +const prepareProposalHandlerFactory = require('./abci/handlers/prepareProposalHandlerFactory'); + +const processProposalHandlerFactory = require('./abci/handlers/processProposalHandlerFactory'); +const verifyVoteExtensionHandlerFactory = require('./abci/handlers/verifyVoteExtensionHandlerFactory'); +const beginBlockFactory = require('./abci/handlers/proposal/beginBlockFactory'); +const deliverTxFactory = require('./abci/handlers/proposal/deliverTxFactory'); +const endBlockFactory = require('./abci/handlers/proposal/endBlockFactory'); +const rotateAndCreateValidatorSetUpdateFactory = require('./abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory'); +const createConsensusParamUpdateFactory = require('./abci/handlers/proposal/createConsensusParamUpdateFactory'); + +const createCoreChainLockUpdateFactory = require('./abci/handlers/proposal/createCoreChainLockUpdateFactory'); +const verifyChainLockFactory = require('./abci/handlers/proposal/verifyChainLockFactory'); +const queryHandlerFactory = require('./abci/handlers/queryHandlerFactory'); +const waitForCoreSyncFactory = require('./core/waitForCoreSyncFactory'); +const waitForCoreChainLockSyncFactory = require('./core/waitForCoreChainLockSyncFactory'); +const updateSimplifiedMasternodeListFactory = require('./core/updateSimplifiedMasternodeListFactory'); + +const waitForChainLockedHeightFactory = require('./core/waitForChainLockedHeightFactory'); +const SimplifiedMasternodeList = require('./core/SimplifiedMasternodeList'); +const SpentAssetLockTransactionsRepository = require('./identity/SpentAssetLockTransactionsRepository'); +const enrichErrorWithConsensusErrorFactory = require('./abci/errors/enrichErrorWithContextLoggerFactory'); +const closeAbciServerFactory = require('./abci/closeAbciServerFactory'); +const getLatestFeatureFlagFactory = require('./featureFlag/getLatestFeatureFlagFactory'); +const getFeatureFlagForHeightFactory = require('./featureFlag/getFeatureFlagForHeightFactory'); +const ValidatorSet = require('./validator/ValidatorSet'); +const createValidatorSetUpdate = require('./abci/handlers/validator/createValidatorSetUpdate'); +const fetchQuorumMembersFactory = require('./core/fetchQuorumMembersFactory'); +const getRandomQuorumFactory = require('./core/getRandomQuorumFactory'); + +const createQueryResponseFactory = require('./abci/handlers/query/response/createQueryResponseFactory'); +const BlockExecutionContextRepository = require('./blockExecution/BlockExecutionContextRepository'); +const synchronizeMasternodeIdentitiesFactory = require('./identity/masternode/synchronizeMasternodeIdentitiesFactory'); +const createMasternodeIdentityFactory = require('./identity/masternode/createMasternodeIdentityFactory'); +const handleNewMasternodeFactory = require('./identity/masternode/handleNewMasternodeFactory'); +const handleUpdatedPubKeyOperatorFactory = require('./identity/masternode/handleUpdatedPubKeyOperatorFactory'); +const handleUpdatedVotingAddressFactory = require('./identity/masternode/handleUpdatedVotingAddressFactory'); +const createRewardShareDocumentFactory = require('./identity/masternode/createRewardShareDocumentFactory'); +const handleRemovedMasternodeFactory = require('./identity/masternode/handleRemovedMasternodeFactory'); +const handleUpdatedScriptPayoutFactory = require('./identity/masternode/handleUpdatedScriptPayoutFactory'); + +const getWithdrawPubKeyTypeFromPayoutScriptFactory = require('./identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory'); +const getPublicKeyFromPayoutScript = require('./identity/masternode/getPublicKeyFromPayoutScript'); +const updateWithdrawalTransactionIdAndStatusFactory = require('./identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory'); +const broadcastWithdrawalTransactionsFactory = require('./abci/handlers/proposal/broadcastWithdrawalTransactionsFactory'); + +const DocumentRepository = require('./document/DocumentRepository'); +const ExecutionTimer = require('./util/ExecutionTimer'); +const noopLoggerInstance = require('./util/noopLogger'); +const fetchTransactionFactory = require('./core/fetchTransactionFactory'); +const LastSyncedCoreHeightRepository = require('./identity/masternode/LastSyncedCoreHeightRepository'); +const fetchSimplifiedMNListFactory = require('./core/fetchSimplifiedMNListFactory'); +const processProposalFactory = require('./abci/handlers/proposal/processProposalFactory'); +const createContextLoggerFactory = require('./abci/errors/createContextLoggerFactory'); +const IdentityBalanceStoreRepository = require('./identity/IdentityBalanceStoreRepository'); + +/** + * + * @param {Object} options + * @param {string} options.ABCI_HOST + * @param {string} options.ABCI_PORT + * @param {string} options.DB_PATH + * @param {string} options.GROVEDB_LATEST_FILE + * @param {string} options.DATA_CONTRACTS_GLOBAL_CACHE_SIZE + * @param {string} options.DATA_CONTRACTS_BLOCK_CACHE_SIZE + * @param {string} options.CORE_JSON_RPC_HOST + * @param {string} options.CORE_JSON_RPC_PORT + * @param {string} options.CORE_JSON_RPC_USERNAME + * @param {string} options.CORE_JSON_RPC_PASSWORD + * @param {string} options.CORE_ZMQ_HOST + * @param {string} options.CORE_ZMQ_PORT + * @param {string} options.CORE_ZMQ_CONNECTION_RETRIES + * @param {string} options.NETWORK + * @param {string} options.DPNS_MASTER_PUBLIC_KEY + * @param {string} options.DPNS_SECOND_PUBLIC_KEY + * @param {string} options.DASHPAY_MASTER_PUBLIC_KEY + * @param {string} options.DASHPAY_SECOND_PUBLIC_KEY + * @param {string} options.FEATURE_FLAGS_MASTER_PUBLIC_KEY + * @param {string} options.FEATURE_FLAGS_SECOND_PUBLIC_KEY + * @param {string} options.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY + * @param {string} options.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY + * @param {string} options.WITHDRAWALS_MASTER_PUBLIC_KEY + * @param {string} options.WITHDRAWALS_SECOND_PUBLIC_KEY + * @param {string} options.INITIAL_CORE_CHAINLOCKED_HEIGHT + * @param {string} options.VALIDATOR_SET_LLMQ_TYPE + * @param {string} options.TENDERDASH_P2P_PORT + * @param {string} options.LOG_STDOUT_LEVEL + * @param {string} options.LOG_PRETTY_FILE_LEVEL + * @param {string} options.LOG_PRETTY_FILE_PATH + * @param {string} options.LOG_JSON_FILE_LEVEL + * @param {string} options.LOG_JSON_FILE_PATH + * @param {string} options.LOG_STATE_REPOSITORY + * @param {string} options.NODE_ENV + * + * @return {AwilixContainer} + */ +function createDIContainer(options) { + if (!options.DPNS_MASTER_PUBLIC_KEY) { + throw new Error('DPNS_MASTER_PUBLIC_KEY must be set'); + } + if (!options.DPNS_SECOND_PUBLIC_KEY) { + throw new Error('DPNS_SECOND_PUBLIC_KEY must be set'); + } + + if (!options.DASHPAY_MASTER_PUBLIC_KEY) { + throw new Error('DASHPAY_MASTER_PUBLIC_KEY must be set'); + } + + if (!options.DASHPAY_SECOND_PUBLIC_KEY) { + throw new Error('DASHPAY_SECOND_PUBLIC_KEY must be set'); + } + + if (!options.FEATURE_FLAGS_MASTER_PUBLIC_KEY) { + throw new Error('FEATURE_FLAGS_MASTER_PUBLIC_KEY must be set'); + } + + if (!options.FEATURE_FLAGS_SECOND_PUBLIC_KEY) { + throw new Error('FEATURE_FLAGS_SECOND_PUBLIC_KEY must be set'); + } + + if (!options.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY) { + throw new Error('MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY must be set'); + } + + if (!options.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY) { + throw new Error('MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY must be set'); + } + + if (!options.WITHDRAWALS_MASTER_PUBLIC_KEY) { + throw new Error('WITHDRAWALS_MASTER_PUBLIC_KEY must be set'); + } + + if (!options.WITHDRAWALS_SECOND_PUBLIC_KEY) { + throw new Error('WITHDRAWALS_SECOND_PUBLIC_KEY must be set'); + } + + const container = createAwilixContainer({ + injectionMode: InjectionMode.CLASSIC, + }); + + /** + * Register itself (usually to solve recursive dependencies) + */ + container.register({ + container: asValue(container), + }); + + /** + * Register latest protocol version + * Define highest supported protocol version + */ + container.register({ + latestProtocolVersion: asValue(Long.fromInt(protocolVersion.latestVersion)), + }); + + /** + * Register environment variables + */ + container.register({ + abciHost: asValue(options.ABCI_HOST), + abciPort: asValue(options.ABCI_PORT), + + dbPath: asValue(options.DB_PATH), + + groveDBLatestFile: asValue(options.GROVEDB_LATEST_FILE), + + dataContractsGlobalCacheSize: asValue( + parseInt(options.DATA_CONTRACTS_GLOBAL_CACHE_SIZE, 10), + ), + dataContractsBlockCacheSize: asValue( + parseInt(options.DATA_CONTRACTS_BLOCK_CACHE_SIZE, 10), + ), + + coreJsonRpcHost: asValue(options.CORE_JSON_RPC_HOST), + coreJsonRpcPort: asValue(options.CORE_JSON_RPC_PORT), + coreJsonRpcUsername: asValue(options.CORE_JSON_RPC_USERNAME), + coreJsonRpcPassword: asValue(options.CORE_JSON_RPC_PASSWORD), + coreZMQHost: asValue(options.CORE_ZMQ_HOST), + coreZMQPort: asValue(options.CORE_ZMQ_PORT), + coreZMQConnectionRetries: asValue( + parseInt(options.CORE_ZMQ_CONNECTION_RETRIES, 10), + ), + network: asValue(options.NETWORK), + logStdoutLevel: asValue(options.LOG_STDOUT_LEVEL), + logPrettyFileLevel: asValue(options.LOG_PRETTY_FILE_LEVEL), + logPrettyFilePath: asValue(options.LOG_PRETTY_FILE_PATH), + logJsonFileLevel: asValue(options.LOG_JSON_FILE_LEVEL), + logJsonFilePath: asValue(options.LOG_JSON_FILE_PATH), + logStateRepository: asValue(options.LOG_STATE_REPOSITORY === 'true'), + isProductionEnvironment: asValue(options.NODE_ENV === 'production'), + maxIdentitiesPerRequest: asValue(25), + smlMaxListsLimit: asValue(16), + initialCoreChainLockedHeight: asValue( + parseInt(options.INITIAL_CORE_CHAINLOCKED_HEIGHT, 10), + ), + validatorSetLLMQType: asValue( + parseInt(options.VALIDATOR_SET_LLMQ_TYPE, 10), + ), + masternodeRewardSharesContractId: asValue( + Identifier.from(masternodeRewardsSystemIds.contractId), + ), + masternodeRewardSharesOwnerId: asValue( + Identifier.from(masternodeRewardsSystemIds.ownerId), + ), + masternodeRewardSharesOwnerMasterPublicKey: asValue( + PublicKey.fromString( + options.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY, + ), + ), + masternodeRewardSharesOwnerSecondPublicKey: asValue( + PublicKey.fromString( + options.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY, + ), + ), + masternodeRewardSharesDocuments: asValue( + masternodeRewardsDocuments, + ), + featureFlagsContractId: asValue( + Identifier.from(featureFlagsSystemIds.contractId), + ), + featureFlagsOwnerId: asValue( + Identifier.from(featureFlagsSystemIds.ownerId), + ), + featureFlagsOwnerMasterPublicKey: asValue( + PublicKey.fromString( + options.FEATURE_FLAGS_MASTER_PUBLIC_KEY, + ), + ), + featureFlagsOwnerSecondPublicKey: asValue( + PublicKey.fromString( + options.FEATURE_FLAGS_SECOND_PUBLIC_KEY, + ), + ), + featureFlagsDocuments: asValue(featureFlagsDocuments), + dpnsContractId: asValue(Identifier.from(dpnsSystemIds.contractId)), + dpnsOwnerId: asValue(Identifier.from(dpnsSystemIds.ownerId)), + dpnsOwnerMasterPublicKey: asValue( + PublicKey.fromString( + options.DPNS_MASTER_PUBLIC_KEY, + ), + ), + dpnsOwnerSecondPublicKey: asValue( + PublicKey.fromString( + options.DPNS_SECOND_PUBLIC_KEY, + ), + ), + dpnsDocuments: asValue(dpnsDocuments), + dashpayContractId: asValue(Identifier.from(dashpaySystemIds.contractId)), + dashpayOwnerId: asValue(Identifier.from(dashpaySystemIds.ownerId)), + dashpayOwnerMasterPublicKey: asValue( + PublicKey.fromString( + options.DASHPAY_MASTER_PUBLIC_KEY, + ), + ), + dashpayOwnerSecondPublicKey: asValue( + PublicKey.fromString( + options.DASHPAY_SECOND_PUBLIC_KEY, + ), + ), + dashpayDocuments: asValue(dashpayDocuments), + withdrawalsContractId: asValue(Identifier.from(withdrawalsSystemIds.contractId)), + withdrawalsOwnerId: asValue(Identifier.from(withdrawalsSystemIds.ownerId)), + withdrawalsOwnerMasterPublicKey: asValue( + PublicKey.fromString( + options.WITHDRAWALS_MASTER_PUBLIC_KEY, + ), + ), + withdrawalsOwnerSecondPublicKey: asValue( + PublicKey.fromString( + options.WITHDRAWALS_SECOND_PUBLIC_KEY, + ), + ), + withdrawalsDocuments: asValue(withdrawalsDocuments), + tenderdashP2pPort: asValue(options.TENDERDASH_P2P_PORT), + }); + + /** + * Register global DPP options + */ + container.register({ + dppOptions: asValue({}), + }); + + /** + * Register Core related + */ + container.register({ + latestCoreChainLock: asValue(new LatestCoreChainLock()), + simplifiedMasternodeList: asClass(SimplifiedMasternodeList).proxy().singleton(), + fetchQuorumMembers: asFunction(fetchQuorumMembersFactory), + fetchSimplifiedMNList: asFunction(fetchSimplifiedMNListFactory), + getRandomQuorum: asFunction(getRandomQuorumFactory), + coreZMQClient: asFunction(( + coreZMQHost, + coreZMQPort, + coreZMQConnectionRetries, + ) => ( + new ZMQClient(coreZMQHost, coreZMQPort, { + maxRetryCount: coreZMQConnectionRetries, + }) + )).singleton(), + + coreRpcClient: asFunction(( + coreJsonRpcHost, + coreJsonRpcPort, + coreJsonRpcUsername, + coreJsonRpcPassword, + ) => ( + new RpcClient({ + protocol: 'http', + host: coreJsonRpcHost, + port: coreJsonRpcPort, + user: coreJsonRpcUsername, + pass: coreJsonRpcPassword, + }) + )).singleton(), + }); + + /** + * Register common services + */ + container.register({ + loggerPrettyfierOptions: asValue({ + translateTime: true, + }), + + logStdoutStream: asFunction((loggerPrettyfierOptions) => pinoMultistream.prettyStream({ + prettyPrint: loggerPrettyfierOptions, + })).singleton(), + + logPrettyFileStream: asFunction(( + logPrettyFilePath, + loggerPrettyfierOptions, + ) => pinoMultistream.prettyStream({ + prettyPrint: loggerPrettyfierOptions, + dest: fs.createWriteStream(logPrettyFilePath, { flags: 'a' }), + })).singleton(), + + logJsonFileStream: asFunction((logJsonFilePath) => fs.createWriteStream(logJsonFilePath, { flags: 'a' })) + .disposer(async (stream) => new Promise((resolve) => stream.end(resolve))).singleton(), + + loggerStreams: asFunction(( + logStdoutLevel, + logStdoutStream, + logPrettyFileLevel, + logPrettyFileStream, + logJsonFileLevel, + logJsonFileStream, + ) => [ + { + level: logStdoutLevel, + stream: logStdoutStream, + }, + { + level: logPrettyFileLevel, + stream: logPrettyFileStream, + }, + { + level: logJsonFileLevel, + stream: logJsonFileStream, + }, + ]), + + logger: asFunction( + (loggerStreams) => pino({ + level: 'trace', + }, pinoMultistream.multistream(loggerStreams)) + .child({ driveVersion: packageJSON.version }), + ).singleton(), + + noopLogger: asValue(noopLoggerInstance), + + sanitizeUrl: asValue(sanitizeUrl), + + executionTimer: asClass(ExecutionTimer).singleton(), + }); + + /** + * RS Drive and GroveDB + */ + + container.register({ + rsDrive: asFunction(( + groveDBLatestFile, + dataContractsGlobalCacheSize, + dataContractsBlockCacheSize, + coreJsonRpcHost, + coreJsonRpcPort, + coreJsonRpcUsername, + coreJsonRpcPassword, + ) => new RSDrive(groveDBLatestFile, { + drive: { + dataContractsGlobalCacheSize, + dataContractsBlockCacheSize, + }, + core: { + rpc: { + url: `${coreJsonRpcHost}:${coreJsonRpcPort}`, + username: coreJsonRpcUsername, + password: coreJsonRpcPassword, + }, + }, + })) + .disposer(async (rsDrive) => { + // Flush data on disk + await rsDrive.getGroveDB().flush(); + + await rsDrive.close(); + + if (process.env.NODE_ENV === 'test') { + fs.rmSync(options.GROVEDB_LATEST_FILE, { recursive: true }); + } + }).singleton(), + + groveDB: asFunction((rsDrive) => rsDrive.getGroveDB()).singleton(), + + rsAbci: asFunction((rsDrive) => rsDrive.getAbci()).singleton(), + + groveDBStore: asFunction((rsDrive) => new GroveDBStore(rsDrive)).singleton(), + }); + + /** + * Register Identity + */ + container.register({ + identityRepository: asClass(IdentityStoreRepository).singleton(), + + identityBalanceRepository: asClass(IdentityBalanceStoreRepository).singleton(), + + identityPublicKeyRepository: asClass(IdentityPublicKeyStoreRepository).singleton(), + + synchronizeMasternodeIdentities: asFunction(synchronizeMasternodeIdentitiesFactory).singleton(), + + lastSyncedCoreHeightRepository: asClass(LastSyncedCoreHeightRepository).singleton(), + + createMasternodeIdentity: asFunction(createMasternodeIdentityFactory).singleton(), + + createRewardShareDocument: asFunction(createRewardShareDocumentFactory).singleton(), + + handleNewMasternode: asFunction(handleNewMasternodeFactory).singleton(), + + handleUpdatedPubKeyOperator: asFunction(handleUpdatedPubKeyOperatorFactory).singleton(), + + handleUpdatedVotingAddress: asFunction(handleUpdatedVotingAddressFactory).singleton(), + + handleRemovedMasternode: asFunction(handleRemovedMasternodeFactory).singleton(), + + handleUpdatedScriptPayout: asFunction(handleUpdatedScriptPayoutFactory).singleton(), + + getWithdrawPubKeyTypeFromPayoutScript: asFunction(getWithdrawPubKeyTypeFromPayoutScriptFactory) + .singleton(), + + getPublicKeyFromPayoutScript: asValue(getPublicKeyFromPayoutScript), + + systemIdentityPublicKeys: asFunction(( + masternodeRewardSharesOwnerMasterPublicKey, + masternodeRewardSharesOwnerSecondPublicKey, + featureFlagsOwnerMasterPublicKey, + featureFlagsOwnerSecondPublicKey, + dpnsOwnerMasterPublicKey, + dpnsOwnerSecondPublicKey, + dashpayOwnerMasterPublicKey, + dashpayOwnerSecondPublicKey, + withdrawalsOwnerMasterPublicKey, + withdrawalsOwnerSecondPublicKey, + ) => ({ + masternodeRewardSharesContractOwner: { + master: masternodeRewardSharesOwnerMasterPublicKey.toBuffer(), + high: masternodeRewardSharesOwnerSecondPublicKey.toBuffer(), + }, + featureFlagsContractOwner: { + master: featureFlagsOwnerMasterPublicKey.toBuffer(), + high: featureFlagsOwnerSecondPublicKey.toBuffer(), + }, + dpnsContractOwner: { + master: dpnsOwnerMasterPublicKey.toBuffer(), + high: dpnsOwnerSecondPublicKey.toBuffer(), + }, + withdrawalsContractOwner: { + master: dashpayOwnerMasterPublicKey.toBuffer(), + high: dashpayOwnerSecondPublicKey.toBuffer(), + }, + dashpayContractOwner: { + master: withdrawalsOwnerMasterPublicKey.toBuffer(), + high: withdrawalsOwnerSecondPublicKey.toBuffer(), + }, + })), + }); + + /** + * Register asset lock transactions + */ + container.register({ + spentAssetLockTransactionsRepository: asClass(SpentAssetLockTransactionsRepository).singleton(), + }); + + /** + * Register Data Contract + */ + container.register({ + dataContractRepository: asFunction(( + groveDBStore, + decodeProtocolEntity, + ) => new DataContractStoreRepository(groveDBStore, decodeProtocolEntity)).singleton(), + }); + + /** + * Register Document + */ + container.register({ + documentRepository: asFunction(( + groveDBStore, + ) => new DocumentRepository(groveDBStore)).singleton(), + + fetchDocuments: asFunction(fetchDocumentsFactory).singleton(), + fetchDataContract: asFunction(fetchDataContractFactory).singleton(), + proveDocuments: asFunction(proveDocumentsFactory).singleton(), + }); + + /** + * Register block execution context + */ + container.register({ + latestBlockExecutionContext: asClass(BlockExecutionContext).singleton(), + proposalBlockExecutionContext: asClass(BlockExecutionContext).singleton(), + blockExecutionContextRepository: asClass(BlockExecutionContextRepository).singleton(), + }); + + /** + * Register DPP + */ + container.register({ + decodeProtocolEntity: asFunction(decodeProtocolEntityFactory), + + calculateOperationFees: asValue(calculateOperationFees), + + calculateStateTransitionFeeFromOperations: + asFunction(calculateStateTransitionFeeFromOperationsFactory), + + calculateStateTransitionFee: asFunction(calculateStateTransitionFeeFactory), + + stateRepository: asFunction(( + identityRepository, + identityBalanceRepository, + identityPublicKeyRepository, + dataContractRepository, + fetchDocuments, + documentRepository, + spentAssetLockTransactionsRepository, + coreRpcClient, + latestBlockExecutionContext, + simplifiedMasternodeList, + rsDrive, + ) => { + const stateRepository = new DriveStateRepository( + identityRepository, + identityBalanceRepository, + identityPublicKeyRepository, + dataContractRepository, + fetchDocuments, + documentRepository, + spentAssetLockTransactionsRepository, + coreRpcClient, + latestBlockExecutionContext, + simplifiedMasternodeList, + rsDrive, + ); + + return new CachedStateRepositoryDecorator( + stateRepository, + ); + }).singleton(), + + transactionalStateRepository: asFunction(( + identityRepository, + identityBalanceRepository, + identityPublicKeyRepository, + dataContractRepository, + fetchDocuments, + documentRepository, + spentAssetLockTransactionsRepository, + coreRpcClient, + proposalBlockExecutionContext, + simplifiedMasternodeList, + logStateRepository, + rsDrive, + ) => { + const stateRepository = new DriveStateRepository( + identityRepository, + identityBalanceRepository, + identityPublicKeyRepository, + dataContractRepository, + fetchDocuments, + documentRepository, + spentAssetLockTransactionsRepository, + coreRpcClient, + proposalBlockExecutionContext, + simplifiedMasternodeList, + rsDrive, + { + useTransaction: true, + }, + ); + + const cachedRepository = new CachedStateRepositoryDecorator( + stateRepository, + ); + + if (!logStateRepository) { + return cachedRepository; + } + + return new LoggedStateRepositoryDecorator( + cachedRepository, + proposalBlockExecutionContext, + ); + }).singleton(), + + unserializeStateTransition: asFunction(( + dpp, + noopLogger, + ) => unserializeStateTransitionFactory(dpp, noopLogger)).singleton(), + + transactionalUnserializeStateTransition: asFunction(( + transactionalDpp, + noopLogger, + ) => unserializeStateTransitionFactory(transactionalDpp, noopLogger)).singleton(), + + dpp: asFunction((stateRepository, dppOptions) => ( + new DashPlatformProtocol({ + ...dppOptions, + stateRepository, + }) + )).singleton(), + + transactionalDpp: asFunction((transactionalStateRepository, dppOptions) => ( + new DashPlatformProtocol({ + ...dppOptions, + stateRepository: transactionalStateRepository, + }) + )).singleton(), + }); + + /** + * Register validator quorums + */ + container.register({ + validatorSet: asClass(ValidatorSet), + }); + + /** + * Register withrawals stuff + */ + container.register({ + updateWithdrawalTransactionIdAndStatus: asFunction( + updateWithdrawalTransactionIdAndStatusFactory, + ), + broadcastWithdrawalTransactions: asFunction(broadcastWithdrawalTransactionsFactory), + }); + + /** + * Register feature flags stuff + */ + container.register({ + getLatestFeatureFlag: asFunction(getLatestFeatureFlagFactory), + getFeatureFlagForHeight: asFunction(getFeatureFlagForHeightFactory), + }); + + /** + * Register Core stuff + */ + container.register({ + waitForCoreSync: asFunction(waitForCoreSyncFactory).singleton(), + + updateSimplifiedMasternodeList: asFunction(updateSimplifiedMasternodeListFactory).singleton(), + + waitForChainLockedHeight: asFunction(waitForChainLockedHeightFactory).singleton(), + + waitForCoreChainLockSync: asFunction(waitForCoreChainLockSyncFactory).singleton(), + + fetchTransaction: asFunction(fetchTransactionFactory).singleton(), + }); + + /** + * Register ABCI handlers + */ + container.register({ + createContextLogger: asFunction(createContextLoggerFactory), + abciAsyncLocalStorage: asValue(new AsyncLocalStorage()), + createQueryResponse: asFunction(createQueryResponseFactory).singleton(), + createValidatorSetUpdate: asValue(createValidatorSetUpdate), + identityQueryHandler: asFunction(identityQueryHandlerFactory).singleton(), + dataContractQueryHandler: asFunction(dataContractQueryHandlerFactory).singleton(), + documentQueryHandler: asFunction(documentQueryHandlerFactory).singleton(), + getProofsQueryHandler: asFunction(getProofsQueryHandlerFactory).singleton(), + identitiesByPublicKeyHashesQueryHandler: + asFunction(identitiesByPublicKeyHashesQueryHandlerFactory).singleton(), + + queryHandlerRouter: asFunction(( + identityQueryHandler, + dataContractQueryHandler, + documentQueryHandler, + identitiesByPublicKeyHashesQueryHandler, + getProofsQueryHandler, + ) => { + const router = findMyWay({ + ignoreTrailingSlash: true, + }); + + router.on('GET', '/identities', identityQueryHandler); + router.on('GET', '/dataContracts', dataContractQueryHandler); + router.on('GET', '/dataContracts/documents', documentQueryHandler); + router.on('GET', '/proofs', getProofsQueryHandler); + router.on('GET', '/identities/by-public-key-hash', identitiesByPublicKeyHashesQueryHandler); + + return router; + }).singleton(), + + beginBlock: asFunction(beginBlockFactory).singleton(), + + processProposal: asFunction(processProposalFactory), + + deliverTx: asFunction(deliverTxFactory).singleton(), + + wrappedDeliverTx: asFunction(( + wrapInErrorHandler, + enrichErrorWithContextError, + deliverTx, + ) => wrapInErrorHandler( + enrichErrorWithContextError(deliverTx), + )).singleton(), + + endBlock: asFunction(endBlockFactory).singleton(), + + verifyChainLock: asFunction(verifyChainLockFactory).singleton(), + + rotateAndCreateValidatorSetUpdate: asFunction( + rotateAndCreateValidatorSetUpdateFactory, + ).singleton(), + + createConsensusParamUpdate: asFunction(createConsensusParamUpdateFactory).singleton(), + + createCoreChainLockUpdate: asFunction(createCoreChainLockUpdateFactory).singleton(), + + infoHandler: asFunction(infoHandlerFactory).singleton(), + + checkTxHandler: asFunction(checkTxHandlerFactory).singleton(), + + initChainHandler: asFunction(initChainHandlerFactory).singleton(), + + queryHandler: asFunction(queryHandlerFactory).singleton(), + + extendVoteHandler: asFunction(extendVoteHandlerFactory).singleton(), + + finalizeBlockHandler: asFunction(finalizeBlockHandlerFactory).singleton(), + + prepareProposalHandler: asFunction(prepareProposalHandlerFactory).singleton(), + + processProposalHandler: asFunction(processProposalHandlerFactory).singleton(), + + verifyVoteExtensionHandler: asFunction(verifyVoteExtensionHandlerFactory).singleton(), + + wrapInErrorHandler: asFunction(wrapInErrorHandlerFactory).singleton(), + enrichErrorWithContextError: asFunction(enrichErrorWithConsensusErrorFactory).singleton(), + errorHandler: asFunction(errorHandlerFactory).singleton(), + + abciHandlers: asFunction(( + infoHandler, + checkTxHandler, + initChainHandler, + wrapInErrorHandler, + enrichErrorWithContextError, + queryHandler, + extendVoteHandler, + finalizeBlockHandler, + prepareProposalHandler, + processProposalHandler, + verifyVoteExtensionHandler, + ) => ({ + info: enrichErrorWithContextError(infoHandler), + checkTx: wrapInErrorHandler(enrichErrorWithContextError(checkTxHandler)), + initChain: enrichErrorWithContextError(initChainHandler), + query: wrapInErrorHandler(enrichErrorWithContextError(queryHandler)), + extendVote: enrichErrorWithContextError(extendVoteHandler), + finalizeBlock: enrichErrorWithContextError(finalizeBlockHandler), + prepareProposal: enrichErrorWithContextError(prepareProposalHandler), + processProposal: enrichErrorWithContextError(processProposalHandler), + verifyVoteExtension: enrichErrorWithContextError(verifyVoteExtensionHandler), + })).singleton(), + + closeAbciServer: asFunction(closeAbciServerFactory).singleton(), + + abciServer: asFunction((abciHandlers) => createABCIServer(abciHandlers)) + .singleton(), + }); + + return container; +} + +module.exports = createDIContainer; diff --git a/packages/js-drive/lib/dataContract/DataContractStoreRepository.js b/packages/js-drive/lib/dataContract/DataContractStoreRepository.js new file mode 100644 index 00000000000..0b8c33925d5 --- /dev/null +++ b/packages/js-drive/lib/dataContract/DataContractStoreRepository.js @@ -0,0 +1,181 @@ +const { createHash } = require('crypto'); + +const PreCalculatedOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/PreCalculatedOperation'); +const StorageResult = require('../storage/StorageResult'); + +class DataContractStoreRepository { + /** + * + * @param {GroveDBStore} groveDBStore + * @param {decodeProtocolEntity} decodeProtocolEntity + * @param {BaseLogger} [logger] + */ + constructor(groveDBStore, decodeProtocolEntity, logger = undefined) { + this.storage = groveDBStore; + this.decodeProtocolEntity = decodeProtocolEntity; + this.logger = logger; + } + + /** + * Create Data Contract in database + * + * @param {DataContract} dataContract + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async create(dataContract, blockInfo, options = {}) { + try { + const feeResult = await this.storage.getDrive().createContract( + dataContract, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + dataContract: dataContract.toBuffer().toString('hex'), + dataContractHash: createHash('sha256') + .update( + dataContract.toBuffer(), + ).digest('hex'), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'createContract'); + } + } + } + + /** + * Update Data Contract in database + * + * @param {DataContract} dataContract + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async update(dataContract, blockInfo, options = {}) { + try { + const feeResult = await this.storage.getDrive().updateContract( + dataContract, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + dataContract: dataContract.toBuffer().toString('hex'), + dataContractHash: createHash('sha256') + .update( + dataContract.toBuffer(), + ).digest('hex'), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'updateContract'); + } + } + } + + /** + * Fetch Data Contract by ID from database + * + * @param {Identifier} id + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @param {BlockInfo} [options.blockInfo] + * + * @return {Promise>} + */ + async fetch(id, options = {}) { + if (options.dryRun) { + return new StorageResult( + null, + [], + ); + } + + const [dataContract, feeResult] = await this.storage.getDrive().fetchContract( + id, + options && options.blockInfo ? options.blockInfo.epoch : undefined, + Boolean(options.useTransaction), + ); + + const operations = []; + if (feeResult) { + operations.push(new PreCalculatedOperation(feeResult)); + } + + return new StorageResult( + dataContract, + operations, + ); + } + + /** + * Prove Data Contract by ID from database + * + * @param {Identifier} id + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + * */ + async prove(id, options) { + return this.proveMany([id], options); + } + + /** + * Prove Data Contract by IDs from database + * + * @param {Identifier[]} ids + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + * */ + async proveMany(ids, options) { + const items = ids.map((id) => ({ + type: 'key', + key: id.toBuffer(), + })); + + return this.storage.proveQuery({ + path: DataContractStoreRepository.TREE_PATH, + query: { + query: { + items, + subqueryPath: [ + DataContractStoreRepository.DATA_CONTRACT_KEY, + ], + }, + }, + }, options); + } +} + +DataContractStoreRepository.TREE_PATH = [Buffer.from([64])]; +DataContractStoreRepository.DATA_CONTRACT_KEY = Buffer.from([0]); +DataContractStoreRepository.DOCUMENTS_TREE_KEY = Buffer.from([0]); + +module.exports = DataContractStoreRepository; diff --git a/packages/js-drive/lib/document/DocumentRepository.js b/packages/js-drive/lib/document/DocumentRepository.js new file mode 100644 index 00000000000..0e2c7fa6183 --- /dev/null +++ b/packages/js-drive/lib/document/DocumentRepository.js @@ -0,0 +1,330 @@ +const { createHash } = require('crypto'); + +const lodashCloneDeep = require('lodash/cloneDeep'); + +const PreCalculatedOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/PreCalculatedOperation'); +const DummyFeeResult = require('@dashevo/dpp/lib/stateTransition/fee/DummyFeeResult'); +const InvalidQueryError = require('./errors/InvalidQueryError'); +const StorageResult = require('../storage/StorageResult'); +const DataContractStoreRepository = require('../dataContract/DataContractStoreRepository'); + +class DocumentRepository { + /** + * + * @param {GroveDBStore} groveDBStore + * @param {BaseLogger} [logger] + */ + constructor( + groveDBStore, + logger = undefined, + ) { + this.storage = groveDBStore; + this.logger = logger; + } + + /** + * Create document + * + * @param {Document} document + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async create(document, blockInfo, options = {}) { + let feeResult; + + try { + (feeResult = await this.storage.getDrive() + .createDocument( + document, + blockInfo, + Boolean(options.useTransaction), + Boolean(options.dryRun), + )); + } finally { + if (this.logger) { + this.logger.info({ + document: document.toBuffer().toString('hex'), + documentHash: createHash('sha256') + .update( + document.toBuffer(), + ).digest('hex'), + useTransaction: Boolean(options.useTransaction), + dryRun: Boolean(options.dryRun), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'createDocument'); + } + } + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } + + /** + * Update document + * + * @param {Document} document + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async update(document, blockInfo, options = {}) { + let feeResult; + + try { + (feeResult = await this.storage.getDrive() + .updateDocument( + document, + blockInfo, + Boolean(options.useTransaction), + Boolean(options.dryRun), + )); + } finally { + if (this.logger) { + this.logger.info({ + document: document.toBuffer().toString('hex'), + documentHash: createHash('sha256') + .update( + document.toBuffer(), + ).digest('hex'), + useTransaction: Boolean(options.useTransaction), + dryRun: Boolean(options.dryRun), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'updateDocument'); + } + } + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } + + /** + * Find documents with query + * + * @param {DataContract} dataContract + * @param {string} documentType + * @param {Object} [options] + * @param {Array} [options.where] + * @param {number} [options.limit] + * @param {Buffer} [options.startAt] + * @param {Buffer} [options.startAfter] + * @param {Array} [options.orderBy] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @param {BlockInfo} [options.blockInfo] + * + * @throws InvalidQueryError + * + * @returns {Promise>} + */ + async find(dataContract, documentType, options = {}) { + const query = lodashCloneDeep(options); + let useTransaction = false; + + if (typeof query === 'object' && !Array.isArray(query) && query !== null) { + ({ useTransaction } = query); + delete query.useTransaction; + delete query.dryRun; + delete query.blockInfo; + + // Remove undefined options before we pass them to RS Drive + Object.keys(query) + .forEach((queryOption) => { + if (query[queryOption] === undefined) { + // eslint-disable-next-line no-param-reassign + delete query[queryOption]; + } + }); + } + + try { + let epochIndex; + + if (options && options.blockInfo) { + epochIndex = options.blockInfo.epoch; + } + + const [documents, processingCost] = await this.storage.getDrive() + .queryDocuments( + dataContract, + documentType, + epochIndex, + query, + useTransaction, + ); + + return new StorageResult( + documents, + [ + new PreCalculatedOperation(new DummyFeeResult(0, processingCost, [])), + ], + ); + } catch (e) { + if (e.message.startsWith('query: ')) { + throw new InvalidQueryError(e.message.substring(7, e.message.length)); + } + + if (e.message.startsWith('structure: ')) { + throw new InvalidQueryError(e.message.substring(11, e.message.length)); + } + + if (e.message.startsWith('contract: ')) { + throw new InvalidQueryError(e.message.substring(10, e.message.length)); + } + + if (e.message.startsWith('protocol: ')) { + throw new InvalidQueryError(e.message.substring(10, e.message.length)); + } + + throw e; + } + } + + /** + * @param {DataContract} dataContract + * @param {string} documentType + * @param {Identifier} id + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @return {Promise>} + */ + async delete(dataContract, documentType, id, blockInfo, options = { }) { + try { + const feeResult = await this.storage.getDrive() + .deleteDocument( + dataContract.getId(), + documentType, + id, + blockInfo, + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.info({ + dataContractId: dataContract.getId().toString(), + documentType, + id: id.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'deleteDocument'); + } + } + } + + /** + * @param {DataContract} dataContract + * @param {string} documentType + * @param {Object} options + * @param {boolean} [options.useTransaction=false] + * @return {Promise} + */ + async prove(dataContract, documentType, options = {}) { + const query = lodashCloneDeep(options); + let useTransaction = false; + + if (typeof query === 'object' && !Array.isArray(query) && query !== null) { + ({ useTransaction } = query); + delete query.useTransaction; + delete query.dryRun; + + // Remove undefined options before we pass them to RS Drive + Object.keys(query) + .forEach((queryOption) => { + if (query[queryOption] === undefined) { + // eslint-disable-next-line no-param-reassign + delete query[queryOption]; + } + }); + } + + try { + const [prove, processingCost] = await this.storage.getDrive() + .proveDocumentsQuery( + dataContract, + documentType, + query, + useTransaction, + ); + + return new StorageResult( + prove, + [ + new PreCalculatedOperation(new DummyFeeResult(0, processingCost, [])), + ], + ); + } catch (e) { + if (e.message.startsWith('query: ')) { + throw new InvalidQueryError(e.message.substring(7, e.message.length)); + } + + if (e.message.startsWith('structure: ')) { + throw new InvalidQueryError(e.message.substring(11, e.message.length)); + } + + if (e.message.startsWith('contract: ')) { + throw new InvalidQueryError(e.message.substring(10, e.message.length)); + } + + throw e; + } + } + + /** + * Prove documents from different contracts + * + * @param {{ dataContractId: Buffer, documentId: Buffer, type: string }[]} documents + * @return {Promise>} + */ + async proveManyDocumentsFromDifferentContracts(documents) { + const queries = documents.map(({ dataContractId, documentId, type }) => { + const dataContractsDocumentsPath = [ + dataContractId, + Buffer.from([1]), + Buffer.from(type), + Buffer.from([0]), + ]; + + return { + path: DataContractStoreRepository.TREE_PATH.concat(dataContractsDocumentsPath), + query: { + query: { + items: [ + { + type: 'key', + key: documentId, + }, + ], + }, + }, + }; + }); + + return this.storage.proveQueryMany(queries); + } +} + +module.exports = DocumentRepository; diff --git a/packages/js-drive/lib/document/errors/InvalidQueryError.js b/packages/js-drive/lib/document/errors/InvalidQueryError.js new file mode 100644 index 00000000000..899eae1a1f6 --- /dev/null +++ b/packages/js-drive/lib/document/errors/InvalidQueryError.js @@ -0,0 +1,7 @@ +const DriveError = require('../../errors/DriveError'); + +class InvalidQueryError extends DriveError { + +} + +module.exports = InvalidQueryError; diff --git a/packages/js-drive/lib/document/fetchDataContractFactory.js b/packages/js-drive/lib/document/fetchDataContractFactory.js new file mode 100644 index 00000000000..b81b04deeee --- /dev/null +++ b/packages/js-drive/lib/document/fetchDataContractFactory.js @@ -0,0 +1,44 @@ +const IdentifierError = require('@dashevo/dpp/lib/identifier/errors/IdentifierError'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); + +const InvalidQueryError = require('./errors/InvalidQueryError'); + +/** + * @param {DataContractStoreRepository} dataContractRepository + * @returns {fetchDocuments} + */ +function fetchDataContractFactory( + dataContractRepository, +) { + /** + * Fetch Data Contract by Contract ID and type + * + * @typedef {Promise} fetchDataContract + * @param {Buffer|Identifier} contractId + * @returns {Promise>} + */ + async function fetchDataContract(contractId) { + let contractIdIdentifier; + try { + contractIdIdentifier = new Identifier(contractId); + } catch (e) { + if (e instanceof IdentifierError) { + throw new InvalidQueryError(`invalid data contract ID: ${e.message}`); + } + + throw e; + } + + const dataContractResult = await dataContractRepository.fetch(contractIdIdentifier); + + if (dataContractResult.isNull()) { + throw new InvalidQueryError(`data contract ${contractIdIdentifier} not found`); + } + + return dataContractResult; + } + + return fetchDataContract; +} + +module.exports = fetchDataContractFactory; diff --git a/packages/js-drive/lib/document/fetchDocumentsFactory.js b/packages/js-drive/lib/document/fetchDocumentsFactory.js new file mode 100644 index 00000000000..31f4cda35d3 --- /dev/null +++ b/packages/js-drive/lib/document/fetchDocumentsFactory.js @@ -0,0 +1,46 @@ +const InvalidQueryError = require('./errors/InvalidQueryError'); + +/** + * @param {DocumentRepository} documentRepository + * @param {fetchDataContract} fetchDataContract + * @returns {fetchDocuments} + */ +function fetchDocumentsFactory( + documentRepository, + fetchDataContract, +) { + /** + * Fetch original Documents by Contract ID and type + * + * @typedef {Promise} fetchDocuments + * @param {Buffer|Identifier} dataContractId + * @param {string} type + * @param {Object} [options] options + * @param {boolean} [options.useTransaction=false] + * @returns {Promise} + */ + async function fetchDocuments(dataContractId, type, options) { + const dataContractResult = await fetchDataContract(dataContractId); + + const dataContract = dataContractResult.getValue(); + const operations = dataContractResult.getOperations(); + + if (!dataContract.isDocumentDefined(type)) { + throw new InvalidQueryError(`document type ${type} is not defined in the data contract`); + } + + const result = await documentRepository.find( + dataContract, + type, + options, + ); + + result.addOperation(...operations); + + return result; + } + + return fetchDocuments; +} + +module.exports = fetchDocumentsFactory; diff --git a/packages/js-drive/lib/document/groveDB/createDocumentTreePath.js b/packages/js-drive/lib/document/groveDB/createDocumentTreePath.js new file mode 100644 index 00000000000..b2dc1625d57 --- /dev/null +++ b/packages/js-drive/lib/document/groveDB/createDocumentTreePath.js @@ -0,0 +1,15 @@ +const DataContractStoreRepository = require('../../dataContract/DataContractStoreRepository'); +/** + * @param {DataContract} dataContract + * @param {string} documentType + * @return {Buffer[]} + */ +function createDocumentTypeTreePath(dataContract, documentType) { + return DataContractStoreRepository.TREE_PATH.concat([ + dataContract.getId().toBuffer(), + Buffer.from([1]), + Buffer.from(documentType), + ]); +} + +module.exports = createDocumentTypeTreePath; diff --git a/packages/js-drive/lib/document/proveDocumentsFactory.js b/packages/js-drive/lib/document/proveDocumentsFactory.js new file mode 100644 index 00000000000..3da870652a0 --- /dev/null +++ b/packages/js-drive/lib/document/proveDocumentsFactory.js @@ -0,0 +1,39 @@ +/** + * @param {DocumentRepository} documentRepository + * @param {fetchDataContract} fetchDataContract + * @returns {fetchDocuments} + */ +function proveDocumentsFactory( + documentRepository, + fetchDataContract, +) { + /** + * + * @typedef {Promise} proveDocuments + * @param {Buffer|Identifier} dataContractId + * @param {string} type + * @param {Object} [options] options + * @param {boolean} [options.useTransaction=false] + * @returns {Promise} + */ + async function proveDocuments(dataContractId, type, options) { + const dataContractResult = await fetchDataContract(dataContractId); + + const dataContract = dataContractResult.getValue(); + const operations = dataContractResult.getOperations(); + + const result = await documentRepository.prove( + dataContract, + type, + options, + ); + + result.addOperation(...operations); + + return result; + } + + return proveDocuments; +} + +module.exports = proveDocumentsFactory; diff --git a/packages/js-drive/lib/dpp/CachedStateRepositoryDecorator.js b/packages/js-drive/lib/dpp/CachedStateRepositoryDecorator.js new file mode 100644 index 00000000000..7313dcbb84a --- /dev/null +++ b/packages/js-drive/lib/dpp/CachedStateRepositoryDecorator.js @@ -0,0 +1,344 @@ +/** + * @implements StateRepository + */ +class CachedStateRepositoryDecorator { + /** + * @param {DriveStateRepository} stateRepository + */ + constructor( + stateRepository, + ) { + this.stateRepository = stateRepository; + } + + /** + * Fetch Identity by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async fetchIdentity(id, executionContext = undefined) { + return this.stateRepository.fetchIdentity(id, executionContext); + } + + /** + * Create identity + * + * @param {Identity} identity + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createIdentity(identity, executionContext = undefined) { + return this.stateRepository.createIdentity(identity, executionContext); + } + + /** + * Add keys to identity + * + * @param {Identifier} identityId + * @param {IdentityPublicKey[]} keys + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addKeysToIdentity(identityId, keys, executionContext = undefined) { + return this.stateRepository.addKeysToIdentity(identityId, keys, executionContext); + } + + /** + * Fetch identity balance + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async fetchIdentityBalance(identityId, executionContext = undefined) { + return this.stateRepository.fetchIdentityBalance( + identityId, + executionContext, + ); + } + + /** + * Fetch identity balance with debt + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} - Balance can be negative in case of debt + */ + async fetchIdentityBalanceWithDebt(identityId, executionContext = undefined) { + return this.stateRepository.fetchIdentityBalanceWithDebt( + identityId, + executionContext, + ); + } + + /** + * Add to identity balance + * + * @param {Identifier} identityId + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToIdentityBalance(identityId, amount, executionContext = undefined) { + return this.stateRepository.addToIdentityBalance( + identityId, + amount, + executionContext, + ); + } + + /** + * Add to system credits + * + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToSystemCredits(amount, executionContext = undefined) { + return this.stateRepository.addToSystemCredits( + amount, + executionContext, + ); + } + + /** + * Disable identity keys + * + * @param {Identifier} identityId + * @param {number[]} keyIds + * @param {number} disableAt + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async disableIdentityKeys(identityId, keyIds, disableAt, executionContext = undefined) { + return this.stateRepository.disableIdentityKeys( + identityId, + keyIds, + disableAt, + executionContext, + ); + } + + /** + * Update identity revision + * + * @param {Identifier} identityId + * @param {number} revision + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async updateIdentityRevision(identityId, revision, executionContext = undefined) { + return this.stateRepository.updateIdentityRevision(identityId, revision, executionContext); + } + + /** + * Store spent asset lock transaction + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async markAssetLockTransactionOutPointAsUsed(outPointBuffer, executionContext = undefined) { + return this.stateRepository.markAssetLockTransactionOutPointAsUsed( + outPointBuffer, + executionContext, + ); + } + + /** + * Check if spent asset lock transaction is stored + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async isAssetLockTransactionOutPointAlreadyUsed(outPointBuffer, executionContext = undefined) { + return this.stateRepository.isAssetLockTransactionOutPointAlreadyUsed( + outPointBuffer, + executionContext, + ); + } + + /** + * Fetch Data Contract by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDataContract(id, executionContext = undefined) { + return this.stateRepository.fetchDataContract( + id, + executionContext, + ); + } + + /** + * Create Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDataContract(dataContract, executionContext = undefined) { + return this.stateRepository.createDataContract(dataContract, executionContext); + } + + /** + * Update Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDataContract(dataContract, executionContext = undefined) { + return this.stateRepository.updateDataContract(dataContract, executionContext); + } + + /** + * Fetch Documents by contract ID and type + * + * @param {Identifier} contractId + * @param {string} type + * @param {{ where: Object }} [options] + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDocuments(contractId, type, options = {}, executionContext = undefined) { + return this.stateRepository.fetchDocuments(contractId, type, options, executionContext); + } + + /** + * Create document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDocument(document, executionContext = undefined) { + return this.stateRepository.createDocument(document, executionContext); + } + + /** + * Update document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDocument(document, executionContext = undefined) { + return this.stateRepository.updateDocument(document, executionContext); + } + + /** + * Remove document + * + * @param {DataContract} dataContract + * @param {string} type + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async removeDocument(dataContract, type, id, executionContext = undefined) { + return this.stateRepository.removeDocument(dataContract, type, id, executionContext); + } + + /** + * Fetch transaction by ID + * + * @param {string} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchTransaction(id, executionContext = undefined) { + return this.stateRepository.fetchTransaction(id, executionContext); + } + + /** + * Fetch the latest platform block height + * + * @return {Promise} + */ + async fetchLatestPlatformBlockHeight() { + return this.stateRepository.fetchLatestPlatformBlockHeight(); + } + + /** + * Fetch the latest platform core chainlocked height + * + * @return {Promise} + */ + async fetchLatestPlatformCoreChainLockedHeight() { + return this.stateRepository.fetchLatestPlatformCoreChainLockedHeight(); + } + + /** + * Verify instant lock + * + * @param {InstantLock} instantLock + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async verifyInstantLock(instantLock, executionContext = undefined) { + return this.stateRepository.verifyInstantLock(instantLock, executionContext); + } + + /** + * Fetch Simplified Masternode List Store + * + * @return {Promise} + */ + async fetchSMLStore() { + return this.stateRepository.fetchSMLStore(); + } + + /** + * Fetch the latest withdrawal transaction index + * + * @returns {Promise} + */ + async fetchLatestWithdrawalTransactionIndex() { + return this.stateRepository.fetchLatestWithdrawalTransactionIndex(); + } + + /** + * Enqueue withdrawal transaction bytes into the queue + * + * @param {number} index + * @param {Buffer} transactionBytes + * + * @returns {Promise} + */ + async enqueueWithdrawalTransaction(index, transactionBytes) { + return this.stateRepository.enqueueWithdrawalTransaction( + index, + transactionBytes, + ); + } + + /** + * Returns block time + * + * @returns {Promise} + */ + async fetchLatestPlatformBlockTime() { + return this.stateRepository.fetchLatestPlatformBlockTime(); + } +} + +module.exports = CachedStateRepositoryDecorator; diff --git a/packages/js-drive/lib/dpp/DriveStateRepository.js b/packages/js-drive/lib/dpp/DriveStateRepository.js new file mode 100644 index 00000000000..173c9f92efa --- /dev/null +++ b/packages/js-drive/lib/dpp/DriveStateRepository.js @@ -0,0 +1,660 @@ +const { TYPES } = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); + +const ReadOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/ReadOperation'); +const SignatureVerificationOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/SignatureVerificationOperation'); +const BlockInfo = require('../blockExecution/BlockInfo'); + +/** + * @implements StateRepository + */ +class DriveStateRepository { + #options = {}; + + /** + * @param {IdentityStoreRepository} identityRepository + * @param {IdentityBalanceStoreRepository} identityBalanceRepository + * @param {IdentityPublicKeyStoreRepository} publicKeyToToIdentitiesRepository + * @param {DataContractStoreRepository} dataContractRepository + * @param {fetchDocuments} fetchDocuments + * @param {DocumentRepository} documentRepository + * @param {SpentAssetLockTransactionsRepository} spentAssetLockTransactionsRepository + * @param {RpcClient} coreRpcClient + * @param {BlockExecutionContext} blockExecutionContext + * @param {SimplifiedMasternodeList} simplifiedMasternodeList + * @param {Drive} rsDrive + * @param {Object} [options] + * @param {Object} [options.useTransaction=false] + */ + constructor( + identityRepository, + identityBalanceRepository, + publicKeyToToIdentitiesRepository, + dataContractRepository, + fetchDocuments, + documentRepository, + spentAssetLockTransactionsRepository, + coreRpcClient, + blockExecutionContext, + simplifiedMasternodeList, + rsDrive, + options = {}, + ) { + this.identityRepository = identityRepository; + this.identityBalanceRepository = identityBalanceRepository; + this.identityPublicKeyRepository = publicKeyToToIdentitiesRepository; + this.dataContractRepository = dataContractRepository; + this.fetchDocumentsFunction = fetchDocuments; + this.documentRepository = documentRepository; + this.spentAssetLockTransactionsRepository = spentAssetLockTransactionsRepository; + this.coreRpcClient = coreRpcClient; + this.blockExecutionContext = blockExecutionContext; + this.simplifiedMasternodeList = simplifiedMasternodeList; + this.rsDrive = rsDrive; + this.#options = options; + } + + /** + * Fetch Identity by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async fetchIdentity(id, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityRepository.fetch( + id, + { + blockInfo, + ...this.#createRepositoryOptions(executionContext), + }, + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return result.getValue(); + } + + /** + * Create identity + * + * @param {Identity} identity + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createIdentity(identity, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityRepository.create( + identity, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Add keys to identity + * + * @param {Identifier} identityId + * @param {IdentityPublicKey[]} keys + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addKeysToIdentity(identityId, keys, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityPublicKeyRepository.add( + identityId, + keys, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Fetch identity balance + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async fetchIdentityBalance(identityId, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityBalanceRepository.fetch( + identityId, + { + blockInfo, + ...this.#createRepositoryOptions(executionContext), + }, + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return result.getValue(); + } + + /** + * Fetch identity balance with debt + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} - Balance can be negative in case of debt + */ + async fetchIdentityBalanceWithDebt(identityId, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityBalanceRepository.fetchWithDebt( + identityId, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return result.getValue(); + } + + /** + * Add to identity balance + * + * @param {Identifier} identityId + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToIdentityBalance(identityId, amount, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityBalanceRepository.add( + identityId, + amount, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Add to system credits + * + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToSystemCredits(amount, executionContext = undefined) { + if (executionContext.isDryRun()) { + return; + } + + await this.rsDrive.addToSystemCredits( + amount, + this.#options.useTransaction || false, + ); + } + + /** + * Disable identity keys + * + * @param {Identifier} identityId + * @param {number[]} keyIds + * @param {number} disableAt + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async disableIdentityKeys(identityId, keyIds, disableAt, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityPublicKeyRepository.disable( + identityId, + keyIds, + disableAt, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Update identity revision + * + * @param {Identifier} identityId + * @param {number} revision + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async updateIdentityRevision(identityId, revision, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.identityRepository.updateRevision( + identityId, + revision, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Store spent asset lock transaction + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async markAssetLockTransactionOutPointAsUsed(outPointBuffer, executionContext = undefined) { + const result = await this.spentAssetLockTransactionsRepository.store( + outPointBuffer, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Check if spent asset lock transaction is stored + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async isAssetLockTransactionOutPointAlreadyUsed(outPointBuffer, executionContext = undefined) { + const result = await this.spentAssetLockTransactionsRepository.fetch( + outPointBuffer, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return !result.isNull(); + } + + /** + * Fetch Data Contract by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDataContract(id, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.dataContractRepository.fetch( + id, + { + ...this.#createRepositoryOptions(executionContext), + // This method doesn't implement dry run because we need a contract + // to proceed dry run validation and collect further operations + dryRun: false, + blockInfo, + }, + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return result.getValue(); + } + + /** + * Create Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDataContract(dataContract, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.dataContractRepository.create( + dataContract, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Update Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDataContract(dataContract, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.dataContractRepository.update( + dataContract, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Fetch Documents by contract ID and type + * + * @param {Identifier} contractId + * @param {string} type + * @param {{ where: Object }} [options] + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDocuments(contractId, type, options = {}, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.fetchDocumentsFunction( + contractId, + type, + { + blockInfo, + ...options, + ...this.#createRepositoryOptions(executionContext), + }, + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + + return result.getValue(); + } + + /** + * Create document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDocument(document, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.documentRepository.create( + document, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Update document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDocument(document, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.documentRepository.update( + document, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Remove document + * + * @param {DataContract} dataContract + * @param {string} type + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async removeDocument(dataContract, type, id, executionContext = undefined) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + const result = await this.documentRepository.delete( + dataContract, + type, + id, + blockInfo, + this.#createRepositoryOptions(executionContext), + ); + + if (executionContext) { + executionContext.addOperation(...result.getOperations()); + } + } + + /** + * Fetch Core transaction by ID + * + * @param {string} id - Transaction ID hex + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchTransaction(id, executionContext = undefined) { + if (executionContext && executionContext.isDryRun()) { + executionContext.addOperation( + // TODO: Revisit this value + new ReadOperation(512), + ); + + return { + data: Buffer.alloc(0), + height: 1, + }; + } + + try { + const { result: transaction } = await this.coreRpcClient.getRawTransaction(id, 1); + + const data = Buffer.from(transaction.hex, 'hex'); + + if (executionContext) { + executionContext.addOperation( + new ReadOperation(data.length), + ); + } + + return { + data, + height: transaction.height, + }; + } catch (e) { + // Invalid address or key error + if (e.code === -5) { + return null; + } + + throw e; + } + } + + /** + * Fetch the latest platform block height + * + * @return {Promise} + */ + async fetchLatestPlatformBlockHeight() { + return this.blockExecutionContext.getHeight(); + } + + /** + * Fetch the latest platform block time + * + * @return {Promise} + */ + async fetchLatestPlatformBlockTime() { + const timeMs = this.blockExecutionContext.getTimeMs(); + + if (!timeMs) { + throw new Error('Time is not set'); + } + + return timeMs; + } + + /** + * Fetch the latest platform core chainlocked height + * + * @return {Promise} + */ + async fetchLatestPlatformCoreChainLockedHeight() { + return this.blockExecutionContext.getCoreChainLockedHeight(); + } + + /** + * Verify instant lock + * + * @param {InstantLock} instantLock + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + // eslint-disable-next-line no-unused-vars + async verifyInstantLock(instantLock, executionContext = undefined) { + const coreChainLockedHeight = this.blockExecutionContext.getCoreChainLockedHeight(); + + if (coreChainLockedHeight === null) { + return false; + } + + if (executionContext) { + executionContext.addOperation( + new SignatureVerificationOperation(TYPES.ECDSA_SECP256K1), + ); + + if (executionContext.isDryRun()) { + return true; + } + } + + try { + const { result: isVerified } = await this.coreRpcClient.verifyIsLock( + instantLock.getRequestId().toString('hex'), + instantLock.txid, + instantLock.signature, + coreChainLockedHeight, + ); + + return isVerified; + } catch (e) { + // Invalid address or key error or + // Invalid, missing or duplicate parameter + // Parse error + if ([-8, -5, -32700].includes(e.code)) { + return false; + } + + throw e; + } + } + + /** + * Fetch Simplified Masternode List Store + * + * @return {Promise} + */ + async fetchSMLStore() { + return this.simplifiedMasternodeList.getStore(); + } + + /** + * Fetch the latest withdrawal transaction index + * + * @returns {Promise} + */ + async fetchLatestWithdrawalTransactionIndex() { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + return this.rsDrive.fetchLatestWithdrawalTransactionIndex( + blockInfo, + this.#options.useTransaction, + this.#options.dryRun, + ); + } + + /** + * Enqueue withdrawal transaction bytes into the queue + * + * @param {number} index + * @param {Buffer} transactionBytes + * + * @returns {Promise} + */ + async enqueueWithdrawalTransaction(index, transactionBytes) { + const blockInfo = BlockInfo.createFromBlockExecutionContext(this.blockExecutionContext); + + // TODO: handle dry run via passing state transition execution context + return this.rsDrive.enqueueWithdrawalTransaction( + index, + transactionBytes, + blockInfo, + this.#options.useTransaction, + ); + } + + /** + * @private + * @param {StateTransitionExecutionContext} [executionContext] + * @return {{dryRun: boolean, useTransaction: boolean}} + */ + #createRepositoryOptions(executionContext) { + return { + useTransaction: this.#options.useTransaction || false, + dryRun: executionContext ? executionContext.isDryRun() : false, + }; + } +} + +module.exports = DriveStateRepository; diff --git a/packages/js-drive/lib/dpp/LoggedStateRepositoryDecorator.js b/packages/js-drive/lib/dpp/LoggedStateRepositoryDecorator.js new file mode 100644 index 00000000000..c75e9b9e00b --- /dev/null +++ b/packages/js-drive/lib/dpp/LoggedStateRepositoryDecorator.js @@ -0,0 +1,676 @@ +/** + * @implements StateRepository + */ +class LoggedStateRepositoryDecorator { + /** + * @param {DriveStateRepository|CachedStateRepositoryDecorator} stateRepository + * @param {BlockExecutionContext} blockExecutionContext + */ + constructor( + stateRepository, + blockExecutionContext, + ) { + this.stateRepository = stateRepository; + this.blockExecutionContext = blockExecutionContext; + } + + /** + * @private + * @param {string} method - state repository method name + * @param {object} parameters - parameters of the state repository call + * @param {object} response - response of the state repository call + */ + log(method, parameters, response) { + const logger = this.blockExecutionContext.getContextLogger(); + + logger.trace({ + stateRepository: { + method, + parameters, + response, + }, + }, `StateRepository#${method}`); + } + + /** + * Fetch Identity by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async fetchIdentity(id, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchIdentity(id, executionContext); + } finally { + this.log( + 'fetchIdentity', + { + id, + }, + response, + ); + } + + return response; + } + + /** + * Create identity + * + * @param {Identity} identity + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createIdentity(identity, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.createIdentity(identity, executionContext); + } finally { + this.log( + 'createIdentity', + { + identity, + }, + response, + ); + } + + return response; + } + + /** + * Add keys to identity + * + * @param {Identifier} identityId + * @param {IdentityPublicKey[]} keys + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addKeysToIdentity(identityId, keys, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.addKeysToIdentity(identityId, keys, executionContext); + } finally { + this.log( + 'addKeysToIdentity', + { + identityId, + keys, + }, + response, + ); + } + + return response; + } + + /** + * Fetch identity balance + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async fetchIdentityBalance(identityId, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchIdentityBalance( + identityId, + executionContext, + ); + } finally { + this.log( + 'fetchIdentityBalance', + { + identityId, + }, + response, + ); + } + + return response; + } + + /** + * Fetch identity balance with debt + * + * @param {Identifier} identityId + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} - Balance can be negative in case of debt + */ + async fetchIdentityBalanceWithDebt(identityId, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchIdentityBalanceWithDebt( + identityId, + executionContext, + ); + } finally { + this.log( + 'fetchIdentityBalanceWithDebt', + { + identityId, + }, + response, + ); + } + + return response; + } + + /** + * Add to identity balance + * + * @param {Identifier} identityId + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToIdentityBalance(identityId, amount, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.addToIdentityBalance( + identityId, + amount, + executionContext, + ); + } finally { + this.log( + 'addToIdentityBalance', + { + identityId, + amount, + }, + response, + ); + } + + return response; + } + + /** + * Add to system credits + * + * @param {number} amount + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async addToSystemCredits(amount, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.addToSystemCredits( + amount, + executionContext, + ); + } finally { + this.log( + 'addToSystemCredits', + { + amount, + }, + response, + ); + } + + return response; + } + + /** + * Disable identity keys + * + * @param {Identifier} identityId + * @param {number[]} keyIds + * @param {number} disableAt + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async disableIdentityKeys(identityId, keyIds, disableAt, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.disableIdentityKeys( + identityId, + keyIds, + disableAt, + executionContext, + ); + } finally { + this.log( + 'disableIdentityKeys', + { + identityId, + keyIds, + disableAt, + }, + response, + ); + } + + return response; + } + + /** + * Update identity revision + * + * @param {Identifier} identityId + * @param {number} revision + * @param {StateTransitionExecutionContext} [executionContext] + * @returns {Promise} + */ + async updateIdentityRevision(identityId, revision, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.updateIdentityRevision( + identityId, + revision, + executionContext, + ); + } finally { + this.log( + 'updateIdentityRevision', + { + identityId, + revision, + }, + response, + ); + } + + return response; + } + + /** + * Store spent asset lock transaction + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async markAssetLockTransactionOutPointAsUsed(outPointBuffer, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.markAssetLockTransactionOutPointAsUsed( + outPointBuffer, + executionContext, + ); + } finally { + this.log( + 'markAssetLockTransactionOutPointAsUsed', + { + outPointBuffer: outPointBuffer.toString('base64'), + }, + response, + ); + } + + return response; + } + + /** + * Check if spent asset lock transaction is stored + * + * @param {Buffer} outPointBuffer + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async isAssetLockTransactionOutPointAlreadyUsed(outPointBuffer, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.isAssetLockTransactionOutPointAlreadyUsed( + outPointBuffer, + executionContext, + ); + } finally { + this.log( + 'isAssetLockTransactionOutPointAlreadyUsed', + { + outPointBuffer: outPointBuffer.toString('base64'), + }, + response, + ); + } + + return response; + } + + /** + * Fetch Data Contract by ID + * + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDataContract(id, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchDataContract(id, executionContext); + } finally { + this.log( + 'fetchDataContract', + { + id, + }, + response, + ); + } + + return response; + } + + /** + * Store Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDataContract(dataContract, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.createDataContract(dataContract, executionContext); + } finally { + this.log('createDataContract', { dataContract }, response); + } + + return response; + } + + /** + * Store Data Contract + * + * @param {DataContract} dataContract + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDataContract(dataContract, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.updateDataContract(dataContract, executionContext); + } finally { + this.log('updateDataContract', { dataContract }, response); + } + + return response; + } + + /** + * Fetch Documents by contract ID and type + * + * @param {Identifier} contractId + * @param {string} type + * @param {{ where: Object }} [options] + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchDocuments(contractId, type, options = {}, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchDocuments( + contractId, + type, + options, + executionContext, + ); + } finally { + this.log( + 'fetchDocuments', + { + contractId, + type, + options, + }, + response, + ); + } + + return response; + } + + /** + * Create document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async createDocument(document, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.createDocument(document, executionContext); + } finally { + this.log( + 'createDocument', + { + document, + }, + response, + ); + } + + return response; + } + + /** + * Update document + * + * @param {Document} document + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async updateDocument(document, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.updateDocument(document, executionContext); + } finally { + this.log( + 'updateDocument', + { + document, + }, + response, + ); + } + + return response; + } + + /** + * Remove document + * + * @param {DataContract} dataContract + * @param {string} type + * @param {Identifier} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async removeDocument(dataContract, type, id, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.removeDocument( + dataContract, + type, + id, + executionContext, + ); + } finally { + this.log( + 'removeDocument', + { + dataContract, + type, + id, + }, + response, + ); + } + + return response; + } + + /** + * Fetch transaction by ID + * + * @param {string} id + * @param {StateTransitionExecutionContext} [executionContext] + * + * @returns {Promise} + */ + async fetchTransaction(id, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.fetchTransaction(id, executionContext); + } finally { + this.log( + 'fetchTransaction', + { + id, + }, + response, + ); + } + + return response; + } + + /** + * Fetch the latest platform block height + * + * @return {Promise} + */ + async fetchLatestPlatformBlockHeight() { + let response; + + try { + response = await this.stateRepository.fetchLatestPlatformBlockHeight(); + } finally { + this.log('fetchLatestPlatformBlockHeight', {}, response); + } + + return response; + } + + /** + * Fetch the latest platform core chainlocked height + * + * @return {Promise} + */ + async fetchLatestPlatformCoreChainLockedHeight() { + let response; + + try { + response = await this.stateRepository.fetchLatestPlatformCoreChainLockedHeight(); + } finally { + this.log('fetchLatestPlatformCoreChainLockedHeight', {}, response); + } + + return response; + } + + /** + * Verify instant lock + * + * @param {InstantLock} instantLock + * @param {StateTransitionExecutionContext} [executionContext] + * + * @return {Promise} + */ + async verifyInstantLock(instantLock, executionContext = undefined) { + let response; + + try { + response = await this.stateRepository.verifyInstantLock(instantLock, executionContext); + } finally { + this.log('verifyInstantLock', { instantLock }, response); + } + + return response; + } + + /** + * Returns block time + * + * @returns {Promise} + */ + async fetchLatestPlatformBlockTime() { + let response; + + try { + response = await this.stateRepository.fetchLatestPlatformBlockTime(); + } finally { + this.log('fetchLatestPlatformBlockTime', { }, response); + } + + return response; + } + + /** + * Fetch the latest withdrawal transaction index + * + * @returns {Promise} + */ + async fetchLatestWithdrawalTransactionIndex() { + let response; + + try { + response = await this.stateRepository.fetchLatestWithdrawalTransactionIndex(); + } finally { + this.log('fetchLatestWithdrawalTransactionIndex', {}, response); + } + + return response; + } + + /** + * Enqueue withdrawal transaction bytes into the queue + * + * @param {number} index + * @param {Buffer} transactionBytes + * + * @returns {Promise} + */ + async enqueueWithdrawalTransaction(index, transactionBytes) { + let response; + + try { + response = await this.stateRepository.enqueueWithdrawalTransaction( + index, + transactionBytes, + ); + } finally { + this.log('enqueueWithdrawalTransaction', { index, transactionBytes }, response); + } + } +} + +module.exports = LoggedStateRepositoryDecorator; diff --git a/packages/js-drive/lib/errorHandlerFactory.js b/packages/js-drive/lib/errorHandlerFactory.js new file mode 100644 index 00000000000..7172646507a --- /dev/null +++ b/packages/js-drive/lib/errorHandlerFactory.js @@ -0,0 +1,59 @@ +const printErrorFace = require('./util/printErrorFace'); + +/** + * @param {BaseLogger} logger + * @param {AwilixContainer} container + * @param {closeAbciServer} closeAbciServer + */ +function errorHandlerFactory(logger, container, closeAbciServer) { + let isCalledAlready = false; + const errors = []; + + /** + * Error handler + * + * @param {Error} error + */ + async function errorHandler(error) { + // Collect all thrown errors + errors.push(error); + + // Gracefully shutdown only once + if (isCalledAlready) { + return; + } + + isCalledAlready = true; + + try { + try { + // Close all ABCI server connections + await closeAbciServer(); + + // Add further code to the end of event loop (the same as process.nextTick) + await Promise.resolve(); + + // eslint-disable-next-line no-console + console.log(printErrorFace()); + + errors.forEach((e) => { + const preferredLogger = e.contextLogger || logger; + delete e.contextLogger; + + preferredLogger.fatal({ err: e }, e.message); + }); + } finally { + await container.dispose(); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } finally { + process.exit(1); + } + } + + return errorHandler; +} + +module.exports = errorHandlerFactory; diff --git a/packages/js-drive/lib/errors/DriveError.js b/packages/js-drive/lib/errors/DriveError.js new file mode 100644 index 00000000000..ba5dc360532 --- /dev/null +++ b/packages/js-drive/lib/errors/DriveError.js @@ -0,0 +1,23 @@ +class DriveError extends Error { + /** + * @param {string} message + */ + constructor(message) { + super(message); + + this.name = this.constructor.name; + + Error.captureStackTrace(this, this.constructor); + } + + /** + * Get message + * + * @return {string} + */ + getMessage() { + return this.message; + } +} + +module.exports = DriveError; diff --git a/packages/js-drive/lib/featureFlag/getFeatureFlagForHeightFactory.js b/packages/js-drive/lib/featureFlag/getFeatureFlagForHeightFactory.js new file mode 100644 index 00000000000..c15175c48da --- /dev/null +++ b/packages/js-drive/lib/featureFlag/getFeatureFlagForHeightFactory.js @@ -0,0 +1,48 @@ +/** + * @param {Identifier} featureFlagsContractId + * @param {fetchDocuments} fetchDocuments + * + * @return {getFeatureFlagForHeight} + */ +function getFeatureFlagForHeightFactory( + featureFlagsContractId, + fetchDocuments, +) { + /** + * @typedef getFeatureFlagForHeight + * + * @param {string} flagType + * @param {Long} blockHeight + * @param {boolean} [useTransaction=false] + * + * @return {Promise} + */ + async function getFeatureFlagForHeight(flagType, blockHeight, useTransaction = false) { + if (!featureFlagsContractId) { + return null; + } + + const query = { + where: [ + ['enableAtHeight', '==', blockHeight.toNumber()], + ], + }; + + const result = await fetchDocuments( + featureFlagsContractId, + flagType, + { + ...query, + useTransaction, + }, + ); + + const [document] = result.getValue(); + + return document; + } + + return getFeatureFlagForHeight; +} + +module.exports = getFeatureFlagForHeightFactory; diff --git a/packages/js-drive/lib/featureFlag/getLatestFeatureFlagFactory.js b/packages/js-drive/lib/featureFlag/getLatestFeatureFlagFactory.js new file mode 100644 index 00000000000..56e1dfcd885 --- /dev/null +++ b/packages/js-drive/lib/featureFlag/getLatestFeatureFlagFactory.js @@ -0,0 +1,52 @@ +/** + * @param {Identifier} featureFlagsContractId + * @param {fetchDocuments} fetchDocuments + * + * @return {getLatestFeatureFlag} + */ +function getLatestFeatureFlagFactory( + featureFlagsContractId, + fetchDocuments, +) { + /** + * @typedef getLatestFeatureFlag + * + * @param {string} flagType + * @param {Long} blockHeight + * @param {boolean} [useTransaction=false] + * + * @return {Promise} + */ + async function getLatestFeatureFlag(flagType, blockHeight, useTransaction = false) { + if (!featureFlagsContractId) { + return null; + } + + const query = { + where: [ + ['enableAtHeight', '<=', blockHeight.toNumber()], + ], + orderBy: [ + ['enableAtHeight', 'desc'], + ], + limit: 1, + }; + + const result = await fetchDocuments( + featureFlagsContractId, + flagType, + { + ...query, + useTransaction, + }, + ); + + const [document] = result.getValue(); + + return document; + } + + return getLatestFeatureFlag; +} + +module.exports = getLatestFeatureFlagFactory; diff --git a/packages/js-drive/lib/identity/IdentityBalanceStoreRepository.js b/packages/js-drive/lib/identity/IdentityBalanceStoreRepository.js new file mode 100644 index 00000000000..7c745a4fb06 --- /dev/null +++ b/packages/js-drive/lib/identity/IdentityBalanceStoreRepository.js @@ -0,0 +1,200 @@ +const PreCalculatedOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/PreCalculatedOperation'); +const StorageResult = require('../storage/StorageResult'); + +class IdentityBalanceStoreRepository { + /** + * + * @param {GroveDBStore} groveDBStore + * @param {decodeProtocolEntity} decodeProtocolEntity + */ + constructor(groveDBStore, decodeProtocolEntity) { + this.storage = groveDBStore; + this.decodeProtocolEntity = decodeProtocolEntity; + } + + /** + * Fetch balance by id from database + * + * @param {Identifier} id + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {BlockInfo} [options.blockInfo] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async fetch(id, options = { }) { + if (options && options.blockInfo) { + const [balance, feeResult] = await this.storage.getDrive().fetchIdentityBalanceWithCosts( + id, + options.blockInfo.toObject(), + Boolean(options.useTransaction), + ); + + return new StorageResult( + balance, + [new PreCalculatedOperation(feeResult)], + ); + } + + const balance = await this.storage.getDrive().fetchIdentityBalance( + id, + Boolean(options.useTransaction), + ); + + return new StorageResult( + balance, + [], + ); + } + + /** + * Fetch identity by id from database + * + * @param {Identifier} id + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async fetchWithDebt(id, blockInfo, options = {}) { + const [ + balance, + feeResult, + ] = await this.storage.getDrive().fetchIdentityBalanceIncludeDebtWithCosts( + id, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + balance, + [new PreCalculatedOperation(feeResult)], + ); + } + + /** + * Add to identity balance in database + * + * @param {Identifier} identityId + * @param {number} amount + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async add(identityId, amount, blockInfo, options = {}) { + try { + const feeResult = await this.storage.getDrive().addToIdentityBalance( + identityId, + amount, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'add'); + } + } + } + + /** + * Apply fees to identity balance in database + * + * @param {Identifier} identityId + * @param {FeeResult} fees + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>} + */ + async applyFees( + identityId, + fees, + options = {}, + ) { + try { + const feeResult = await this.storage.getDrive().applyFeesToIdentityBalance( + identityId, + fees, + Boolean(options.useTransaction), + ); + + return new StorageResult( + feeResult, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'applyFees'); + } + } + } + + /** + * Remove balance from identity in database + * + * @param {Identifier} identityId + * @param {number} amount + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async remove( + identityId, + amount, + blockInfo, + options = {}, + ) { + try { + const feeResult = await this.storage.getDrive().removeFromIdentityBalance( + identityId, + amount, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'remove'); + } + } + } +} + +module.exports = IdentityBalanceStoreRepository; diff --git a/packages/js-drive/lib/identity/IdentityPublicKeyStoreRepository.js b/packages/js-drive/lib/identity/IdentityPublicKeyStoreRepository.js new file mode 100644 index 00000000000..52f7131c43d --- /dev/null +++ b/packages/js-drive/lib/identity/IdentityPublicKeyStoreRepository.js @@ -0,0 +1,109 @@ +const PreCalculatedOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/PreCalculatedOperation'); +const StorageResult = require('../storage/StorageResult'); + +class IdentityPublicKeyStoreRepository { + /** + * + * @param {GroveDBStore} groveDBStore + * @param {decodeProtocolEntity} decodeProtocolEntity + */ + constructor(groveDBStore, decodeProtocolEntity) { + this.storage = groveDBStore; + this.decodeProtocolEntity = decodeProtocolEntity; + } + + /** + * Add keys to an already existing Identity + * + * @param {Identifier} identityId + * @param {IdentityPublicKey[]} keys + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async add( + identityId, + keys, + blockInfo, + options = {}, + ) { + try { + const feeResult = await this.storage.getDrive().addKeysToIdentity( + identityId, + keys, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'add'); + } + } + } + + /** + * Disable keys in already existing Identity + * + * @param {Identifier} identityId + * @param {number[]} keyIds + * @param {number} disabledAt + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async disable( + identityId, + keyIds, + disabledAt, + blockInfo, + options = {}, + ) { + try { + const feeResult = await this.storage.getDrive().disableIdentityKeys( + identityId, + keyIds, + disabledAt, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'disable'); + } + } + } +} + +IdentityPublicKeyStoreRepository.TREE_PATH = [Buffer.from([2])]; + +module.exports = IdentityPublicKeyStoreRepository; diff --git a/packages/js-drive/lib/identity/IdentityStoreRepository.js b/packages/js-drive/lib/identity/IdentityStoreRepository.js new file mode 100644 index 00000000000..2bb339ee4e5 --- /dev/null +++ b/packages/js-drive/lib/identity/IdentityStoreRepository.js @@ -0,0 +1,295 @@ +const PreCalculatedOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/PreCalculatedOperation'); +const StorageResult = require('../storage/StorageResult'); + +class IdentityStoreRepository { + /** + * + * @param {GroveDBStore} groveDBStore + * @param {decodeProtocolEntity} decodeProtocolEntity + */ + constructor(groveDBStore, decodeProtocolEntity) { + this.storage = groveDBStore; + this.decodeProtocolEntity = decodeProtocolEntity; + } + + /** + * Fetch identity by id from database + * + * @param {Identifier} id + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {BlockInfo} [options.blockInfo] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async fetch(id, options = { }) { + if (options.dryRun) { + return new StorageResult( + null, + [], + ); + } + + if (options && options.blockInfo) { + const [identity, feeResult] = await this.storage.getDrive().fetchIdentityWithCosts( + id, + options.blockInfo.epoch, + Boolean(options.useTransaction), + ); + + return new StorageResult( + identity, + [new PreCalculatedOperation(feeResult)], + ); + } + + const identity = await this.storage.getDrive().fetchIdentity( + id, + Boolean(options.useTransaction), + ); + + return new StorageResult( + identity, + [], + ); + } + + /** + * Fetch identity by public key hash + * + * @param {Buffer} hash + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>} + */ + async fetchByPublicKeyHash(hash, options = {}) { + try { + const [identity] = await this.storage.getDrive().fetchIdentitiesByPublicKeyHashes( + [hash], + Boolean(options.useTransaction), + ); + + return new StorageResult( + identity, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + hash, + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'fetchManyByPublicKeyHashes'); + } + } + } + + /** + * Fetch many identities by public key hashes + * + * @param {Buffer[]} hashes + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>>} + */ + async fetchManyByPublicKeyHashes(hashes, options = {}) { + try { + const identities = await this.storage.getDrive().fetchIdentitiesByPublicKeyHashes( + hashes, + Boolean(options.useTransaction), + ); + + return new StorageResult( + identities, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + hashes, + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'fetchManyByPublicKeyHashes'); + } + } + } + + /** + * Prove identities by multiple public key hashes + * + * @param {Buffer[]} hashes + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>} + */ + async proveManyByPublicKeyHashes(hashes, options = {}) { + try { + const proof = await this.storage.getDrive().proveIdentitiesByPublicKeyHashes( + hashes, + Boolean(options.useTransaction), + ); + + return new StorageResult( + proof, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + hashes, + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'proveManyByPublicKeyHashes'); + } + } + } + + /** + * Create Identity in database + * + * @param {Identity} identity + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async create(identity, blockInfo, options = {}) { + try { + const feeResult = await this.storage.getDrive().insertIdentity( + identity, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identity.id.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'create'); + } + } + } + + /** + * Update identity revision in database + * + * @param {Identifier} identityId + * @param {number} revision + * @param {BlockInfo} blockInfo + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async updateRevision( + identityId, + revision, + blockInfo, + options = {}, + ) { + try { + const feeResult = await this.storage.getDrive().updateIdentityRevision( + identityId, + revision, + blockInfo.toObject(), + Boolean(options.useTransaction), + Boolean(options.dryRun), + ); + + return new StorageResult( + undefined, + [ + new PreCalculatedOperation(feeResult), + ], + ); + } finally { + if (this.logger) { + this.logger.trace({ + identity_id: identityId.toString(), + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'updateRevision'); + } + } + } + + /** + * Prove identity by id + * + * @param {Identifier} id + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>} + * */ + async prove(id, options = {}) { + try { + const proof = await this.storage.getDrive().proveIdentity( + id, + Boolean(options.useTransaction), + ); + + return new StorageResult( + proof, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + id, + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'prove'); + } + } + } + + /** + * Prove identity by ids + * + * @param {Identifier[]} ids + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * + * @return {Promise>} + * */ + async proveMany(ids, options = {}) { + try { + const proof = await this.storage.getDrive().proveManyIdentities( + ids, + Boolean(options.useTransaction), + ); + + return new StorageResult( + proof, + [], + ); + } finally { + if (this.logger) { + this.logger.trace({ + ids, + useTransaction: Boolean(options.useTransaction), + appHash: (await this.storage.getRootHash(options)).toString('hex'), + }, 'proveMany'); + } + } + } +} + +module.exports = IdentityStoreRepository; diff --git a/packages/js-drive/lib/identity/SpentAssetLockTransactionsRepository.js b/packages/js-drive/lib/identity/SpentAssetLockTransactionsRepository.js new file mode 100644 index 00000000000..081954da391 --- /dev/null +++ b/packages/js-drive/lib/identity/SpentAssetLockTransactionsRepository.js @@ -0,0 +1,73 @@ +const StorageResult = require('../storage/StorageResult'); + +class SpentAssetLockTransactionsRepository { + /** + * @param {GroveDBStore} groveDBStore + */ + constructor(groveDBStore) { + this.storage = groveDBStore; + } + + /** + * Store the outPoint + * + * @param {Buffer} outPointBuffer + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async store(outPointBuffer, options = {}) { + if (options.dryRun) { + return new StorageResult(undefined, []); + } + + const emptyValue = Buffer.from([0]); + + const result = await this.storage.put( + SpentAssetLockTransactionsRepository.TREE_PATH, + outPointBuffer, + emptyValue, + options, + ); + + return new StorageResult( + undefined, + result.getOperations(), + ); + } + + /** + * Fetch the outPoint + * + * @param {Buffer} outPointBuffer + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * + * @return {Promise>} + */ + async fetch(outPointBuffer, options = {}) { + if (options.dryRun) { + return new StorageResult(null, []); + } + + const result = await this.storage.get( + SpentAssetLockTransactionsRepository.TREE_PATH, + outPointBuffer, + options, + ); + + return new StorageResult( + result.getValue(), + result.getOperations(), + ); + } +} + +SpentAssetLockTransactionsRepository.TREE_PATH = [ + Buffer.from([72]), +]; + +module.exports = SpentAssetLockTransactionsRepository; diff --git a/packages/js-drive/lib/identity/masternode/LastSyncedCoreHeightRepository.js b/packages/js-drive/lib/identity/masternode/LastSyncedCoreHeightRepository.js new file mode 100644 index 00000000000..13ad4d4d140 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/LastSyncedCoreHeightRepository.js @@ -0,0 +1,68 @@ +const StorageResult = require('../../storage/StorageResult'); + +class LastSyncedCoreHeightRepository { + /** + * + * @param {GroveDBStore} groveDBStore + */ + constructor(groveDBStore) { + this.storage = groveDBStore; + } + + /** + * Store last synced core height + * + * @param {number} height + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @return {Promise>} + */ + async store(height, options = {}) { + const value = Buffer.alloc(4); + + value.writeUInt32BE(height); + + return this.storage.put( + LastSyncedCoreHeightRepository.TREE_PATH, + LastSyncedCoreHeightRepository.KEY, + value, + options, + ); + } + + /** + * Fetch last synced core height + * + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @return {Promise>} + */ + async fetch(options = {}) { + const encodedHeightResult = await this.storage.get( + LastSyncedCoreHeightRepository.TREE_PATH, + LastSyncedCoreHeightRepository.KEY, + { + ...options, + predictedValueSize: 4, + }, + ); + + if (encodedHeightResult.isNull()) { + return encodedHeightResult; + } + + const encodedHeight = encodedHeightResult.getValue(); + + return new StorageResult( + encodedHeight.readUInt32BE(), + encodedHeightResult.getOperations(), + ); + } +} + +LastSyncedCoreHeightRepository.TREE_PATH = [Buffer.from([112])]; +LastSyncedCoreHeightRepository.KEY = Buffer.from('lastSyncedCoreHeight'); + +module.exports = LastSyncedCoreHeightRepository; diff --git a/packages/js-drive/lib/identity/masternode/createMasternodeIdentityFactory.js b/packages/js-drive/lib/identity/masternode/createMasternodeIdentityFactory.js new file mode 100644 index 00000000000..9a8b006618b --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/createMasternodeIdentityFactory.js @@ -0,0 +1,83 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const InvalidMasternodeIdentityError = require('./errors/InvalidMasternodeIdentityError'); + +/** + * @param {DashPlatformProtocol} dpp + * @param {IdentityStoreRepository} identityRepository + * @param {getWithdrawPubKeyTypeFromPayoutScript} getWithdrawPubKeyTypeFromPayoutScript + * @param {getPublicKeyFromPayoutScript} getPublicKeyFromPayoutScript + * @return {createMasternodeIdentity} + */ +function createMasternodeIdentityFactory( + dpp, + identityRepository, + // getWithdrawPubKeyTypeFromPayoutScript, + // getPublicKeyFromPayoutScript, +) { + /** + * @typedef createMasternodeIdentity + * @param {BlockInfo} blockInfo + * @param {Identifier} identifier + * @param {Buffer} pubKeyData + * @param {number} pubKeyType + * @param {Script} [payoutScript] + * @return {Promise} + */ + async function createMasternodeIdentity( + blockInfo, + identifier, + pubKeyData, + pubKeyType, + // payoutScript, + ) { + const publicKeys = [{ + id: 0, + type: pubKeyType, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: true, + // Copy data buffer + data: Buffer.from(pubKeyData), + }]; + + // TODO: Enable keys when we have support of non unique keys in DPP + // if (payoutScript) { + // const withdrawPubKeyType = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + // + // publicKeys.push({ + // id: 1, + // type: withdrawPubKeyType, + // purpose: IdentityPublicKey.PURPOSES.WITHDRAW, + // securityLevel: IdentityPublicKey.SECURITY_LEVELS.CRITICAL, + // readOnly: false, + // data: getPublicKeyFromPayoutScript(payoutScript, withdrawPubKeyType), + // }); + // } + + const identity = new Identity({ + protocolVersion: dpp.getProtocolVersion(), + id: identifier.toBuffer(), + publicKeys, + balance: 0, + revision: 0, + }); + + const validationResult = await dpp.identity.validate(identity); + if (!validationResult.isValid()) { + const validationError = validationResult.getFirstError(); + + throw new InvalidMasternodeIdentityError(validationError); + } + + await identityRepository.create(identity, blockInfo, { + useTransaction: true, + }); + + return identity; + } + + return createMasternodeIdentity; +} + +module.exports = createMasternodeIdentityFactory; diff --git a/packages/js-drive/lib/identity/masternode/createOperatorIdentifier.js b/packages/js-drive/lib/identity/masternode/createOperatorIdentifier.js new file mode 100644 index 00000000000..4effefbf931 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/createOperatorIdentifier.js @@ -0,0 +1,20 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const { hash } = require('@dashevo/dpp/lib/util/hash'); + +/** + * @param {SimplifiedMNListEntry} smlEntry + */ +function createOperatorIdentifier(smlEntry) { + const operatorPubKey = Buffer.from(smlEntry.pubKeyOperator, 'hex'); + + return Identifier.from( + hash( + Buffer.concat([ + Buffer.from(smlEntry.proRegTxHash, 'hex'), + operatorPubKey, + ]), + ), + ); +} + +module.exports = createOperatorIdentifier; diff --git a/packages/js-drive/lib/identity/masternode/createRewardShareDocumentFactory.js b/packages/js-drive/lib/identity/masternode/createRewardShareDocumentFactory.js new file mode 100644 index 00000000000..d1462448dcc --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/createRewardShareDocumentFactory.js @@ -0,0 +1,88 @@ +const { hash } = require('@dashevo/dpp/lib/util/hash'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); + +const MAX_DOCUMENTS = 16; + +/** + * @param {DashPlatformProtocol} dpp + * @param {DocumentRepository} documentRepository + * @return {createRewardShareDocument} + */ +function createRewardShareDocumentFactory( + dpp, + documentRepository, +) { + /** + * @typedef {createRewardShareDocument} + * @param {DataContract} dataContract + * @param {Identifier} masternodeIdentifier + * @param {Identifier} operatorIdentifier + * @param {number} percentage + * @param {BlockInfo} blockInfo + * @returns {Promise} + */ + async function createRewardShareDocument( + dataContract, + masternodeIdentifier, + operatorIdentifier, + percentage, + blockInfo, + ) { + const documentsResult = await documentRepository.find( + dataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', masternodeIdentifier.toBuffer()], + ], + useTransaction: true, + }, + ); + + // Do not create a share if it's exist already + // or max shares limit is reached + if (!documentsResult.isEmpty()) { + if (documentsResult.getValue().length > MAX_DOCUMENTS) { + return null; + } + + const operatorShare = documentsResult.getValue().find((shareDocument) => ( + shareDocument.get('payToId').equals(operatorIdentifier) + )); + + if (operatorShare) { + return null; + } + } + + const rewardShareDocument = dpp.document.create( + dataContract, + masternodeIdentifier, + 'rewardShare', + { + payToId: operatorIdentifier, + percentage, + }, + ); + + // Create an identity for operator + const rewardShareDocumentIdSeed = hash( + Buffer.concat([ + masternodeIdentifier.toBuffer(), + operatorIdentifier.toBuffer(), + ]), + ); + + rewardShareDocument.id = Identifier.from(rewardShareDocumentIdSeed); + + await documentRepository.create(rewardShareDocument, blockInfo, { + useTransaction: true, + }); + + return rewardShareDocument; + } + + return createRewardShareDocument; +} + +module.exports = createRewardShareDocumentFactory; diff --git a/packages/js-drive/lib/identity/masternode/createVotingIdentifier.js b/packages/js-drive/lib/identity/masternode/createVotingIdentifier.js new file mode 100644 index 00000000000..69e61c89d2f --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/createVotingIdentifier.js @@ -0,0 +1,21 @@ +const { hash } = require('@dashevo/dpp/lib/util/hash'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const Address = require('@dashevo/dashcore-lib/lib/address'); + +/** + * @param {SimplifiedMNListEntry} smlEntry + */ +function createVotingIdentifier(smlEntry) { + const votingPubKeyHash = Address.fromString(smlEntry.votingAddress).hashBuffer; + + return Identifier.from( + hash( + Buffer.concat([ + Buffer.from(smlEntry.proRegTxHash, 'hex'), + votingPubKeyHash, + ]), + ), + ); +} + +module.exports = createVotingIdentifier; diff --git a/packages/js-drive/lib/identity/masternode/errors/InvalidIdentityPublicKeyTypeError.js b/packages/js-drive/lib/identity/masternode/errors/InvalidIdentityPublicKeyTypeError.js new file mode 100644 index 00000000000..59b2a671f58 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/errors/InvalidIdentityPublicKeyTypeError.js @@ -0,0 +1,22 @@ +const DriveError = require('../../../errors/DriveError'); + +class InvalidIdentityPublicKeyTypeError extends DriveError { + /** + * @param {number} type + */ + constructor(type) { + super('Invalid Identity Public Key type'); + + this.type = type; + } + + /** + * + * @return {number} + */ + getType() { + return this.type; + } +} + +module.exports = InvalidIdentityPublicKeyTypeError; diff --git a/packages/js-drive/lib/identity/masternode/errors/InvalidMasternodeIdentityError.js b/packages/js-drive/lib/identity/masternode/errors/InvalidMasternodeIdentityError.js new file mode 100644 index 00000000000..ad1a8fe7ba7 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/errors/InvalidMasternodeIdentityError.js @@ -0,0 +1,23 @@ +const DriveError = require('../../../errors/DriveError'); + +class InvalidMasternodeIdentityError extends DriveError { + /** + * @param {Error} validationError + */ + constructor(validationError) { + super('Invalid masternode identity'); + + this.validationError = validationError; + } + + /** + * Get validation error + * + * @return {Error} + */ + getValidationError() { + return this.validationError; + } +} + +module.exports = InvalidMasternodeIdentityError; diff --git a/packages/js-drive/lib/identity/masternode/errors/InvalidPayoutScriptError.js b/packages/js-drive/lib/identity/masternode/errors/InvalidPayoutScriptError.js new file mode 100644 index 00000000000..2a224ff0661 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/errors/InvalidPayoutScriptError.js @@ -0,0 +1,22 @@ +const DriveError = require('../../../errors/DriveError'); + +class InvalidPayoutScriptError extends DriveError { + /** + * @param {Buffer} payoutScript + */ + constructor(payoutScript) { + super('Invalid payout script'); + + this.payoutScript = payoutScript; + } + + /** + * + * @return {Buffer} + */ + getPayoutScript() { + return this.payoutScript; + } +} + +module.exports = InvalidPayoutScriptError; diff --git a/packages/js-drive/lib/identity/masternode/getPublicKeyFromPayoutScript.js b/packages/js-drive/lib/identity/masternode/getPublicKeyFromPayoutScript.js new file mode 100644 index 00000000000..e6dc34d4bd1 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/getPublicKeyFromPayoutScript.js @@ -0,0 +1,21 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const InvalidIdentityPublicKeyTypeError = require('@dashevo/dpp/lib/stateTransition/errors/InvalidIdentityPublicKeyTypeError'); + +/** + * @typedef getPublicKeyFromPayoutScript + * @param {Script} payoutScript + * @param {number} type + * @returns {Buffer} + */ +function getPublicKeyFromPayoutScript(payoutScript, type) { + switch (type) { + case IdentityPublicKey.TYPES.ECDSA_HASH160: + return payoutScript.toBuffer().slice(3, 23); + case IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH: + return payoutScript.toBuffer().slice(2, 22); + default: + throw new InvalidIdentityPublicKeyTypeError(type); + } +} + +module.exports = getPublicKeyFromPayoutScript; diff --git a/packages/js-drive/lib/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.js b/packages/js-drive/lib/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.js new file mode 100644 index 00000000000..402fd45f45f --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.js @@ -0,0 +1,38 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); + +const InvalidPayoutScriptError = require('./errors/InvalidPayoutScriptError'); + +/** + * + * @param {string} network + * @returns {getWithdrawPubKeyTypeFromPayoutScript} + */ +function getWithdrawPubKeyTypeFromPayoutScriptFactory(network) { + /** + * @typedef getWithdrawPubKeyTypeFromPayoutScript + * @param {Script} payoutScript + * @returns {number} + */ + function getWithdrawPubKeyTypeFromPayoutScript(payoutScript) { + const address = payoutScript.toAddress(network); + + if (address === false) { + throw new InvalidPayoutScriptError(payoutScript); + } + + let withdrawPubKeyType; + if (address.isPayToScriptHash()) { + withdrawPubKeyType = IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH; + } else if (address.isPayToPublicKeyHash()) { + withdrawPubKeyType = IdentityPublicKey.TYPES.ECDSA_HASH160; + } else { + throw new InvalidPayoutScriptError(payoutScript); + } + + return withdrawPubKeyType; + } + + return getWithdrawPubKeyTypeFromPayoutScript; +} + +module.exports = getWithdrawPubKeyTypeFromPayoutScriptFactory; diff --git a/packages/js-drive/lib/identity/masternode/handleNewMasternodeFactory.js b/packages/js-drive/lib/identity/masternode/handleNewMasternodeFactory.js new file mode 100644 index 00000000000..8303b2ab78a --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/handleNewMasternodeFactory.js @@ -0,0 +1,128 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const createOperatorIdentifier = require('./createOperatorIdentifier'); +const createVotingIdentifier = require('./createVotingIdentifier'); + +/** + * + * @param {DashPlatformProtocol} transactionalDpp + * @param {createMasternodeIdentity} createMasternodeIdentity + * @param {createRewardShareDocument} createRewardShareDocument + * @param {fetchTransaction} fetchTransaction + * @return {handleNewMasternode} + */ +function handleNewMasternodeFactory( + transactionalDpp, + createMasternodeIdentity, + createRewardShareDocument, + fetchTransaction, +) { + /** + * @typedef handleNewMasternode + * @param {SimplifiedMNListEntry} masternodeEntry + * @param {DataContract} dataContract + * @param {BlockInfo} blockInfo + * @return {Promise<{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }>} + */ + async function handleNewMasternode(masternodeEntry, dataContract, blockInfo) { + const createdEntities = []; + + const { extraPayload: proRegTxPayload } = await fetchTransaction(masternodeEntry.proRegTxHash); + + const proRegTxHash = Buffer.from(masternodeEntry.proRegTxHash, 'hex'); + + let payoutScript; + + if (masternodeEntry.payoutAddress) { + const payoutAddress = Address.fromString(masternodeEntry.payoutAddress); + payoutScript = new Script(payoutAddress); + } + + // Create a masternode identity + const masternodeIdentifier = Identifier.from( + proRegTxHash, + ); + + const ownerPublicKeyHash = Buffer.from(proRegTxPayload.keyIDOwner, 'hex').reverse(); + + createdEntities.push( + await createMasternodeIdentity( + blockInfo, + masternodeIdentifier, + ownerPublicKeyHash, + IdentityPublicKey.TYPES.ECDSA_HASH160, + payoutScript, + ), + ); + + // we need to crate reward shares only if it's enabled in proRegTx + + if (proRegTxPayload.operatorReward > 0) { + const operatorPubKey = Buffer.from(masternodeEntry.pubKeyOperator, 'hex'); + + let operatorPayoutScript; + if (masternodeEntry.operatorPayoutAddress) { + const operatorPayoutAddress = Address.fromString(masternodeEntry.operatorPayoutAddress); + operatorPayoutScript = new Script(operatorPayoutAddress); + } + + const operatorIdentifier = createOperatorIdentifier(masternodeEntry); + + createdEntities.push( + await createMasternodeIdentity( + blockInfo, + operatorIdentifier, + operatorPubKey, + IdentityPublicKey.TYPES.BLS12_381, + operatorPayoutScript, + ), + ); + + // Create a document in rewards data contract with percentage + const rewardShareDocument = await createRewardShareDocument( + dataContract, + masternodeIdentifier, + operatorIdentifier, + proRegTxPayload.operatorReward, + blockInfo, + ); + + if (rewardShareDocument) { + createdEntities.push(rewardShareDocument); + } + } + + const votingPubKeyHash = Buffer.from(proRegTxPayload.keyIDVoting, 'hex').reverse(); + + // don't need to create a separate Identity in case we don't have + // voting public key (keyIDVoting === keyIDOwner) + if (!votingPubKeyHash.equals(ownerPublicKeyHash)) { + const votingIdentifier = createVotingIdentifier(masternodeEntry); + + createdEntities.push( + await createMasternodeIdentity( + blockInfo, + votingIdentifier, + votingPubKeyHash, + IdentityPublicKey.TYPES.ECDSA_HASH160, + ), + ); + } + + return { + createdEntities, + updatedEntities: [], + removedEntities: [], + }; + } + + return handleNewMasternode; +} + +module.exports = handleNewMasternodeFactory; diff --git a/packages/js-drive/lib/identity/masternode/handleRemovedMasternodeFactory.js b/packages/js-drive/lib/identity/masternode/handleRemovedMasternodeFactory.js new file mode 100644 index 00000000000..97f298451a6 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/handleRemovedMasternodeFactory.js @@ -0,0 +1,63 @@ +/** + * @param {DocumentRepository} documentRepository + * + * @returns {handleRemovedMasternode} + */ +function handleRemovedMasternodeFactory( + documentRepository, +) { + /** + * @typedef {handleRemovedMasternode} + * + * @param {Identifier} masternodeIdentifier + * @param {DataContract} dataContract + * @param {BlockInfo} blockInfo + * @return {Promise<{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }>} + */ + async function handleRemovedMasternode(masternodeIdentifier, dataContract, blockInfo) { + // Delete documents belongs to masternode identity (ownerId) from rewards contract + // since max amount is 16, we can fetch all of them in one request + const removedEntities = []; + + const fetchedDocumentsResult = await documentRepository.find( + dataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', masternodeIdentifier], + ], + useTransaction: true, + }, + ); + + const documentsToDelete = fetchedDocumentsResult.getValue(); + + for (const document of documentsToDelete) { + await documentRepository.delete( + dataContract, + 'rewardShare', + document.getId(), + blockInfo, + { useTransaction: true }, + ); + + removedEntities.push( + document, + ); + } + + return { + createdEntities: [], + updatedEntities: [], + removedEntities, + }; + } + + return handleRemovedMasternode; +} + +module.exports = handleRemovedMasternodeFactory; diff --git a/packages/js-drive/lib/identity/masternode/handleUpdatedPubKeyOperatorFactory.js b/packages/js-drive/lib/identity/masternode/handleUpdatedPubKeyOperatorFactory.js new file mode 100644 index 00000000000..663e990662b --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/handleUpdatedPubKeyOperatorFactory.js @@ -0,0 +1,145 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const createOperatorIdentifier = require('./createOperatorIdentifier'); + +/** + * + * @param {DashPlatformProtocol} transactionalDpp + * @param {createMasternodeIdentity} createMasternodeIdentity + * @param {Identifier} masternodeRewardSharesContractId + * @param {createRewardShareDocument} createRewardShareDocument + * @param {DocumentRepository} documentRepository + * @param {IdentityStoreRepository} identityRepository + * @param {fetchTransaction} fetchTransaction + * @return {handleUpdatedPubKeyOperator} + */ +function handleUpdatedPubKeyOperatorFactory( + transactionalDpp, + createMasternodeIdentity, + masternodeRewardSharesContractId, + createRewardShareDocument, + documentRepository, + identityRepository, + fetchTransaction, +) { + /** + * @typedef handleUpdatedPubKeyOperator + * @param {SimplifiedMNListEntry} masternodeEntry + * @param {SimplifiedMNListEntry} previousMasternodeEntry + * @param {DataContract} dataContract + * @param {BlockInfo} blockInfo + * @return Promise<{ + * createdEntities: Array, + * removedEntities: Array, + * }> + */ + async function handleUpdatedPubKeyOperator( + masternodeEntry, + previousMasternodeEntry, + dataContract, + blockInfo, + ) { + const createdEntities = []; + const removedEntities = []; + + const { extraPayload: proRegTxPayload } = await fetchTransaction(masternodeEntry.proRegTxHash); + + // we need to crate reward shares only if it's enabled in proRegTx + if (proRegTxPayload.operatorReward === 0) { + return { + createdEntities, + removedEntities, + }; + } + + const proRegTxHash = Buffer.from(masternodeEntry.proRegTxHash, 'hex'); + const operatorPublicKey = Buffer.from(masternodeEntry.pubKeyOperator, 'hex'); + + const operatorIdentifier = createOperatorIdentifier(masternodeEntry); + + const operatorIdentityResult = await identityRepository.fetch( + operatorIdentifier, + { useTransaction: true }, + ); + + let operatorPayoutPubKey; + if (masternodeEntry.operatorPayoutAddress) { + const operatorPayoutAddress = Address.fromString(masternodeEntry.operatorPayoutAddress); + operatorPayoutPubKey = new Script(operatorPayoutAddress); + } + + // Create an identity for operator if there is no identity exist with the same ID + if (operatorIdentityResult.isNull()) { + createdEntities.push( + await createMasternodeIdentity( + blockInfo, + operatorIdentifier, + operatorPublicKey, + IdentityPublicKey.TYPES.BLS12_381, + operatorPayoutPubKey, + ), + ); + } + + // Create a document in rewards data contract with percentage defined + // in corresponding ProRegTx + + const masternodeIdentifier = Identifier.from( + proRegTxHash, + ); + + const rewardShareDocument = await createRewardShareDocument( + dataContract, + masternodeIdentifier, + operatorIdentifier, + proRegTxPayload.operatorReward, + blockInfo, + ); + + if (rewardShareDocument) { + createdEntities.push(rewardShareDocument); + } + + // Delete document from reward shares data contract with ID corresponding to the + // masternode identity (ownerId) and previous operator identity (payToId) + + const previousOperatorIdentifier = createOperatorIdentifier(previousMasternodeEntry); + + const previousDocumentsResult = await documentRepository.find( + dataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', masternodeIdentifier], + ['payToId', '==', previousOperatorIdentifier], + ], + useTransaction: true, + }, + ); + + if (!previousDocumentsResult.isEmpty()) { + const [previousDocument] = previousDocumentsResult.getValue(); + + await documentRepository.delete( + dataContract, + 'rewardShare', + previousDocument.getId(), + blockInfo, + { useTransaction: true }, + ); + + removedEntities.push(previousDocument); + } + + return { + createdEntities, + removedEntities, + }; + } + + return handleUpdatedPubKeyOperator; +} + +module.exports = handleUpdatedPubKeyOperatorFactory; diff --git a/packages/js-drive/lib/identity/masternode/handleUpdatedScriptPayoutFactory.js b/packages/js-drive/lib/identity/masternode/handleUpdatedScriptPayoutFactory.js new file mode 100644 index 00000000000..2d2fd03a103 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/handleUpdatedScriptPayoutFactory.js @@ -0,0 +1,117 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const identitySchema = require('@dashevo/dpp/schema/identity/identity.json'); + +/** + * + * @param {IdentityStoreRepository} identityRepository + * @param {IdentityPublicKeyStoreRepository} identityPublicKeyRepository + * @param {getWithdrawPubKeyTypeFromPayoutScript} getWithdrawPubKeyTypeFromPayoutScript + * @param {getPublicKeyFromPayoutScript} getPublicKeyFromPayoutScript + * @returns {handleUpdatedScriptPayout} + */ +function handleUpdatedScriptPayoutFactory( + identityRepository, + identityPublicKeyRepository, + getWithdrawPubKeyTypeFromPayoutScript, + getPublicKeyFromPayoutScript, +) { + /** + * @typedef handleUpdatedScriptPayout + * @param {Identifier} identityId + * @param {Script} newPayoutScript + * @param {BlockInfo} blockInfo + * @param {Script} [previousPayoutScript] + * @return {Promise<{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }>} + */ + async function handleUpdatedScriptPayout( + identityId, + newPayoutScript, + blockInfo, + previousPayoutScript, + ) { + const result = { + createdEntities: [], + updatedEntities: [], + removedEntities: [], + }; + + const identityResult = await identityRepository.fetch(identityId, { useTransaction: true }); + + const identity = identityResult.getValue(); + + const identityPublicKeys = identity + .getPublicKeys(); + + if (identityPublicKeys.length === identitySchema.properties.publicKeys.maxItems) { + // do not add new public key + return result; + } + + // disable previous + if (previousPayoutScript) { + const previousPubKeyType = getWithdrawPubKeyTypeFromPayoutScript(previousPayoutScript); + const previousPubKeyData = getPublicKeyFromPayoutScript( + previousPayoutScript, + previousPubKeyType, + ); + + const keyIds = identityPublicKeys + .filter((pk) => pk.getData().equals(previousPubKeyData)) + .map((pk) => pk.getId()); + + if (keyIds.length > 0) { + await identityPublicKeyRepository.disable( + identityId, + keyIds, + blockInfo.timeMs, + blockInfo, + { useTransaction: true }, + ); + + result.updatedEntities.push({ disabledKeys: keyIds }); + } + } + + // add new + const withdrawPubKeyType = getWithdrawPubKeyTypeFromPayoutScript(newPayoutScript); + const pubKeyData = getPublicKeyFromPayoutScript(newPayoutScript, withdrawPubKeyType); + + const newWithdrawalIdentityPublicKey = new IdentityPublicKey() + .setId(identity.getPublicKeyMaxId() + 1) + .setType(withdrawPubKeyType) + .setData(pubKeyData) + .setPurpose(IdentityPublicKey.PURPOSES.WITHDRAW) + .setReadOnly(true) + .setSecurityLevel(IdentityPublicKey.SECURITY_LEVELS.MASTER); + + await identityPublicKeyRepository.add( + identityId, + [newWithdrawalIdentityPublicKey], + blockInfo, + { useTransaction: true }, + ); + + result.createdEntities.push(newWithdrawalIdentityPublicKey); + + identity.setRevision(identity.getRevision() + 1); + + await identityRepository.updateRevision( + identityId, + identity.getRevision(), + blockInfo, + { useTransaction: true }, + ); + + result.updatedEntities.push(identity); + + return result; + } + + return handleUpdatedScriptPayout; +} + +module.exports = handleUpdatedScriptPayoutFactory; diff --git a/packages/js-drive/lib/identity/masternode/handleUpdatedVotingAddressFactory.js b/packages/js-drive/lib/identity/masternode/handleUpdatedVotingAddressFactory.js new file mode 100644 index 00000000000..b86e45db12e --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/handleUpdatedVotingAddressFactory.js @@ -0,0 +1,77 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const createVotingIdentifier = require('./createVotingIdentifier'); + +/** + * + * @param {IdentityStoreRepository} identityRepository + * @param {createMasternodeIdentity} createMasternodeIdentity + * @param {fetchTransaction} fetchTransaction + * @return {handleUpdatedVotingAddress} + */ +function handleUpdatedVotingAddressFactory( + identityRepository, + createMasternodeIdentity, + fetchTransaction, +) { + /** + * @typedef handleUpdatedVotingAddress + * @param {SimplifiedMNListEntry} masternodeEntry + * @param {BlockInfo} blockInfo + * @return {Promise<{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }>} + */ + async function handleUpdatedVotingAddress( + masternodeEntry, + blockInfo, + ) { + const result = { + createdEntities: [], + updatedEntities: [], + removedEntities: [], + }; + + const { extraPayload: proRegTxPayload } = await fetchTransaction(masternodeEntry.proRegTxHash); + + const ownerPublicKeyHash = Buffer.from(proRegTxPayload.keyIDOwner, 'hex').reverse(); + const votingPubKeyHash = Buffer.from(proRegTxPayload.keyIDVoting, 'hex').reverse(); + + // don't need to create a separate Identity in case we don't have + // public key used for proposal voting (keyIDVoting === keyIDOwner) + if (ownerPublicKeyHash.equals(votingPubKeyHash)) { + return result; + } + + // Create a voting identity if there is no identity exist with the same ID + const votingIdentifier = createVotingIdentifier(masternodeEntry); + + const votingIdentityResult = await identityRepository.fetch( + votingIdentifier, + { useTransaction: true }, + ); + + // Create an identity for operator if there is no identity exist with the same ID + if (votingIdentityResult.isNull()) { + const votingAddress = Address.fromString(masternodeEntry.votingAddress); + const votingPublicKeyHash = votingAddress.hashBuffer; + + result.createdEntities.push( + await createMasternodeIdentity( + blockInfo, + votingIdentifier, + votingPublicKeyHash, + IdentityPublicKey.TYPES.ECDSA_HASH160, + ), + ); + } + + return result; + } + + return handleUpdatedVotingAddress; +} + +module.exports = handleUpdatedVotingAddressFactory; diff --git a/packages/js-drive/lib/identity/masternode/synchronizeMasternodeIdentitiesFactory.js b/packages/js-drive/lib/identity/masternode/synchronizeMasternodeIdentitiesFactory.js new file mode 100644 index 00000000000..0e288f59fe0 --- /dev/null +++ b/packages/js-drive/lib/identity/masternode/synchronizeMasternodeIdentitiesFactory.js @@ -0,0 +1,258 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const createOperatorIdentifier = require('./createOperatorIdentifier'); + +/** + * + * @param result {{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }} + * @param newData {{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }} + * @return {{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * }} + */ +function mergeEntities(result, newData) { + return { + ...result, + createdEntities: result.createdEntities.concat(newData.createdEntities), + updatedEntities: result.updatedEntities.concat(newData.updatedEntities), + removedEntities: result.removedEntities.concat(newData.removedEntities), + }; +} + +/** + * + * @param {DataContractStoreRepository} dataContractRepository + * @param {SimplifiedMasternodeList} simplifiedMasternodeList + * @param {Identifier} masternodeRewardSharesContractId + * @param {handleNewMasternode} handleNewMasternode + * @param {handleUpdatedPubKeyOperator} handleUpdatedPubKeyOperator + * @param {handleRemovedMasternode} handleRemovedMasternode + * @param {handleUpdatedScriptPayout} handleUpdatedScriptPayout + * @param {handleUpdatedVotingAddress} handleUpdatedVotingAddress + * @param {number} smlMaxListsLimit + * @param {LastSyncedCoreHeightRepository} lastSyncedCoreHeightRepository + * @param {fetchSimplifiedMNList} fetchSimplifiedMNList + * @return {synchronizeMasternodeIdentities} + */ +function synchronizeMasternodeIdentitiesFactory( + dataContractRepository, + simplifiedMasternodeList, + masternodeRewardSharesContractId, + handleNewMasternode, + handleUpdatedPubKeyOperator, + handleRemovedMasternode, + handleUpdatedScriptPayout, + handleUpdatedVotingAddress, + smlMaxListsLimit, + lastSyncedCoreHeightRepository, + fetchSimplifiedMNList, +) { + let lastSyncedCoreHeight = 0; + + /** + * @typedef synchronizeMasternodeIdentities + * @param {number} coreHeight + * @param {BlockInfo} blockInfo + * @return {Promise<{ + * createdEntities: Array, + * updatedEntities: Array, + * removedEntities: Array, + * fromHeight: number, + * toHeight: number, + * }>} + */ + async function synchronizeMasternodeIdentities(coreHeight, blockInfo) { + let result = { + createdEntities: [], + updatedEntities: [], + removedEntities: [], + }; + + if (!lastSyncedCoreHeight) { + const lastSyncedHeightResult = await lastSyncedCoreHeightRepository.fetch({ + useTransaction: true, + }); + + lastSyncedCoreHeight = lastSyncedHeightResult.getValue() || 0; + } + + let newMasternodes; + let previousMNList = []; + + const currentMNList = simplifiedMasternodeList.getStore() + .getSMLbyHeight(coreHeight) + .mnList; + + const dataContractResult = await dataContractRepository.fetch( + masternodeRewardSharesContractId, + { + useTransaction: true, + }, + ); + + const dataContract = dataContractResult.getValue(); + + if (lastSyncedCoreHeight === 0) { + // Create identities for all masternodes on the first sync + newMasternodes = currentMNList; + } else { + // simplifiedMasternodeList contains sml only for the last `smlMaxListsLimit` number of blocks + if (coreHeight - lastSyncedCoreHeight >= smlMaxListsLimit) { + // get diff directly from core + ({ mnList: previousMNList } = await fetchSimplifiedMNList(1, lastSyncedCoreHeight)); + } else { + previousMNList = simplifiedMasternodeList.getStore() + .getSMLbyHeight(lastSyncedCoreHeight) + .mnList; + } + + // Get the difference between last sync and requested core height + newMasternodes = currentMNList.filter((currentMnListEntry) => ( + !previousMNList.find((previousMnListEntry) => ( + previousMnListEntry.proRegTxHash === currentMnListEntry.proRegTxHash + )) + )); + + // Update operator identities (PubKeyOperator is changed) + for (const mnEntry of currentMNList) { + const previousMnEntry = previousMNList.find((previousMnListEntry) => ( + previousMnListEntry.proRegTxHash === mnEntry.proRegTxHash + && previousMnListEntry.pubKeyOperator !== mnEntry.pubKeyOperator + )); + + if (previousMnEntry) { + const affectedEntities = await handleUpdatedPubKeyOperator( + mnEntry, + previousMnEntry, + dataContract, + blockInfo, + ); + + result = mergeEntities(result, affectedEntities); + } + + const previousVotingMnEntry = previousMNList.find((previousMnListEntry) => ( + previousMnListEntry.proRegTxHash === mnEntry.proRegTxHash + && previousMnListEntry.votingAddress !== mnEntry.votingAddress + )); + + if (previousVotingMnEntry) { + const affectedEntities = await handleUpdatedVotingAddress( + mnEntry, + blockInfo, + ); + + result = mergeEntities(result, affectedEntities); + } + + if (mnEntry.payoutAddress) { + const mnEntryWithChangedPayoutAddress = previousMNList.find((previousMnListEntry) => ( + previousMnListEntry.proRegTxHash === mnEntry.proRegTxHash + && previousMnListEntry.payoutAddress !== mnEntry.payoutAddress + )); + + if (mnEntryWithChangedPayoutAddress) { + const newPayoutScript = new Script(Address.fromString(mnEntry.payoutAddress)); + const previousPayoutScript = mnEntryWithChangedPayoutAddress.payoutAddress + ? new Script(Address.fromString(mnEntryWithChangedPayoutAddress.payoutAddress)) + : undefined; + + const affectedEntities = await handleUpdatedScriptPayout( + Identifier.from(Buffer.from(mnEntry.proRegTxHash, 'hex')), + newPayoutScript, + blockInfo, + previousPayoutScript, + ); + + result = mergeEntities(result, affectedEntities); + } + } + + if (mnEntry.operatorPayoutAddress) { + const mnEntryWithChangedOperatorPayoutAddress = previousMNList + .find((previousMnListEntry) => ( + previousMnListEntry.proRegTxHash === mnEntry.proRegTxHash + && previousMnListEntry.operatorPayoutAddress !== mnEntry.operatorPayoutAddress + )); + + if (mnEntryWithChangedOperatorPayoutAddress) { + const newOperatorPayoutAddress = Address.fromString(mnEntry.operatorPayoutAddress); + + const { operatorPayoutAddress } = mnEntryWithChangedOperatorPayoutAddress; + + const previousOperatorPayoutScript = operatorPayoutAddress + ? new Script(Address.fromString(operatorPayoutAddress)) + : undefined; + + const affectedEntities = await handleUpdatedScriptPayout( + createOperatorIdentifier(mnEntry), + new Script(newOperatorPayoutAddress), + blockInfo, + previousOperatorPayoutScript, + ); + + result = mergeEntities(result, affectedEntities); + } + } + } + } + + // Create identities and shares for new masternodes + + for (const newMasternodeEntry of newMasternodes) { + const affectedEntities = await handleNewMasternode( + newMasternodeEntry, + dataContract, + blockInfo, + ); + + result = mergeEntities(result, affectedEntities); + } + + // Remove masternode reward shares for invalid/removed masternodes + + const disappearedOrInvalidMasterNodes = previousMNList + .filter((previousMnListEntry) => + // eslint-disable-next-line max-len,implicit-arrow-linebreak + (!currentMNList.find((currentMnListEntry) => currentMnListEntry.proRegTxHash === previousMnListEntry.proRegTxHash))) + .concat(currentMNList.filter((currentMnListEntry) => !currentMnListEntry.isValid)); + + for (const masternodeEntry of disappearedOrInvalidMasterNodes) { + const masternodeIdentifier = Identifier.from( + Buffer.from(masternodeEntry.proRegTxHash, 'hex'), + ); + + const affectedEntities = await handleRemovedMasternode( + masternodeIdentifier, + dataContract, + blockInfo, + ); + + result = mergeEntities(result, affectedEntities); + } + + result.fromHeight = lastSyncedCoreHeight; + result.toHeight = coreHeight; + + await lastSyncedCoreHeightRepository.store(coreHeight, { + useTransaction: true, + }); + + return result; + } + + return synchronizeMasternodeIdentities; +} + +module.exports = synchronizeMasternodeIdentitiesFactory; diff --git a/packages/js-drive/lib/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.js b/packages/js-drive/lib/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.js new file mode 100644 index 00000000000..abd8b2c562e --- /dev/null +++ b/packages/js-drive/lib/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.js @@ -0,0 +1,76 @@ +const WITHDRAWALS_DOCUMENT_TYPE = 'withdrawal'; + +const WITHDRAWALS_STATUS_POOLED = 1; +const WITHDRAWALS_STATUS_BROADCASTED = 2; + +/** + * @param {DocumentRepository} documentRepository + * @param {fetchDocuments} fetchDocuments + * @param {Identifier} withdrawalsContractId + * + * @returns {updateWithdrawalTransactionIdAndStatus} + */ +function updateWithdrawalTransactionIdAndStatusFactory( + documentRepository, + fetchDocuments, + withdrawalsContractId, +) { + /** + * Update withdrawal transactionId and set status to BROADCASTED + * + * @typedef updateWithdrawalTransactionIdAndStatus + * + * @param {BlockInfo} blockInfo + * @param {number} coreChainLockedHeight + * @param {Object} transactionIdMap + * @param {Object} options + * + * @returns {Promise} + */ + async function updateWithdrawalTransactionIdAndStatus( + blockInfo, + coreChainLockedHeight, + transactionIdMap, + options, + ) { + const originalTransactionIds = Object.keys(transactionIdMap).map((key) => Buffer.from(key, 'hex')); + + if (originalTransactionIds.length === 0) { + return; + } + + const fetchOptions = { + where: [ + ['status', '==', WITHDRAWALS_STATUS_POOLED], + ['transactionId', 'in', originalTransactionIds], + ], + orderBy: [ + ['transactionId', 'asc'], + ], + ...options, + }; + + const documents = await fetchDocuments( + withdrawalsContractId, + WITHDRAWALS_DOCUMENT_TYPE, + fetchOptions, + ); + + for (const document of documents) { + const originalTransactionIdHex = document.get('transactionId').toString('hex'); + + const updatedTransactionId = transactionIdMap[originalTransactionIdHex]; + + document.set('transactionId', updatedTransactionId); + document.set('transactionSignHeight', coreChainLockedHeight); + document.set('status', WITHDRAWALS_STATUS_BROADCASTED); + document.setRevision(document.getRevision() + 1); + + await documentRepository.update(document, blockInfo, options); + } + } + + return updateWithdrawalTransactionIdAndStatus; +} + +module.exports = updateWithdrawalTransactionIdAndStatusFactory; diff --git a/packages/js-drive/lib/storage/GroveDBStore.js b/packages/js-drive/lib/storage/GroveDBStore.js new file mode 100644 index 00000000000..35a1eb474d9 --- /dev/null +++ b/packages/js-drive/lib/storage/GroveDBStore.js @@ -0,0 +1,519 @@ +const { createHash } = require('crypto'); +const StorageResult = require('./StorageResult'); + +class GroveDBStore { + /** + * @param {Drive} rsDrive + * @param {Object} [logger] + */ + constructor(rsDrive, logger = undefined) { + this.rsDrive = rsDrive; + this.db = rsDrive.getGroveDB(); + this.logger = logger; + } + + /** + * Store a key + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Buffer} value + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.skipIfExists=false] + * + * @return {Promise>} + */ + async put(path, key, value, options = {}) { + const method = options.skipIfExists ? 'insertIfNotExists' : 'insert'; + + try { + await this.db[method]( + path, + key, + { + type: 'item', + value, + }, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + path: path.map((segment) => segment.toString('hex')), + pathHash: createHash('sha256') + .update( + path.reduce((segment, buffer) => Buffer.concat([segment, buffer]), Buffer.alloc(0)), + ).digest('hex'), + key: key.toString('hex'), + value: value.toString('hex'), + valueHash: createHash('sha256') + .update(value) + .digest('hex'), + useTransaction: Boolean(options.useTransaction), + type: 'item', + method, + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'put'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Store a reference to the specified key + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Buffer[]} referencePath + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.skipIfExists=false] + * @return {Promise>} + */ + async putReference(path, key, referencePath, options = {}) { + const method = options.skipIfExists ? 'insertIfNotExists' : 'insert'; + + try { + await this.db[method]( + path, + key, + { + type: 'reference', + value: { + type: 'absolutePathReference', + path: referencePath, + }, + }, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + path: path.map((segment) => segment.toString('hex')), + pathHash: createHash('sha256') + .update( + path.reduce((segment, buffer) => Buffer.concat([segment, buffer]), Buffer.alloc(0)), + ) + .digest('hex'), + key: key.toString('hex'), + value: referencePath.map((segment) => segment.toString('hex')), + valueHash: createHash('sha256') + .update( + referencePath.reduce((segment, buffer) => ( + Buffer.concat([segment, buffer]) + ), Buffer.alloc(0)), + ) + .digest('hex'), + useTransaction: Boolean(options.useTransaction), + type: 'reference', + method, + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'putReference'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Create empty key + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.skipIfExists=false] + * @return {Promise>} + */ + async createTree(path, key, options = { }) { + const method = options.skipIfExists ? 'insertIfNotExists' : 'insert'; + + try { + await this.db[method]( + path, + key, + { + type: 'tree', + }, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + path: path.map((segment) => segment.toString('hex')), + pathHash: createHash('sha256') + .update( + path.reduce((segment, buffer) => Buffer.concat([segment, buffer]), Buffer.alloc(0)), + ).digest('hex'), + key: key.toString('hex'), + value: Buffer.alloc(32).toString('hex'), + valueHash: createHash('sha256') + .update(Buffer.alloc(32)) + .digest('hex'), + useTransaction: Boolean(options.useTransaction), + type: 'tree', + method, + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'createTree'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Get a value by key + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {number} [options.predictedValueSize] + * @return {Promise>} + */ + async get(path, key, options = { }) { + let type; + let value; + + try { + ({ + type, + value, + } = await this.db.get( + path, + key, + options.useTransaction || false, + )); + } catch (e) { + if ( + e.message.startsWith('grovedb: path key not found') + || e.message.startsWith('grovedb: path not found') + ) { + return new StorageResult( + null, + [], + ); + } + + throw e; + } + + if (type === undefined) { + return new StorageResult( + null, + [], + ); + } + + if (type !== 'item') { + throw new Error('Key should point to item element type'); + } + + return new StorageResult( + value, + [], + ); + } + + /** + * Query keys and values + * + * @param {PathQuery} query + * @param {Object} [options] + * @param {boolean} [options.skipCache=false] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + */ + async query(query, options = { }) { + let items; + + try { + [items] = await this.db.query( + query, + options.skipCache || false, + options.useTransaction || false, + ); + } catch (e) { + if ( + e.message.startsWith('grovedb: path key not found') + || e.message.startsWith('grovedb: path not found') + ) { + return new StorageResult( + null, + [], + ); + } + + throw e; + } + + return new StorageResult( + items, + [], + ); + } + + /** + * Prove query + * + * @param {PathQuery} query + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + * */ + async proveQuery(query, options = {}) { + const proof = await this.db.proveQuery( + query, + options.useTransaction || false, + ); + + return new StorageResult( + proof, + [], + ); + } + + /** + * Prove many queries + * + * @param {PathQuery[]} queries + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + * */ + async proveQueryMany(queries, options = {}) { + const proof = await this.db.proveQueryMany( + queries, + options.useTransaction || false, + ); + + return new StorageResult( + proof, + [], + ); + } + + /** + * Delete value by key + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Promise>} + */ + async delete(path, key, options = {}) { + try { + await this.db.delete( + path, + key, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + path: path.map((segment) => segment.toString('hex')), + pathHash: createHash('sha256') + .update( + path.reduce((segment, buffer) => Buffer.concat([segment, buffer]), Buffer.alloc(0)), + ).digest('hex'), + key: key.toString('hex'), + useTransaction: Boolean(options.useTransaction), + method: 'delete', + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'delete'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Get auxiliary value by key + * + * @param {Buffer} key + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.predictedValueSize] + * @return {Promise>} + */ + async getAux(key, options = {}) { + let result = null; + + try { + result = await this.db.getAux( + key, + options.useTransaction || false, + ); + } catch (e) { + if (e.message.startsWith('grovedb: path key not found')) { + return new StorageResult( + null, + [], + ); + } + + throw e; + } + + return new StorageResult( + result, + [], + ); + } + + /** + * Store auxiliary value by key + * + * @param {Buffer} key + * @param {Buffer} value + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @return {Promise>} + */ + async putAux(key, value, options = {}) { + try { + await this.db.putAux( + key, + value, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + key: key.toString('hex'), + value: value.toString('hex'), + valueHash: createHash('sha256') + .update(value) + .digest('hex'), + useTransaction: Boolean(options.useTransaction), + method: 'putAux', + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'putAux'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Delete auxiliary value by key + * + * @param {Buffer} key + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @param {boolean} [options.dryRun=false] + * @return {Promise>} + */ + async deleteAux(key, options = {}) { + try { + await this.db.deleteAux( + key, + options.useTransaction || false, + ); + } finally { + if (this.logger) { + this.logger.info({ + key: key.toString('hex'), + useTransaction: Boolean(options.useTransaction), + method: 'deleteAux', + appHash: (await this.getRootHash(options)).toString('hex'), + }, 'deleteAux'); + } + } + + return new StorageResult( + undefined, + [], + ); + } + + /** + * Get tree root hash + * + * @param {Object} [options] + * @param {boolean} [options.useTransaction=false] + * @return {Buffer} + */ + async getRootHash(options = {}) { + return this.db.getRootHash(options.useTransaction || false); + } + + /** + * @return {Promise} + */ + async startTransaction() { + return this.db.startTransaction(); + } + + /** + * @return {Promise} + */ + async isTransactionStarted() { + return this.db.isTransactionStarted(); + } + + /** + * Rollback transaction to this initial state when it was created + * + * @returns {Promise} + */ + async rollbackTransaction() { + return this.db.rollbackTransaction(); + } + + /** + * @return {Promise} + */ + async commitTransaction() { + return this.db.commitTransaction(); + } + + /** + * @return {Promise} + */ + async abortTransaction() { + return this.db.abortTransaction(); + } + + /** + * @return {Drive} + */ + getDrive() { + return this.rsDrive; + } + + /** + * @returns {GroveDB} + */ + getDB() { + return this.db; + } + + /** + * @param {GroveDB} db + */ + setDB(db) { + this.db = db; + } +} + +module.exports = GroveDBStore; diff --git a/packages/js-drive/lib/storage/StorageResult.js b/packages/js-drive/lib/storage/StorageResult.js new file mode 100644 index 00000000000..fb548802ef5 --- /dev/null +++ b/packages/js-drive/lib/storage/StorageResult.js @@ -0,0 +1,68 @@ +/** + * @template T + */ +class StorageResult { + /** + * @type {T} + */ + #value; + + /** + * @type {AbstractOperation[]} + */ + #operations; + + /** + * @template T + * @param {T} value + * @param {AbstractOperation[]} operations + */ + constructor(value, operations = []) { + this.#value = value; + this.#operations = operations; + } + + /** + * @return {T} + */ + getValue() { + return this.#value; + } + + /** + * @param {T} value + */ + setValue(value) { + this.#value = value; + } + + /** + * @return {AbstractOperation[]} + */ + getOperations() { + return this.#operations; + } + + /** + * @return {boolean} + */ + isNull() { + return this.#value === null || this.#value === undefined; + } + + /** + * @return {boolean} + */ + isEmpty() { + return this.isNull() || (Array.isArray(this.#value) && this.#value.length === 0); + } + + /** + * @param {AbstractOperation} operation + */ + addOperation(...operation) { + this.#operations.push(...operation); + } +} + +module.exports = StorageResult; diff --git a/packages/js-drive/lib/test/.eslintrc b/packages/js-drive/lib/test/.eslintrc new file mode 100644 index 00000000000..4c2b11fe817 --- /dev/null +++ b/packages/js-drive/lib/test/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" + } +} diff --git a/packages/js-drive/lib/test/bootstrap.js b/packages/js-drive/lib/test/bootstrap.js new file mode 100644 index 00000000000..2393794a2de --- /dev/null +++ b/packages/js-drive/lib/test/bootstrap.js @@ -0,0 +1,78 @@ +const path = require('path'); +const dotenvSafe = require('dotenv-safe'); +const dotenvExpand = require('dotenv-expand'); +const { expect, use } = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); +const dirtyChai = require('dirty-chai'); +const chaiAsPromised = require('chai-as-promised'); +const chaiString = require('chai-string'); +const DashCoreOptions = require('@dashevo/dp-services-ctl/lib/services/dashCore/DashCoreOptions'); + +use(sinonChai); +use(chaiAsPromised); +use(chaiString); +use(dirtyChai); + +process.env.NODE_ENV = 'test'; + +// Workaround for dotenv-safe +if (process.env.INITIAL_CORE_CHAINLOCKED_HEIGHT === undefined) { + process.env.INITIAL_CORE_CHAINLOCKED_HEIGHT = 0; +} +if (process.env.DPNS_MASTER_PUBLIC_KEY === undefined) { + process.env.DPNS_MASTER_PUBLIC_KEY = '037d074eb00aa286c438b5d12b7c6ca25104d61b03e6601b6ace7d5eb036fbbc23'; +} +if (process.env.DPNS_SECOND_PUBLIC_KEY === undefined) { + process.env.DPNS_SECOND_PUBLIC_KEY = '025852df611a228b0e7fbccff4eaa117500ead84622809ea7fc05dcf6d2dbbc1d4'; +} +if (process.env.DASHPAY_MASTER_PUBLIC_KEY === undefined) { + process.env.DASHPAY_MASTER_PUBLIC_KEY = '02c571ff0cdb72634de4fd23f40c4ed530b3d31defc987a55479d65e7e8c1e249a'; +} +if (process.env.DASHPAY_SECOND_PUBLIC_KEY === undefined) { + process.env.DASHPAY_SECOND_PUBLIC_KEY = '03834f92a2132e55273cb713e855a6fbf2179704830c19094470720d6434ce4547'; +} +if (process.env.FEATURE_FLAGS_MASTER_PUBLIC_KEY === undefined) { + process.env.FEATURE_FLAGS_MASTER_PUBLIC_KEY = '022393486382a5bb262856c49f869827a1c79a3a3c38747f3cb8c32dd7bd191797'; +} +if (process.env.FEATURE_FLAGS_SECOND_PUBLIC_KEY === undefined) { + process.env.FEATURE_FLAGS_SECOND_PUBLIC_KEY = '03c10ac08a77dfdfcdc706ea43d9651ac0866181b835411587eca4d2d5477f39f7'; +} +if (process.env.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY === undefined) { + process.env.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY = '0333a42c628e8c93ce0386856f8f2239c84bf816cf8590716c7891fdc981a4df0b'; +} +if (process.env.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY === undefined) { + process.env.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY = '03a3002856ad91662dc34b6650a2c7f8b2726c947a419e0b880fb3acc38763a271'; +} +if (process.env.WITHDRAWALS_MASTER_PUBLIC_KEY === undefined) { + process.env.WITHDRAWALS_MASTER_PUBLIC_KEY = '02ee6d9b15ed1c310535297739e69973406dbd1a679be7fad2bdd2e08685033077'; +} +if (process.env.WITHDRAWALS_SECOND_PUBLIC_KEY === undefined) { + process.env.WITHDRAWALS_SECOND_PUBLIC_KEY = '02711ce1fedafde67694d771950c474fe300e464d4f67c2a6447f9b61e90fa01f3'; +} + +const dotenvConfig = dotenvSafe.config({ + path: path.resolve(__dirname, '..', '..', '.env'), +}); + +dotenvExpand(dotenvConfig); + +DashCoreOptions.setDefaultCustomOptions({ + container: { + image: 'dashpay/dashd:18.1.0-rc.1', + }, +}); + +beforeEach(function beforeEach() { + if (!this.sinon) { + this.sinon = sinon.createSandbox(); + } else { + this.sinon.restore(); + } +}); + +afterEach(function afterEach() { + this.sinon.restore(); +}); + +global.expect = expect; diff --git a/packages/js-drive/lib/test/createTestDIContainer.js b/packages/js-drive/lib/test/createTestDIContainer.js new file mode 100644 index 00000000000..ade6458f5e3 --- /dev/null +++ b/packages/js-drive/lib/test/createTestDIContainer.js @@ -0,0 +1,30 @@ +const createDIContainer = require('../createDIContainer'); + +async function createTestDIContainer(dashCore = undefined) { + let coreOptions = {}; + if (dashCore) { + coreOptions = { + CORE_JSON_RPC_HOST: '127.0.0.1', + CORE_JSON_RPC_PORT: dashCore.options.getRpcPort(), + CORE_JSON_RPC_USERNAME: dashCore.options.getRpcUser(), + CORE_JSON_RPC_PASSWORD: dashCore.options.getRpcPassword(), + }; + } + + const container = createDIContainer({ + ...process.env, + GROVEDB_LATEST_FILE: './db/latest_state_test', + EXTERNAL_STORE_LEVEL_DB_FILE: './db/external_leveldb_test', + ...coreOptions, + }); + + const dpp = container.resolve('dpp'); + const transactionalDpp = container.resolve('transactionalDpp'); + + await dpp.initialize(); + await transactionalDpp.initialize(); + + return container; +} + +module.exports = createTestDIContainer; diff --git a/packages/js-drive/lib/test/fixtures/createDataContractDocuments.js b/packages/js-drive/lib/test/fixtures/createDataContractDocuments.js new file mode 100644 index 00000000000..42aa2c7976c --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/createDataContractDocuments.js @@ -0,0 +1,28 @@ +const dataContractMetaSchema = require('@dashevo/dpp/schema/dataContract/dataContractMeta.json'); +const createIndices = require('./createIndices'); +const createProperties = require('./createProperties'); + +/** + * + * @param {number} count + * @returns {Object} + */ +function createDataContractDocuments(count = 2) { + const documents = {}; + + for (let i = 0; i < count; i++) { + documents[`doc${i}`] = { + type: 'object', + indices: createIndices(dataContractMetaSchema.$defs.documentProperties.maxProperties, true), + properties: createProperties(dataContractMetaSchema.$defs.documentProperties.maxProperties, { + type: 'string', + maxLength: 63, + }), + additionalProperties: false, + }; + } + + return documents; +} + +module.exports = createDataContractDocuments; diff --git a/packages/js-drive/lib/test/fixtures/createIndices.js b/packages/js-drive/lib/test/fixtures/createIndices.js new file mode 100644 index 00000000000..9cca689b728 --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/createIndices.js @@ -0,0 +1,36 @@ +/** + * @param {number} count + * @param {boolean} [unique=false] + */ +function createIndices(count, unique = false) { + const indices = []; + + const indexCount = (count < 10 ? count : 10); + + let propertyIndex = 0; + + const basePropertyCount = Math.floor(count / indexCount); + const propertyLeftovers = count % indexCount; + + for (let i = 0; i < indexCount; i++) { + const properties = []; + + for (let x = 0; x < basePropertyCount + ((i < propertyLeftovers) ? 1 : 0); x++) { + const name = `property${propertyIndex}`; + + propertyIndex++; + + properties.push({ [name]: 'asc' }); + } + + indices.push({ + name: `index${i}`, + properties, + unique: unique && i < 3, + }); + } + + return indices; +} + +module.exports = createIndices; diff --git a/packages/js-drive/lib/test/fixtures/createProperties.js b/packages/js-drive/lib/test/fixtures/createProperties.js new file mode 100644 index 00000000000..11ba4876908 --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/createProperties.js @@ -0,0 +1,17 @@ +/** + * @param {number} count + * @param {Object} subSchema + */ +function createProperties(count, subSchema) { + const properties = {}; + + for (let i = 0; i < count; i++) { + const name = `property${i}`; + + properties[name] = subSchema; + } + + return properties; +} + +module.exports = createProperties; diff --git a/packages/js-drive/lib/test/fixtures/getBlockExecutionContextObjectFixture.js b/packages/js-drive/lib/test/fixtures/getBlockExecutionContextObjectFixture.js new file mode 100644 index 00000000000..ada7cbc0d35 --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/getBlockExecutionContextObjectFixture.js @@ -0,0 +1,90 @@ +const { + tendermint: { + abci: { + CommitInfo, + ValidatorSetUpdate, + }, + types: { + ConsensusParams, + }, + }, +} = require('@dashevo/abci/types'); + +const pino = require('pino'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const { hash } = require('@dashevo/dpp/lib/util/hash'); + +/** + * @param {DataContract} [dataContract] + * @return {{ + * dataContracts: Object[], + * lastCommitInfo, + * coreChainLockedHeight: number, + * height: number, + * version: number, + * timeMs: number, + * validTxs: number, + * contextLogger: Logger, + * withdrawalTransactionsMap: Object, + * round: number, + * }} + */ +function getBlockExecutionContextObjectFixture(dataContract = getDataContractFixture()) { + const lastCommitInfo = new CommitInfo({ + quorumHash: Buffer.from('000003c60ecd9576a05a7e15d93baae18729cb4477d44246093bd2cf8d4f53d8', 'hex'), + blockSignature: Buffer.from('003657bb44d74c371d14485117de43313ca5c2848f3622d691c2b1bf3576a64bdc2538efab24854eb82ae7db38482dbd15a1cb3bc98e55173817c9d05c86e47a5d67614a501414aae6dd1565e59422d1d77c41ae9b38de34ecf1e9f778b2a97b', 'hex'), + }); + + const version = { + app: '1', + block: '2', + }; + + const [txOneBytes, txTwoBytes] = [ + Buffer.alloc(32, 0), + Buffer.alloc(32, 1), + ]; + + return { + dataContracts: [dataContract.toObject()], + lastCommitInfo: CommitInfo.toObject(lastCommitInfo), + height: 10, + proposedAppVersion: 42, + coreChainLockedHeight: 10, + version, + contextLogger: pino(), + epochInfo: { + height: 1, + timeMs: 100, + epoch: 0, + }, + timeMs: Date.now(), + withdrawalTransactionsMap: { + [hash(txOneBytes).toString('hex')]: txOneBytes, + [hash(txTwoBytes).toString('hex')]: txTwoBytes, + }, + round: 42, + prepareProposalResult: { + appHash: Buffer.alloc(32, 3), + txResults: new Array(3).fill({ code: 0 }), + consensusParamUpdates: new ConsensusParams({ + block: { + maxBytes: 1, + maxGas: 2, + }, + evidence: { + maxAgeDuration: null, + maxAgeNumBlocks: 1, + maxBytes: 2, + }, + version: { + appVersion: 1, + }, + }), + validatorSetUpdate: new ValidatorSetUpdate(), + }, + }; +} + +module.exports = getBlockExecutionContextObjectFixture; diff --git a/packages/js-drive/lib/test/fixtures/getSmlFixture.js b/packages/js-drive/lib/test/fixtures/getSmlFixture.js new file mode 100644 index 00000000000..8fed978d62e --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/getSmlFixture.js @@ -0,0 +1,87 @@ +module.exports = function getSmlFixture() { + return [ + { + baseBlockHash: '36d8cdedb8b2a0a8e893a26cbfd2234fb7eb3deee722f12f9f22605fba5bca91', + blockHash: '12d4bffbb6323da88ac6d35d742e33dec01d7f324dd44a5c5758cbf64d3ef024', + cbTxMerkleTree: '0100000001100322c9aa95701ed2b345e2a65e42d50e126ee6e6d2e2d7e4480140ab259bf40101', + cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05029b040101ffffffff02aa8b4e1e030000001976a914331d5c5153b6599e256c61fc1da7905b43fbbd6788aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac000000004602009b040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', + deletedMNs: [], + mnList: [{ + proRegTxHash: 'f5ec54aed788c434da2fc535ea6b125ec6fc54e58bc0a00a005d1a8d5e477a90', confirmedHash: '53125505b0e9d11b371cf3e12c92d164296dfa215fde6201d28ea44bed992187', service: '192.168.65.2:20101', pubKeyOperator: '951a3208ba531ea75aedd2dc0a9efc75f2c4d9492f1ee0a989b593bcd9722b1a101774d80a426552a9f91d24eb55af6e', votingAddress: 'yYH1rgZsgvkmT8bSSSw1cKCjyVPnFpTBCw', isValid: true, nVersion: 2, nType: 0, payoutAddress: 'yZv7wf496sjqJVgnEUAtYKozWQhVpoHRh9', + }, { + proRegTxHash: 'a2c9b34ef525271d84f70a0d4d2c107e8a2f81cd4d8256dc7b3911ed253d5611', confirmedHash: '29ff8afb463604ba7d984b483e92dfefa4e80e12de3acae6d75f9b910df9eab6', service: '192.168.65.2:20201', pubKeyOperator: 'a5ad6d8cad7b233210b718a5fc9ec3cea18aeebe38b2e3122deb581e430aa28875fe7336c283871db42808f8d4107745', votingAddress: 'yRXtaRmQ7LCmT5XcgzQdLwPEf31dycBaeY', isValid: true, nVersion: 2, nType: 0, payoutAddress: 'yiBP17AgHGit2TE9p9FpHEh4ouowNSxMxg', + }, { + proRegTxHash: '1c81a5faa2c0e0d96eb59c58a10fcbc87f431bb6cd880d960b43b269e682d2d2', confirmedHash: '03cc2acc135ab51304d3cff42215c7a8041902fa3f19451d5562a03b38143e8f', service: '192.168.65.2:20001', pubKeyOperator: '96f83eedc8a7b87663e591987f051ce341a6fb88989322c64bbbf56d205e4e77d2cb7d839d8b4106a8a1f5d5cf7cfa57', votingAddress: 'ybJfuKs59MJWkPEnS8qNmtvdisHrCy7Njn', isValid: true, nVersion: 2, nType: 0, payoutAddress: 'yd3AnRA5YRtN1jsv7jqUK8egA6Mk9e8HoS', + }], + nVersion: 2, + deletedQuorums: [], + newQuorums: [], + merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', + merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '12d4bffbb6323da88ac6d35d742e33dec01d7f324dd44a5c5758cbf64d3ef024', blockHash: '4fa3b35537280a5ff0f8adbc966960bb0cd95ecb4782444ec9f9b62d72afd571', cbTxMerkleTree: '01000000010cc244c3b969a94ab809d87c2f31c9d9feeae5798a4afcd80191f605580367770101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05029c040101ffffffff02aa8b4e1e030000001976a91419dbb5e39a9c9bce01849dbbf4193929506e0f4588aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac000000004602009c040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '4fa3b35537280a5ff0f8adbc966960bb0cd95ecb4782444ec9f9b62d72afd571', blockHash: '62ec67e46a2c9710381e28f34c9c7f6605766b394cef156643ea906bb3b6701a', cbTxMerkleTree: '0100000001979a5826b0065b8dee4632c7f1d45c862c302d339e11656d58bdfc17b62b966e0101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05029d040101ffffffff02aa8b4e1e030000001976a914fbbc45c3fdbd26d51f8558f152b6f772d3260a7488aca28b4e1e030000001976a9149528697532e5e020c44aebaa1b6d0b66e239b3fe88ac000000004602009d040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '62ec67e46a2c9710381e28f34c9c7f6605766b394cef156643ea906bb3b6701a', blockHash: '0b00be86c561b54e4dfc661815ed107627fd7f29693b2a46f8c769c3504c16f1', cbTxMerkleTree: '01000000015d6ccc29f288527e8c800218385587382e2216fd2508058fb6e98fca91e08e6f0101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05029e040101ffffffff02aa8b4e1e030000001976a9142cb7b39eabcf81b31457806c9499a2d596f8a55788aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac000000004602009e040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '0b00be86c561b54e4dfc661815ed107627fd7f29693b2a46f8c769c3504c16f1', blockHash: '42adcd0b2b330c842754489a9f6cb1a0d1dcc6113229b14574696c5b87e6debe', cbTxMerkleTree: '0100000001ded16207cc61edee7129967c8ecf22de5de9c4035c63f72bf6582ff6aaa70c0e0101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05029f040101ffffffff02aa8b4e1e030000001976a914b87e86cab49d5ce4c6f2e3927f1f46acc2b0391188aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac000000004602009f040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '42adcd0b2b330c842754489a9f6cb1a0d1dcc6113229b14574696c5b87e6debe', blockHash: '22f50323777d6544e614ccd3bd7c4c52122974cf9fa34bf9330ddca7b76ded37', cbTxMerkleTree: '0100000001f09a49ac7c163ee6d92aee33d675665ef0f720720b5c84559ed1875ed99f5fc00101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a0040101ffffffff02aa8b4e1e030000001976a91405f5fd0cc306a3a826bd58b379429c44eb44b47c88aca28b4e1e030000001976a9149528697532e5e020c44aebaa1b6d0b66e239b3fe88ac00000000460200a0040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '22f50323777d6544e614ccd3bd7c4c52122974cf9fa34bf9330ddca7b76ded37', blockHash: '2ec79cb473fd23457f284c7635264cf5b4df7d6d232357f280a728c5beb28484', cbTxMerkleTree: '01000000014421d1126fef6d6dc7ba20db021397c73f5b702654b50fa6acc0c38712b799db0101', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a1040101ffffffff02aa8b4e1e030000001976a9143f17462c6fd90af0e1fb5dd53e404ded6f4c09a688aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac00000000460200a1040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '2ec79cb473fd23457f284c7635264cf5b4df7d6d232357f280a728c5beb28484', blockHash: '2482fb1720e54dea0243b7797ce5c3c44eb5ab9827916a97f6960e34210d4dc9', cbTxMerkleTree: '0500000004a6b029eb4dfa6fc655e0680e1e997e54fad8670ea332b7257118c7e960e5060ce2f09f1b5015b4eed4884246004df64d3f6d5fff1191b460a56801294036197bc9d3da79f4768cdb70282d9f96b82c345a082eb84229d4d3bd2fedb428770496d663ddda0ccd66f5cdd47a51403fe89c1d06982401c05c3c021c85995301e243010f', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a2040101ffffffff02aa8b4e1e030000001976a914d6dc361352264c01c1d439dab42cf67896971ced88aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac00000000460200a2040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e80000000000000000000000000000000000000000000000000000000000000000', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }, + { + baseBlockHash: '2482fb1720e54dea0243b7797ce5c3c44eb5ab9827916a97f6960e34210d4dc9', + blockHash: '52422c1f4ead389b73ca34fb786fab4c22663ef015e766010b635a99de993770', + cbTxMerkleTree: '0500000004bba36da8fd518b33d346625d330a924616b19ecf452f69bf200a4a7313299061a90f2604811e6794cace64f71ec8e5e11ad2e033272cfbbfb43c0fb522476148d5d2f1559de709b3e97569cb405476f46950bdc64dc7280d1734f0e0398f6002b3bd98c64208dc0cb3546e878e15e875f82a641c86cdf5a0fbe5d1146b90d767010f', + cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a3040101ffffffff02aa8b4e1e030000001976a91409bba8d0a9569d1332a98dcbecf1719165b01f1788aca28b4e1e030000001976a9149528697532e5e020c44aebaa1b6d0b66e239b3fe88ac00000000460200a3040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', + deletedMNs: [], + mnList: [], + nVersion: 2, + deletedQuorums: [], + newQuorums: [{ + version: 3, llmqType: 100, quorumHash: '53834140bf3774419d424b88de6e9d6e7bcea9e0b59c03110b561b8622a15f71', quorumIndex: 0, signersCount: 3, signers: '07', validMembersCount: 3, validMembers: '07', quorumPublicKey: '94ef4696184537701850d5b5ccc6d1e253cce668680327bea7329feb63f0de8db0cd85708f10cdeda33aaf17362a71a3', quorumVvecHash: 'e899b8cd254edc2389a40c114a5a75920e16e8ad29dc5da0a3cb1e228180ec4b', quorumSig: '936f0db22feee7e72d7a130635d25e64b1300595ff335b88b330271abc74c789f92248c26a9fef696e1eac72da7ac4f90620aa7aaadfdd0b4e923d1cc6dcc1aff99f8cd456e84fe2405bad614b79d9230f0f7df39f7d63e283443a2b734e0672', membersSig: '93970da789c60a2c070be0a57d7e25e29eff943581be9967aaa81d544f9ded4512182b03664a407b05de3a682c27b9a305499618bee7550329612a05aeddb591854b03fb23203d8ef1afbd9bb2ce3f874a8afa77ca69a12b4bc77d8d0e4d936f', + }, { + version: 3, llmqType: 102, quorumHash: '53834140bf3774419d424b88de6e9d6e7bcea9e0b59c03110b561b8622a15f71', quorumIndex: 0, signersCount: 3, signers: '07', validMembersCount: 3, validMembers: '07', quorumPublicKey: '902f14411145221b2c7333c9ea5b24b02771583b571b32496251b87cbf9f4421c50c7608d785ceae0e8e8774befb1bbe', quorumVvecHash: '1ff155a5a27490d92db5fba42bfaf8bfd0b73d651d80f19b76a1d1377f9901ab', quorumSig: 'a2cdd139c6a0be507f04712a1a38bc76942b8b8631ede0fa8797156576c4a82016316b5ed3ed8042dc1f77971f305f7306ad7333c761d9f3d4471a27ce964dd0ef66b8591b3f406eda25745466c31b9feb5a52e2fd3ee124142bc96579c1a853', membersSig: 'a9118d64f72443d525de207e64bcc02b1d26961de73f89a052c5c01d8fbfab18fecb29b9f714722c1eb4e61b3e58133507d5a065534a82df16dbd917ccad1b2c1175026a242a44f69713f5a9a5ef0b10956272a1b17880c7d1173e47e068f352', + }, { + version: 3, llmqType: 104, quorumHash: '53834140bf3774419d424b88de6e9d6e7bcea9e0b59c03110b561b8622a15f71', quorumIndex: 0, signersCount: 3, signers: '07', validMembersCount: 3, validMembers: '07', quorumPublicKey: 'aa1067497034c595f85f783a69be6d9bb1a139555e990ee6f60ddb2886ae52bff2c90f2669ea254204011940b8da92d3', quorumVvecHash: '79ac426c3cac22d0a8790df583424a319064f778df47bcedae9aa8cd00c09819', quorumSig: '97283c23faac0939a06ee1b959fe913eab816c3e1d924798ecf23094a1b711c7e8dd129e1ea082f995d40aa63d1eb7431402c306bc7d8a86105bbf07b6228f8c5ab76cab52c18a5c0d91d673afae199ca10e7dbb4f513bf47551f4b6d7e9a484', membersSig: 'b6712c9db9694da6986edd2c89b9ee912c08a06fad9ced811aae536f37eacf3196891ef9a0b319c0d27948b84cf42da00006a74daefb05e5b17995f2784f9ba2ceeedf9cbc205971d1d64401258cf9050da70d16af0533e9398188287e3dd165', + }], + merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', + merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '52422c1f4ead389b73ca34fb786fab4c22663ef015e766010b635a99de993770', blockHash: '401f9127185c81245d92ae9e93b2fc571e99bcdaf89404c61e1a918729a16a1f', cbTxMerkleTree: '04000000034bbfebdc0500a6403256d717c3d26c56334bd70175bf1c05675a247865bed851e277025a4cfb9b398ffaa5e7736d30bca29fae3cebf839d1ac94001f056b87f9d59d34522656a8ee3b2f4f0c7ee52d34f9c143665193bf3cd0ec6266945eb9d00107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a4040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac00000000460200a4040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '401f9127185c81245d92ae9e93b2fc571e99bcdaf89404c61e1a918729a16a1f', blockHash: '7618f37265698bac8944b075e259974e10844d4e03f8febc5d19c663369e27d4', cbTxMerkleTree: '0400000003e4c7d33cbf8f3577d7154391b4dbb7bf606202d97d2a7c119e61808f5274b0af8fcc5f70790cd0c9a2e3803fcbd368e80352f9a2dae07c6d91c98e29809c42b2d9cecbf65eadcd9d1c573181852bcbf5e2d54e166bc0a269585b776c4cdc129f0107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a5040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac00000000460200a5040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '7618f37265698bac8944b075e259974e10844d4e03f8febc5d19c663369e27d4', blockHash: '5c5a00c6bdfcbdfbf6d8bf97de5b3364701c2f7bbd62ea5a84611e746d662e85', cbTxMerkleTree: '0400000003631e7d56314d01d605f57a231fdc5c45ec8d41f9efa917426fae8da5891a34fd6189bf0e09922b2b865c5b520cd5bbb32530955d08b598d9c8776e213f66b5ad8c478bf186219418a2110f1ee300c2397559b9a2907aac8521653733b745fa2b0107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a6040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a9149528697532e5e020c44aebaa1b6d0b66e239b3fe88ac00000000460200a6040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '5c5a00c6bdfcbdfbf6d8bf97de5b3364701c2f7bbd62ea5a84611e746d662e85', blockHash: '1d86bfae61ae28380425327fda49aed105dabe1a584bf31b9e2fa18af5e63c05', cbTxMerkleTree: '040000000343017a1282bb74dc8078da1f458d7bb9c94c9f14c80c83edcbef02a782409c23d8d4eb59d1a3ce68f2fa3ea0bac2d4635db38a68f2582c409a1f161d7ff046dfc1b9c42b11a3f8d1a48e67a6f838de7a0a66cfcf5e01eeb249137b1385088f3d0107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a7040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac00000000460200a7040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '1d86bfae61ae28380425327fda49aed105dabe1a584bf31b9e2fa18af5e63c05', blockHash: '0ac9982e6cb21e568d409e0f52e45c74b174c9d6bc8866b07697fc55389332c4', cbTxMerkleTree: '04000000032e6d48f3ef9588579cc73bf48349f81423856314d31a37be9db34ca9b524c41fe8ec194cbf921e081bb9ece166ab9fe2d468eb52b913b030d24a51d8bfdba77834cf47adc014dd341f21d46aa7d527468d87e1a266ed9e3ed8609ac6d41c66120107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a8040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac00000000460200a8040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '0ac9982e6cb21e568d409e0f52e45c74b174c9d6bc8866b07697fc55389332c4', blockHash: '3bb27b4b4426019de01af80efdc751b8060a6fefbfadc4c9ccd8dac1b64a3740', cbTxMerkleTree: '04000000032a2da104b6d7ee01cccd5c91ec10d0714db0f0c2783dfd27a5ca51dbdbf58ee76d923b90c571ef8affe8228cba4ad7260753a43e7ce6089082dac63dca3b178a2b99e5f5aca8ffa045fd0d4ec5ee6fff3b77a07842681639222a02ae6c0aec990107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502a9040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a9149528697532e5e020c44aebaa1b6d0b66e239b3fe88ac00000000460200a9040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '3bb27b4b4426019de01af80efdc751b8060a6fefbfadc4c9ccd8dac1b64a3740', blockHash: '78a209f1187ddd2a9c674831be1dd024f7a61ea3ba48a48eac5b142f23193476', cbTxMerkleTree: '040000000355777cf283c0254f0171448b13c210bb0fea0bbfbf5158d19408aa093ccf71efbc30690a8d7d5ad9ad6bf34b5e71a3c66bd0e26b4e61b0817fd8783c258103074e129ef70b857be58c4dcef5bef8dae4b04c161553ad4c277d2c50219a3b3f970107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502aa040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914b7661283b68f07579114ac58aad96345acaaf2b488ac00000000460200aa040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + { + baseBlockHash: '78a209f1187ddd2a9c674831be1dd024f7a61ea3ba48a48eac5b142f23193476', blockHash: '48302f2c3d408228bf155b83600fac1e8e10a0f1b1b672116a0519c73807ddb2', cbTxMerkleTree: '030000000346ced9f4fd1379736bf9f20eec8b4b95d64227810da7fdee8ffe61b1fc62c7bbf0fbe0b62e41650587c00f65bc38d2ac2dc0724c7acf476b1148af625fde735bbc0eeae765e610a5808a88bf4ae18b8ad1318c0262e3f7edfcfd4ae76afd14290107', cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502ab040101ffffffff02aa8b4e1e030000001976a914e826d8130e37c9fc09641ee95f7255786c34234188aca28b4e1e030000001976a914efcc3a89faf4df61f3ba4f65f2e742a6c6e3453088ac00000000460200ab040000471ac09f6c873ae56b9e49de07efaf3f7b46fecbf64ba01eff65c3c3d755a4e82302877d6dd07fc54bf2f8c160ee1d74b37c7b4d866d674c097c4eddb6db6775', deletedMNs: [], mnList: [], nVersion: 2, deletedQuorums: [], newQuorums: [], merkleRootMNList: 'e8a455d7c3c365ff1ea04bf6cbfe467b3fafef07de499e6be53a876c9fc01a47', merkleRootQuorums: '7567dbb6dd4e7c094c676d864d7b7cb3741dee60c1f8f24bc57fd06d7d870223', + }, + ]; +}; diff --git a/packages/js-drive/lib/test/fixtures/getSystemIdentityPublicKeysFixture.js b/packages/js-drive/lib/test/fixtures/getSystemIdentityPublicKeysFixture.js new file mode 100644 index 00000000000..16b1d1557a5 --- /dev/null +++ b/packages/js-drive/lib/test/fixtures/getSystemIdentityPublicKeysFixture.js @@ -0,0 +1,28 @@ +const { PublicKey } = require('@dashevo/dashcore-lib'); + +function getSystemIdentityPublicKeysFixture() { + return { + masternodeRewardSharesContractOwner: { + master: new PublicKey(process.env.MASTERNODE_REWARD_SHARES_MASTER_PUBLIC_KEY).toBuffer(), + high: new PublicKey(process.env.MASTERNODE_REWARD_SHARES_SECOND_PUBLIC_KEY).toBuffer(), + }, + featureFlagsContractOwner: { + master: new PublicKey(process.env.FEATURE_FLAGS_MASTER_PUBLIC_KEY).toBuffer(), + high: new PublicKey(process.env.FEATURE_FLAGS_SECOND_PUBLIC_KEY).toBuffer(), + }, + dpnsContractOwner: { + master: new PublicKey(process.env.DPNS_MASTER_PUBLIC_KEY).toBuffer(), + high: new PublicKey(process.env.DPNS_SECOND_PUBLIC_KEY).toBuffer(), + }, + withdrawalsContractOwner: { + master: new PublicKey(process.env.WITHDRAWALS_MASTER_PUBLIC_KEY).toBuffer(), + high: new PublicKey(process.env.WITHDRAWALS_SECOND_PUBLIC_KEY).toBuffer(), + }, + dashpayContractOwner: { + master: new PublicKey(process.env.DASHPAY_MASTER_PUBLIC_KEY).toBuffer(), + high: new PublicKey(process.env.DASHPAY_SECOND_PUBLIC_KEY).toBuffer(), + }, + }; +} + +module.exports = getSystemIdentityPublicKeysFixture; diff --git a/packages/js-drive/lib/test/mock/BlockExecutionContextMock.js b/packages/js-drive/lib/test/mock/BlockExecutionContextMock.js new file mode 100644 index 00000000000..f35a0e7553a --- /dev/null +++ b/packages/js-drive/lib/test/mock/BlockExecutionContextMock.js @@ -0,0 +1,67 @@ +/** + * @method addDataContract + * @method hasDataContract + * @method getDataContracts + * @method reset + * @method setHeight + * @method getHeight + * @method setVersion + * @method getVersion + * @method setProposedAppVersion + * @method getProposedAppVersion + * @method setLastCommitInfo + * @method getLastCommitInfo + * @method getValidTxCount + * @method getInvalidTxCount + * @method setContextLogger + * @method getContextLogger + * @method getRound + * @method fromObject + * @method toObject + * @method getEpochInfo + * @method setEpochInfo + * @method setTimeMs + * @method getTimeMs + * @method getRound + * @method setRound + */ +class BlockExecutionContextMock { + /** + * @param {SinonSandbox} sinon + */ + constructor(sinon) { + this.addDataContract = sinon.stub(); + this.hasDataContract = sinon.stub(); + this.getDataContracts = sinon.stub(); + this.setCoreChainLockedHeight = sinon.stub(); + this.getCoreChainLockedHeight = sinon.stub(); + this.setHeight = sinon.stub(); + this.getHeight = sinon.stub(); + this.reset = sinon.stub(); + this.setVersion = sinon.stub(); + this.getVersion = sinon.stub(); + this.setProposedAppVersion = sinon.stub(); + this.getProposedAppVersion = sinon.stub(); + this.setLastCommitInfo = sinon.stub(); + this.getLastCommitInfo = sinon.stub(); + this.setContextLogger = sinon.stub(); + this.getContextLogger = sinon.stub(); + this.setWithdrawalTransactionsMap = sinon.stub(); + this.getWithdrawalTransactionsMap = sinon.stub(); + this.getRound = sinon.stub(); + this.populate = sinon.stub(); + this.isEmpty = sinon.stub(); + this.fromObject = sinon.stub(); + this.toObject = sinon.stub(); + this.setEpochInfo = sinon.stub(); + this.getEpochInfo = sinon.stub(); + this.setTimeMs = sinon.stub(); + this.getTimeMs = sinon.stub(); + this.setRound = sinon.stub(); + this.getRound = sinon.stub(); + this.getPrepareProposalResult = sinon.stub(); + this.setPrepareProposalResult = sinon.stub(); + } +} + +module.exports = BlockExecutionContextMock; diff --git a/packages/js-drive/lib/test/mock/BlockExecutionContextRepositoryMock.js b/packages/js-drive/lib/test/mock/BlockExecutionContextRepositoryMock.js new file mode 100644 index 00000000000..23af323c9f2 --- /dev/null +++ b/packages/js-drive/lib/test/mock/BlockExecutionContextRepositoryMock.js @@ -0,0 +1,15 @@ +/** + * @method store + * @method fetch + */ +class BlockExecutionContextRepositoryMock { + /** + * @param {SinonSandbox} sinon + */ + constructor(sinon) { + this.store = sinon.stub(); + this.fetch = sinon.stub(); + } +} + +module.exports = BlockExecutionContextRepositoryMock; diff --git a/packages/js-drive/lib/test/mock/BlockExecutionStoreTransactionsMock.js b/packages/js-drive/lib/test/mock/BlockExecutionStoreTransactionsMock.js new file mode 100644 index 00000000000..44b2f87f360 --- /dev/null +++ b/packages/js-drive/lib/test/mock/BlockExecutionStoreTransactionsMock.js @@ -0,0 +1,21 @@ +/** + * @method start + * @method commit + * @method abort + * @method getTransaction + */ +class BlockExecutionStoreTransactionsMock { + /** + * @param {SinonSandbox} sinon + */ + constructor(sinon) { + this.start = sinon.stub(); + this.commit = sinon.stub(); + this.abort = sinon.stub(); + this.getTransaction = sinon.stub(); + this.clone = sinon.stub(); + this.isStarted = sinon.stub(); + } +} + +module.exports = BlockExecutionStoreTransactionsMock; diff --git a/packages/js-drive/lib/test/mock/DriveMock.js b/packages/js-drive/lib/test/mock/DriveMock.js new file mode 100644 index 00000000000..322cdfb5543 --- /dev/null +++ b/packages/js-drive/lib/test/mock/DriveMock.js @@ -0,0 +1,29 @@ +class DriveMock { + /** + * @param {Sandbox} sinon + * @method getGroveDB + * @method close + * @method createRootTree + * @method fetchContract + * @method createContract + * @method updateContract + * @method createDocument + * @method updateDocument + * @method deleteDocument + * @method queryDocuments + */ + constructor(sinon) { + this.getGroveDB = sinon.stub(); + this.close = sinon.stub(); + this.createRootTree = sinon.stub(); + this.fetchContract = sinon.stub(); + this.createContract = sinon.stub(); + this.createContract = sinon.stub(); + this.createDocument = sinon.stub(); + this.updateDocument = sinon.stub(); + this.deleteDocument = sinon.stub(); + this.queryDocuments = sinon.stub(); + } +} + +module.exports = DriveMock; diff --git a/packages/js-drive/lib/test/mock/GroveDBStoreMock.js b/packages/js-drive/lib/test/mock/GroveDBStoreMock.js new file mode 100644 index 00000000000..795a3ea6ece --- /dev/null +++ b/packages/js-drive/lib/test/mock/GroveDBStoreMock.js @@ -0,0 +1,43 @@ +class GroveDBStoreMock { + /** + * @param {Sandbox} sinon + * @method put + * @method putReference + * @method createTree + * @method get + * @method delete + * @method getAux + * @method putAux + * @method deleteAux + * @method getRootHash + * @method startTransaction + * @method isTransactionStarted + * @method rollbackTransaction + * @method commitTransaction + * @method abortTransaction + * @method getDrive + * @method getDB + * @method setDB + */ + constructor(sinon) { + this.put = sinon.stub(); + this.putReference = sinon.stub(); + this.createTree = sinon.stub(); + this.get = sinon.stub(); + this.delete = sinon.stub(); + this.getAux = sinon.stub(); + this.putAux = sinon.stub(); + this.deleteAux = sinon.stub(); + this.getRootHash = sinon.stub(); + this.startTransaction = sinon.stub(); + this.isTransactionStarted = sinon.stub(); + this.rollbackTransaction = sinon.stub(); + this.commitTransaction = sinon.stub(); + this.abortTransaction = sinon.stub(); + this.getDrive = sinon.stub(); + this.getDB = sinon.stub(); + this.setDB = sinon.stub(); + } +} + +module.exports = GroveDBStoreMock; diff --git a/packages/js-drive/lib/test/mock/LoggerMock.js b/packages/js-drive/lib/test/mock/LoggerMock.js new file mode 100644 index 00000000000..ad45d080d92 --- /dev/null +++ b/packages/js-drive/lib/test/mock/LoggerMock.js @@ -0,0 +1,25 @@ +/** + * @method trace + * @method debug + * @method info + * @method warn + * @method error + * @method fatal + * @method child + */ +class LoggerMock { + /** + * @param {SinonSandbox} sinon + */ + constructor(sinon) { + this.trace = sinon.stub(); + this.debug = sinon.stub(); + this.info = sinon.stub(); + this.warn = sinon.stub(); + this.error = sinon.stub(); + this.fatal = sinon.stub(); + this.child = () => this; + } +} + +module.exports = LoggerMock; diff --git a/packages/js-drive/lib/test/mock/RootTreeMock.js b/packages/js-drive/lib/test/mock/RootTreeMock.js new file mode 100644 index 00000000000..2347b53ba25 --- /dev/null +++ b/packages/js-drive/lib/test/mock/RootTreeMock.js @@ -0,0 +1,11 @@ +class RootTreeMock { + /** + * @param {Sandbox} sinon + */ + constructor(sinon) { + this.getRootHash = sinon.stub(); + this.rebuild = sinon.stub(); + } +} + +module.exports = RootTreeMock; diff --git a/packages/js-drive/lib/test/mock/StateViewTransactionMock.js b/packages/js-drive/lib/test/mock/StateViewTransactionMock.js new file mode 100644 index 00000000000..4c8b767ba04 --- /dev/null +++ b/packages/js-drive/lib/test/mock/StateViewTransactionMock.js @@ -0,0 +1,20 @@ +/** + * @method start + * @method commit + * @method abort + * @property {boolean} isTransactionStarted + */ +class StateViewTransactionMock { + /** + * @param {Sandbox} sinon + */ + constructor(sinon) { + this.start = sinon.stub(); + this.commit = sinon.stub(); + this.abort = sinon.stub(); + + this.isTransactionStarted = false; + } +} + +module.exports = StateViewTransactionMock; diff --git a/packages/js-drive/lib/test/mock/StoreMock.js b/packages/js-drive/lib/test/mock/StoreMock.js new file mode 100644 index 00000000000..6a5c77d74b3 --- /dev/null +++ b/packages/js-drive/lib/test/mock/StoreMock.js @@ -0,0 +1,13 @@ +class StoreMock { + /** + * @param {Sandbox} sinon + */ + constructor(sinon) { + this.put = sinon.stub(); + this.get = sinon.stub(); + this.delete = sinon.stub(); + this.createTransaction = sinon.stub(); + } +} + +module.exports = StoreMock; diff --git a/packages/js-drive/lib/test/mock/StoreRepositoryMock.js b/packages/js-drive/lib/test/mock/StoreRepositoryMock.js new file mode 100644 index 00000000000..d37a99001ee --- /dev/null +++ b/packages/js-drive/lib/test/mock/StoreRepositoryMock.js @@ -0,0 +1,17 @@ +class StoreRepositoryMock { + /** + * @param {Sandbox} sinon + * @method store + * @method fetch + * @method createTree + */ + constructor(sinon) { + this.store = sinon.stub(); + this.fetch = sinon.stub(); + this.prove = sinon.stub(); + this.proveMany = sinon.stub(); + this.createTree = sinon.stub(); + } +} + +module.exports = StoreRepositoryMock; diff --git a/packages/js-drive/lib/test/mock/getBiggestPossibleIdentity.js b/packages/js-drive/lib/test/mock/getBiggestPossibleIdentity.js new file mode 100644 index 00000000000..98a69715569 --- /dev/null +++ b/packages/js-drive/lib/test/mock/getBiggestPossibleIdentity.js @@ -0,0 +1,46 @@ +const identityCreateTransitionSchema = require('@dashevo/dpp/schema/identity/stateTransition/identityCreate.json'); + +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); + +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +let identity; + +/** + * @return {Identity} + */ +function getBiggestPossibleIdentity() { + if (identity) { + return identity; + } + + const publicKeys = []; + + for (let i = 0; i < identityCreateTransitionSchema.properties.publicKeys.maxItems; i++) { + const securityLevel = i === 0 + ? IdentityPublicKey.SECURITY_LEVELS.MASTER + : IdentityPublicKey.SECURITY_LEVELS.HIGH; + + publicKeys.push({ + id: i, + type: IdentityPublicKey.TYPES.BLS12_381, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel, + readOnly: false, + data: Buffer.alloc(48).fill(255), + }); + } + + identity = new Identity({ + protocolVersion: 1, + id: generateRandomIdentifier().toBuffer(), + publicKeys, + balance: Math.floor(9223372036854775807 / 10000), // credits (i64) max + room for tests + revision: Math.floor(18446744073709551615 / 10000), // u64 max + room for tests + }); + + return identity; +} + +module.exports = getBiggestPossibleIdentity; diff --git a/packages/js-drive/lib/test/util/allocateRandomMemory.js b/packages/js-drive/lib/test/util/allocateRandomMemory.js new file mode 100644 index 00000000000..4fda2848440 --- /dev/null +++ b/packages/js-drive/lib/test/util/allocateRandomMemory.js @@ -0,0 +1,16 @@ +/** + * + * @param {number} sizeInBytes - size of memory to allocate + * @returns {number[]} - result of the allocation - array filled with random doubles + */ +/* istanbul ignore next */ +module.exports = function allocateRandomMemory(sizeInBytes) { + // This constant is inside of this function because + // it's easier to pass the whole function to the isolate in this case + const NUMBER_SIZE_IN_BYTES = 64 / 8; + const storage = []; + while ((storage.length * NUMBER_SIZE_IN_BYTES) < sizeInBytes) { + storage.push(Math.random()); + } + return storage; +}; diff --git a/packages/js-drive/lib/test/util/setTimeoutShim.js b/packages/js-drive/lib/test/util/setTimeoutShim.js new file mode 100644 index 00000000000..976a0d47fe1 --- /dev/null +++ b/packages/js-drive/lib/test/util/setTimeoutShim.js @@ -0,0 +1,14 @@ +/* istanbul ignore next */ +async function wait(timeout) { + const timeStarted = Date.now(); + let finished = false; + + while (!finished) { + if (Date.now() > timeStarted + timeout) { + finished = true; + } + await Promise.resolve(); + } +} + +module.exports = wait; diff --git a/packages/js-drive/lib/util/ExecutionTimer.js b/packages/js-drive/lib/util/ExecutionTimer.js new file mode 100644 index 00000000000..baead9caa20 --- /dev/null +++ b/packages/js-drive/lib/util/ExecutionTimer.js @@ -0,0 +1,95 @@ +const process = require('process'); + +class ExecutionTimer { + /** + * @type {Object.} + */ + #started = {}; + + /** + * @type {Object.} + */ + #stopped = {}; + + /** + * Start named timer + * + * @param {string} name + * + * @return {void} + */ + startTimer(name) { + if (this.isStarted(name)) { + throw new Error(`${name} timer is already started`); + } + + this.#started[name] = process.hrtime(); + } + + /** + * Clear timer + * + * @param {string} name + */ + clearTimer(name) { + delete this.#started[name]; + delete this.#stopped[name]; + } + + /** + * Get timer + * + * @param {string} name + * @param {boolean} clear - clear timer after getting + * @returns {string} + */ + getTimer(name, clear = false) { + if (!this.#stopped[name]) { + throw new Error(`${name} timer is not stopped`); + } + + const timing = this.#stopped[name]; + + if (clear) { + this.clearTimer(name); + } + + return timing; + } + + /** + * Stop named timer and get timings + * + * @param {string} name + * @param {boolean} keep - do not delete timer + * + * @return {string} + */ + stopTimer(name, keep = false) { + if (!this.isStarted(name)) { + throw new Error(`${name} timer is not started`); + } + + const timings = process.hrtime(this.#started[name]); + + const result = ( + parseFloat(timings[0].toString()) + timings[1] / 1000000000 + ).toFixed(3); + + if (keep) { + this.#stopped[name] = result; + } + + return result; + } + + /** + * @param {string} name + * @return {boolean} + */ + isStarted(name) { + return this.#started[name] !== undefined; + } +} + +module.exports = ExecutionTimer; diff --git a/packages/js-drive/lib/util/millisToProtoTimestamp.js b/packages/js-drive/lib/util/millisToProtoTimestamp.js new file mode 100644 index 00000000000..49f076b62b2 --- /dev/null +++ b/packages/js-drive/lib/util/millisToProtoTimestamp.js @@ -0,0 +1,28 @@ +const { + google: { + protobuf: { + Timestamp, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +/** + * Get milliseconds time from seconds and nanoseconds + * + * @param {number} milliseconds + * + * @returns {number} + */ +function millisToProtoTimestamp(milliseconds) { + const seconds = Math.floor(milliseconds / 1000); + const nanos = (milliseconds - (seconds * 1000)) * (10 ** 6); + + return new Timestamp({ + seconds: Long.fromNumber(seconds), + nanos, + }); +} + +module.exports = millisToProtoTimestamp; diff --git a/packages/js-drive/lib/util/noopLogger.js b/packages/js-drive/lib/util/noopLogger.js new file mode 100644 index 00000000000..a1593e9d71c --- /dev/null +++ b/packages/js-drive/lib/util/noopLogger.js @@ -0,0 +1,8 @@ +const pino = require('pino'); + +const noopLogger = Object.keys(pino.levels.values).reduce((logger, functionName) => ({ + ...logger, + [functionName]: () => {}, +}), {}); + +module.exports = noopLogger; diff --git a/packages/js-drive/lib/util/printErrorFace.js b/packages/js-drive/lib/util/printErrorFace.js new file mode 100644 index 00000000000..b6e745d647a --- /dev/null +++ b/packages/js-drive/lib/util/printErrorFace.js @@ -0,0 +1,35 @@ +const chalk = require('chalk'); + +// Faces https://github.com/maxogden/cool-ascii-faces +const faces = [ + '\\_(ʘ_ʘ)_/', + '(•̀o•́)ง', + 'ヽ༼° ͟ل͜ ͡°༽ノ', + 'ノ( ゜-゜ノ)', + '༼ ºل͟º ༽', + '(ಥ﹏ಥ)', + '¯\\_(ツ)_/¯', + '(╯°□°)╯︵ ┻━┻', // https://looks.wtf/flipping-tables +]; + +/** + * @return {string} + */ +function printErrorFace() { + let face = ''; + + // top padding + face += '\n\n'; + + // face + face += chalk.red( + faces[Math.floor(Math.random() * faces.length)], + ); + + // bottom padding + face += '\n\n'; + + return face; +} + +module.exports = printErrorFace; diff --git a/packages/js-drive/lib/util/protoTimestampToMillis.js b/packages/js-drive/lib/util/protoTimestampToMillis.js new file mode 100644 index 00000000000..79e47a8bf1d --- /dev/null +++ b/packages/js-drive/lib/util/protoTimestampToMillis.js @@ -0,0 +1,11 @@ +const timeToMillis = require('./timeToMillis'); + +/** + * @param {google.protobuf.ITimestamp} timestamp + * @returns {number} + */ +function protoTimestampToMillis(timestamp) { + return timeToMillis(timestamp.seconds.toNumber(), timestamp.nanos); +} + +module.exports = protoTimestampToMillis; diff --git a/packages/js-drive/lib/util/rejectAfter.js b/packages/js-drive/lib/util/rejectAfter.js new file mode 100644 index 00000000000..4701716d38f --- /dev/null +++ b/packages/js-drive/lib/util/rejectAfter.js @@ -0,0 +1,25 @@ +/** + * Reject with @param error if @param promise is not resolved in @param ms + * + * @param {Promise} promise + * @param {Error} error + * @param {number} ms + * @return {Promise} + */ +module.exports = async function rejectAfter(promise, error, ms) { + let timeout; + let res; + try { + res = await Promise.race([ + promise, + new Promise((resolve, reject) => { + timeout = setTimeout(() => reject(error), ms); + }), + ]); + } finally { + // noinspection JSUnusedAssignment + clearTimeout(timeout); + } + + return res; +}; diff --git a/packages/js-drive/lib/util/sanitizeUrl.js b/packages/js-drive/lib/util/sanitizeUrl.js new file mode 100644 index 00000000000..d3fedd42249 --- /dev/null +++ b/packages/js-drive/lib/util/sanitizeUrl.js @@ -0,0 +1,15 @@ +function sanitizeUrl(url) { + for (let i = 0, len = url.length; i < len; i++) { + const charCode = url.charCodeAt(i); + // Some systems do not follow RFC and separate the path and query + // string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`. + // Thus, we need to split on `;` as well as `?` and `#`. + if (charCode === 63 || charCode === 59 || charCode === 35) { + return url.slice(0, i); + } + } + + return url; +} + +module.exports = sanitizeUrl; diff --git a/packages/js-drive/lib/util/shuffleArray.js b/packages/js-drive/lib/util/shuffleArray.js new file mode 100644 index 00000000000..1e6c97f9102 --- /dev/null +++ b/packages/js-drive/lib/util/shuffleArray.js @@ -0,0 +1,14 @@ +/* eslint-disable no-param-reassign */ +/** + * Shuffle the given array in place + * + * @param {Array} array + * @returns {Array} + */ +module.exports = function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; diff --git a/packages/js-drive/lib/util/timeToMillis.js b/packages/js-drive/lib/util/timeToMillis.js new file mode 100644 index 00000000000..49037e73bfd --- /dev/null +++ b/packages/js-drive/lib/util/timeToMillis.js @@ -0,0 +1,15 @@ +/** + * Get milliseconds time from seconds and nanoseconds + * + * @param {number} seconds + * @param {number} nanoseconds + * + * @returns {number} + */ +function timeToMillis(seconds, nanoseconds) { + const overallNanos = nanoseconds + seconds * (10 ** 9); + + return Math.floor(overallNanos / (10 ** 6)); +} + +module.exports = timeToMillis; diff --git a/packages/js-drive/lib/util/wait.js b/packages/js-drive/lib/util/wait.js new file mode 100644 index 00000000000..b0d45b4c5ce --- /dev/null +++ b/packages/js-drive/lib/util/wait.js @@ -0,0 +1,10 @@ +/** + * Asynchronously wait for a specified number of milliseconds. + * @param {Number} ms - Number of milliseconds to wait. + * @return {Promise} The promise to await on. + */ +async function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +module.exports = wait; diff --git a/packages/js-drive/lib/validator/Validator.js b/packages/js-drive/lib/validator/Validator.js new file mode 100644 index 00000000000..daefdb63af4 --- /dev/null +++ b/packages/js-drive/lib/validator/Validator.js @@ -0,0 +1,72 @@ +const PublicKeyShareIsNotPresentError = require('./errors/PublicKeyShareIsNotPresentError'); + +class Validator { + /** + * @param {Buffer} proTxHash + * @param {ValidatorNetworkInfo} networkInfo + * @param {Buffer} [pubKeyShare] + */ + constructor(proTxHash, networkInfo, pubKeyShare = undefined) { + this.proTxHash = proTxHash; + this.networkInfo = networkInfo; + this.pubKeyShare = pubKeyShare; + } + + /** + * Get validator pro tx hash + * + * @return {Buffer} + */ + getProTxHash() { + return this.proTxHash; + } + + /** + * Get validator public key share + * @return {Buffer} + */ + getPublicKeyShare() { + return this.pubKeyShare; + } + + /** + * Get validator voting power + * + * @return {number} + */ + getVotingPower() { + return Validator.DEFAULT_DASH_VOTING_POWER; + } + + /** + * Get network info + * + * @returns {ValidatorNetworkInfo} + */ + getNetworkInfo() { + return this.networkInfo; + } + + /** + * @param {Object} member + * @param {ValidatorNetworkInfo} networkInfo + * @param {boolean} [pubKeyShareRequired=false] + * @return {Validator} + */ + static createFromQuorumMember(member, networkInfo, pubKeyShareRequired = false) { + const proTxHash = Buffer.from(member.proTxHash, 'hex'); + + let pubKeyShare; + if (member.pubKeyShare) { + pubKeyShare = Buffer.from(member.pubKeyShare, 'hex'); + } else if (pubKeyShareRequired) { + throw new PublicKeyShareIsNotPresentError(member); + } + + return new Validator(proTxHash, networkInfo, pubKeyShare); + } +} + +Validator.DEFAULT_DASH_VOTING_POWER = 100; + +module.exports = Validator; diff --git a/packages/js-drive/lib/validator/ValidatorNetworkInfo.js b/packages/js-drive/lib/validator/ValidatorNetworkInfo.js new file mode 100644 index 00000000000..91593bd4f8f --- /dev/null +++ b/packages/js-drive/lib/validator/ValidatorNetworkInfo.js @@ -0,0 +1,29 @@ +class ValidatorNetworkInfo { + /** + * + * @param {string} host + * @param {number} port + */ + constructor(host, port) { + this.host = host; + this.port = port; + } + + /** + * Get validator host + * @returns {string} + */ + getHost() { + return this.host; + } + + /** + * Get validator port + * @returns {number} + */ + getPort() { + return this.port; + } +} + +module.exports = ValidatorNetworkInfo; diff --git a/packages/js-drive/lib/validator/ValidatorSet.js b/packages/js-drive/lib/validator/ValidatorSet.js new file mode 100644 index 00000000000..ca3d0616bff --- /dev/null +++ b/packages/js-drive/lib/validator/ValidatorSet.js @@ -0,0 +1,171 @@ +const Validator = require('./Validator'); +const ValidatorSetIsNotInitializedError = require('./errors/ValidatorSetIsNotInitializedError'); +const ValidatorNetworkInfo = require('./ValidatorNetworkInfo'); + +class ValidatorSet { + /** + * @param {SimplifiedMasternodeList} simplifiedMasternodeList + * @param {getRandomQuorum} getRandomQuorum + * @param {fetchQuorumMembers} fetchQuorumMembers + * @param {number} validatorSetLLMQType + * @param {RpcClient} coreRpcClient + * @param {number} tenderdashP2pPort + */ + constructor( + simplifiedMasternodeList, + getRandomQuorum, + fetchQuorumMembers, + validatorSetLLMQType, + coreRpcClient, + tenderdashP2pPort, + ) { + this.simplifiedMasternodeList = simplifiedMasternodeList; + this.getRandomQuorum = getRandomQuorum; + this.fetchQuorumMembers = fetchQuorumMembers; + this.validatorSetLLMQType = validatorSetLLMQType; + this.coreRpcClient = coreRpcClient; + this.tenderdashP2pPort = tenderdashP2pPort; + + this.quorum = null; + this.validators = []; + } + + /** + * Chooses an active validator set from among all active validator quorums for the first time + * + * @param {number} coreHeight + */ + async initialize(coreHeight) { + const sml = this.simplifiedMasternodeList.getStore().getSMLbyHeight(coreHeight); + + // using the block hash at the first core height as entropy + const rotationEntropy = Buffer.from(sml.toSimplifiedMNListDiff().blockHash, 'hex'); + + await this.switchToRandomQuorum( + sml, + coreHeight, + rotationEntropy, + ); + } + + /** + * Rotates to a new active validator set from among all active validator quorums + * + * @param {Long} height + * @param {number} coreHeight + * @param {Buffer} rotationEntropy + */ + async rotate(height, coreHeight, rotationEntropy) { + const sml = this.simplifiedMasternodeList.getStore().getSMLbyHeight(coreHeight); + + // validator set is rotated every ROTATION_BLOCK_INTERVAL blocks + if (height.toNumber() % ValidatorSet.ROTATION_BLOCK_INTERVAL !== 0) { + return false; + } + + await this.switchToRandomQuorum( + sml, + coreHeight, + rotationEntropy, + ); + + return true; + } + + /** + * Get Validator Set Quorum + * + * @return {QuorumEntry} + */ + getQuorum() { + if (!this.quorum) { + throw new ValidatorSetIsNotInitializedError(); + } + + return this.quorum; + } + + /** + * Get validators + * + * @return {Validator[]} + */ + getValidators() { + if (this.validators.length === 0) { + throw new ValidatorSetIsNotInitializedError(); + } + + return this.validators; + } + + /** + * @private + * @param {SimplifiedMNList} sml + * @param {number} coreHeight + * @param {Buffer} rotationEntropy + * @return {Promise} + */ + async switchToRandomQuorum(sml, coreHeight, rotationEntropy) { + this.quorum = await this.getRandomQuorum( + sml, + this.validatorSetLLMQType, + rotationEntropy, + coreHeight, + ); + + const quorumMembers = await this.fetchQuorumMembers( + this.validatorSetLLMQType, + this.quorum.quorumHash, + ); + + // If the node is a quorum member and doesn't receive public key share for members + // it should throw an error + let proTxHash; + + try { + ({ + result: { + proTxHash, + }, + } = await this.coreRpcClient.masternode('status')); + } catch (e) { + // This node is not a masternode + if (e.code !== -32603) { + throw e; + } + } + + const isThisNodeMember = !!quorumMembers + .find((member) => member.valid && member.proTxHash === proTxHash); + + const validMasternodesList = this.simplifiedMasternodeList + .getStore() + .getCurrentSML() + .getValidMasternodesList(); + + const masternodes = {}; + + this.validators = quorumMembers.filter((member) => { + // Ignore invalid quorum members + if (!member.valid) { + return false; + } + + // Ignore members which are not part of SML + masternodes[member.proTxHash] = validMasternodesList + .find((mnEntry) => mnEntry.proRegTxHash === member.proTxHash); + + return Boolean(masternodes[member.proTxHash]); + }).map((member) => { + const masternode = masternodes[member.proTxHash]; + + const networkInfo = new ValidatorNetworkInfo(masternode.getIp(), this.tenderdashP2pPort); + + return Validator.createFromQuorumMember(member, networkInfo, isThisNodeMember); + }); + } +} + +ValidatorSet.ROTATION_BLOCK_INTERVAL = 15; + +module.exports = ValidatorSet; diff --git a/packages/js-drive/lib/validator/errors/PublicKeyShareIsNotPresentError.js b/packages/js-drive/lib/validator/errors/PublicKeyShareIsNotPresentError.js new file mode 100644 index 00000000000..aebcec4b46b --- /dev/null +++ b/packages/js-drive/lib/validator/errors/PublicKeyShareIsNotPresentError.js @@ -0,0 +1,23 @@ +const DriveError = require('../../errors/DriveError'); + +class PublicKeyShareIsNotPresentError extends DriveError { + /** + * @param {Object} member + */ + constructor(member) { + super('Public key share is not present for validator'); + + this.member = member; + } + + /** + * Get quorum member info + * + * @return {Object} + */ + getMember() { + return this.member; + } +} + +module.exports = PublicKeyShareIsNotPresentError; diff --git a/packages/js-drive/lib/validator/errors/ValidatorSetIsNotInitializedError.js b/packages/js-drive/lib/validator/errors/ValidatorSetIsNotInitializedError.js new file mode 100644 index 00000000000..c6a2b1c0ea7 --- /dev/null +++ b/packages/js-drive/lib/validator/errors/ValidatorSetIsNotInitializedError.js @@ -0,0 +1,9 @@ +const DriveError = require('../../errors/DriveError'); + +class ValidatorSetIsNotInitializedError extends DriveError { + constructor() { + super('Validator Set is not initialized'); + } +} + +module.exports = ValidatorSetIsNotInitializedError; diff --git a/packages/js-drive/package.json b/packages/js-drive/package.json new file mode 100644 index 00000000000..019ff95d9b0 --- /dev/null +++ b/packages/js-drive/package.json @@ -0,0 +1,104 @@ +{ + "name": "@dashevo/drive", + "private": true, + "version": "0.24.0-dev.16", + "description": "Replicated state machine for Dash Platform", + "engines": { + "node": ">=12" + }, + "contributors": [ + { + "name": "Ivan Shumkov", + "email": "ivan@shumkov.ru", + "url": "https://github.com/shumkov" + }, + { + "name": "Djavid Gabibiyan", + "email": "djavid@dash.org", + "url": "https://github.com/jawid-h" + }, + { + "name": "Anton Suprunchuk", + "email": "anton.suprunchuk@dash.org", + "url": "https://github.com/antouhou" + }, + { + "name": "Konstantin Shuplenkov", + "email": "konstantin.shuplenkov@dash.org", + "url": "https://github.com/shuplenkov" + } + ], + "scripts": { + "abci": "node scripts/abci", + "echo": "node scripts/echo", + "lint": "eslint .", + "test": "yarn run test:coverage", + "test:coverage": "nyc --check-coverage --stmts=93 --branch=85 --funcs=90 --lines=88 yarn run mocha './test/unit/**/*.spec.js' './test/integration/**/*.spec.js'", + "test:unit": "mocha './test/unit/**/*.spec.js'", + "test:integration": "mocha './test/integration/**/*.spec.js'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dashevo/js-drive.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/dashevo/js-drive/issues" + }, + "homepage": "https://github.com/dashevo/js-drive", + "devDependencies": { + "@dashevo/dp-services-ctl": "github:dashevo/js-dp-services-ctl#v0.19-dev", + "@types/pino": "^6.3.0", + "babel-eslint": "^10.1.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "chai-string": "^1.5.0", + "dirty-chai": "^2.0.1", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.24.2", + "levelup": "^4.4.0", + "memdown": "^5.1.0", + "mocha": "^9.1.2", + "moment": "^2.29.4", + "nyc": "^15.1.0", + "rimraf": "^3.0.2", + "sinon": "^11.1.2", + "sinon-chai": "^3.7.0" + }, + "dependencies": { + "@dashevo/abci": "github:dashpay/js-abci#09f72120bc2059144f72eb7a246d632ead3fc3c6", + "@dashevo/dapi-grpc": "workspace:*", + "@dashevo/dashcore-lib": "~0.20.0", + "@dashevo/dashd-rpc": "^18.2.0", + "@dashevo/dashpay-contract": "workspace:*", + "@dashevo/dpns-contract": "workspace:*", + "@dashevo/dpp": "workspace:*", + "@dashevo/feature-flags-contract": "workspace:*", + "@dashevo/grpc-common": "workspace:*", + "@dashevo/masternode-reward-shares-contract": "workspace:*", + "@dashevo/rs-drive": "workspace:*", + "@dashevo/withdrawals-contract": "workspace:*", + "ajv": "^8.6.0", + "ajv-keywords": "^5.0.0", + "awilix": "^4.2.6", + "blake3": "^2.1.4", + "bs58": "^4.0.1", + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "dotenv-expand": "^5.1.0", + "dotenv-safe": "^8.2.0", + "find-my-way": "^2.2.2", + "js-merkle": "^0.1.5", + "lodash": "^4.17.21", + "long": "^5.2.0", + "node-graceful": "^3.0.1", + "pino": "^6.4.0", + "pino-multi-stream": "^5.2.0", + "pino-pretty": "^4.0.3", + "rimraf": "^3.0.2", + "setimmediate": "^1.0.5", + "through2": "^3.0.1", + "zeromq": "^5.2.8" + } +} diff --git a/packages/js-drive/scripts/abci.js b/packages/js-drive/scripts/abci.js new file mode 100644 index 00000000000..4d577e27e22 --- /dev/null +++ b/packages/js-drive/scripts/abci.js @@ -0,0 +1,181 @@ +require('dotenv-expand')(require('dotenv-safe').config()); + +const graceful = require('node-graceful'); + +const chalk = require('chalk'); + +const ZMQClient = require('../lib/core/ZmqClient'); + +const createDIContainer = require('../lib/createDIContainer'); + +const { version: driveVersion } = require('../package.json'); + +const banner = '\n ____ ______ ____ __ __ ____ ____ ______ __ __ ____ \n' ++ '/\\ _`\\ /\\ _ \\ /\\ _`\\ /\\ \\/\\ \\ /\\ _`\\ /\\ _`\\ /\\__ _\\ /\\ \\/\\ \\ /\\ _`\\ \n' ++ '\\ \\ \\/\\ \\ \\ \\ \\L\\ \\ \\ \\,\\L\\_\\ \\ \\ \\_\\ \\ \\ \\ \\/\\ \\ \\ \\ \\L\\ \\ \\/_/\\ \\/ \\ \\ \\ \\ \\ \\ \\ \\L\\_\\ \n' ++ ' \\ \\ \\ \\ \\ \\ \\ __ \\ \\/_\\__ \\ \\ \\ _ \\ \\ \\ \\ \\ \\ \\ \\ , / \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ _\\L \n' ++ ' \\ \\ \\_\\ \\ \\ \\ \\/\\ \\ /\\ \\L\\ \\ \\ \\ \\ \\ \\ \\ \\ \\_\\ \\ \\ \\ \\\\ \\ \\_\\ \\__ \\ \\ \\_/ \\ \\ \\ \\L\\ \\\n' ++ ' \\ \\____/ \\ \\_\\ \\_\\ \\ `\\____\\ \\ \\_\\ \\_\\ \\ \\____/ \\ \\_\\ \\_\\ /\\_____\\ \\ `\\___/ \\ \\____/\n' ++ ' \\/___/ \\/_/\\/_/ \\/_____/ \\/_/\\/_/ \\/___/ \\/_/\\/ / \\/_____/ `\\/__/ \\/___/\n\n\n'; + +// eslint-disable-next-line no-console +console.log(chalk.hex('#008de4')(banner)); + +(async function main() { + const container = createDIContainer(process.env); + const logger = container.resolve('logger'); + const dpp = container.resolve('dpp'); + const transactionalDpp = container.resolve('transactionalDpp'); + const errorHandler = container.resolve('errorHandler'); + const latestProtocolVersion = container.resolve('latestProtocolVersion'); + const closeAbciServer = container.resolve('closeAbciServer'); + + logger.info(`Starting Drive ABCI application v${driveVersion} (latest protocol v${latestProtocolVersion})`); + + /** + * Ensure graceful shutdown + */ + + process + .on('unhandledRejection', errorHandler) + .on('uncaughtException', errorHandler); + + graceful.DEADLY_SIGNALS.push('SIGQUIT'); + + graceful.on('exit', async (signal) => { + logger.info({ signal }, `Received ${signal}. Stopping Drive ABCI application...`); + + await closeAbciServer(); + + await container.dispose(); + }); + + /** + * Initialize DPP + */ + + await dpp.initialize(); + await transactionalDpp.initialize(); + + /** + * Make sure Core is synced + */ + + const network = container.resolve('network'); + + logger.info(`Connecting to Core in ${network} network...`); + + const waitForCoreSync = container.resolve('waitForCoreSync'); + await waitForCoreSync((currentBlockHeight, currentHeaderNumber) => { + let message = `waiting for core to finish sync ${currentBlockHeight}/${currentHeaderNumber}...`; + + if (currentBlockHeight === 0 && currentHeaderNumber === 0) { + message = 'waiting for core to connect to peers...'; + } + + logger.info(message); + }); + + /** + * Connect to Core ZMQ socket + */ + + const coreZMQClient = container.resolve('coreZMQClient'); + + coreZMQClient.on(ZMQClient.events.CONNECTED, () => { + logger.debug('Connected to core ZMQ socket'); + }); + + coreZMQClient.on(ZMQClient.events.DISCONNECTED, () => { + logger.debug('Disconnected from core ZMQ socket'); + }); + + coreZMQClient.on(ZMQClient.events.MAX_RETRIES_REACHED, async () => { + const error = new Error('Can\'t connect to core ZMQ'); + + await errorHandler(error); + }); + + try { + await coreZMQClient.start(); + } catch (e) { + const error = new Error(`Can't connect to core ZMQ socket: ${e.message}`); + + await errorHandler(error); + } + + /** + * Obtain chain lock + */ + + logger.info('Obtaining the latest chain lock...'); + + const waitForCoreChainLockSync = container.resolve('waitForCoreChainLockSync'); + await waitForCoreChainLockSync(); + + /** + * Wait for initial core chain locked height + */ + const initialCoreChainLockedHeight = container.resolve('initialCoreChainLockedHeight'); + + logger.info(`Waiting for initial core chain locked height #${initialCoreChainLockedHeight}...`); + + const waitForChainLockedHeight = container.resolve('waitForChainLockedHeight'); + await waitForChainLockedHeight(initialCoreChainLockedHeight); + + /** + * Start ABCI server + */ + + const abciServer = container.resolve('abciServer'); + + abciServer.on('connection', (socket) => { + logger.debug( + { + abciConnectionId: socket.connection.id, + }, + `Accepted new ABCI connection #${socket.connection.id} from ${socket.remoteAddress}:${socket.remotePort}`, + ); + + socket.on('error', (e) => { + logger.error( + { + err: e, + abciConnectionId: socket.connection.id, + }, + `ABCI connection #${socket.connection.id} error: ${e.message}`, + ); + }); + + socket.once('close', (hasError) => { + let message = `ABCI connection #${socket.connection.id} is closed`; + if (hasError) { + message += ' with error'; + } + + logger.debug( + { + abciConnectionId: socket.connection.id, + }, + message, + ); + }); + }); + + abciServer.once('close', () => { + logger.info('ABCI server and all connections are closed'); + }); + + abciServer.on('error', async (e) => { + await errorHandler(e); + }); + + abciServer.on('listening', () => { + logger.info(`ABCI server is waiting for connection on port ${container.resolve('abciPort')}`); + }); + + abciServer.listen( + container.resolve('abciPort'), + container.resolve('abciHost'), + ); +}()); diff --git a/packages/js-drive/scripts/echo.js b/packages/js-drive/scripts/echo.js new file mode 100644 index 00000000000..efd6d7f8353 --- /dev/null +++ b/packages/js-drive/scripts/echo.js @@ -0,0 +1,34 @@ +const net = require('net'); + +async function sendEcho(ip) { + const echoRequestBytes = Buffer.from('0a0a080a0668656c6c6f21', 'hex'); + + return new Promise((resolve, reject) => { + const client = net.connect(26658, ip); + + client.on('connect', () => { + client.write(echoRequestBytes); + }); + + client.on('data', () => { + client.destroy(); + + resolve('ok'); + }); + + client.on('error', reject); + + setTimeout(() => { + reject(new Error('Can\'t connect to ABCI port: timeout.')); + }, 2000); + }); +} + +sendEcho('127.0.0.1') + // eslint-disable-next-line no-console + .then(console.log) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + process.exit(1); + }); diff --git a/packages/js-drive/test/.eslintrc b/packages/js-drive/test/.eslintrc new file mode 100644 index 00000000000..720ced73852 --- /dev/null +++ b/packages/js-drive/test/.eslintrc @@ -0,0 +1,12 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" + }, + "globals": { + "expect": true + } +} diff --git a/packages/js-drive/test/README.md b/packages/js-drive/test/README.md new file mode 100644 index 00000000000..ce3a86a76af --- /dev/null +++ b/packages/js-drive/test/README.md @@ -0,0 +1,54 @@ +# Drive Tests + +We believe in [Test Pyramid](http://verraes.net/2015/01/economy-of-tests/). + +## Structure + + - `integration/` - [Integration tests](https://en.wikipedia.org/wiki/Integration_testing) + - `unit/` - [Unit tests](https://en.wikipedia.org/wiki/Unit_testing) + +A subsequent paths the same as the structure of the code in the [lib/](../lib) directory. + +## How to run tests + +Run all tests: + +```bash +npm test +``` + +Run unit tests: + +```bash +npm run test:unit +``` + +Run integration tests: + +```bash +npm run test:integration +``` + +## How to write tests + +We use: + - [Mocha](https://mochajs.org) as testing framework + - [Sinon.JS](http://sinonjs.org/) for stubs and spies + - [Chai](http://chaijs.com/) with several plugins for assertions: + - [Sinon Chai](https://github.com/domenic/sinon-chai) for Sinon.JS assertions + - [Chai as promised](https://github.com/domenic/chai-as-promised) for assertions about promises + - [Dirty Chai](https://github.com/prodatakey/dirty-chai) for lint-friendly terminating assertions + +We prefer `expect` assertions syntax instead of `should`. + +All tools are [bootstrapped](../lib/test/bootstrap.js) before tests: + - `expect` function is available in global context + - Sinon sandbox is created before each test and available as `this.sinon` property in the test's context + - Envs from `.env` are loaded before all tests + +## Evolution helpers +We use [js-evo-services-ctl](https://github.com/dashevo/js-evo-services-ctl) library to manipulate Evolution's services. + +## Other tools + +You may find other useful tools for testing in [lib/test](../lib/test) directory. diff --git a/packages/js-drive/test/integration/abci/handlers/queryHandlerFactory.spec.js b/packages/js-drive/test/integration/abci/handlers/queryHandlerFactory.spec.js new file mode 100644 index 00000000000..a06c3c3e9a4 --- /dev/null +++ b/packages/js-drive/test/integration/abci/handlers/queryHandlerFactory.spec.js @@ -0,0 +1,150 @@ +const cbor = require('cbor'); + +const { + asValue, +} = require('awilix'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); + +const createTestDIContainer = require('../../../../lib/test/createTestDIContainer'); +const InvalidArgumentAbciError = require('../../../../lib/abci/errors/InvalidArgumentAbciError'); + +describe('queryHandlerFactory', function main() { + this.timeout(90000); + + let container; + let queryHandler; + let identityQueryHandlerMock; + let dataContractQueryHandlerMock; + let documentQueryHandlerMock; + let dataContract; + let documents; + let identity; + let proof; + + beforeEach(async function beforeEach() { + proof = Buffer.from('GbYYWuLCU6u7nb4pdnMM1uzAeURhE7ZPxGqAbUARBsb3', 'hex'); + + container = await createTestDIContainer(); + + dataContract = getDataContractFixture(); + documents = getDocumentsFixture(dataContract); + identity = getIdentityFixture(); + + identityQueryHandlerMock = this.sinon.stub(); + identityQueryHandlerMock.resolves({ + value: identity, + proof, + }); + + dataContractQueryHandlerMock = this.sinon.stub(); + dataContractQueryHandlerMock.resolves(dataContract); + + documentQueryHandlerMock = this.sinon.stub(); + documentQueryHandlerMock.resolves(documents); + + container.register('identityQueryHandler', asValue(identityQueryHandlerMock)); + container.register('dataContractQueryHandler', asValue(dataContractQueryHandlerMock)); + container.register('documentQueryHandler', asValue(documentQueryHandlerMock)); + + const enrichErrorWithContextError = container.resolve('enrichErrorWithContextError'); + queryHandler = enrichErrorWithContextError(container.resolve('queryHandler')); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + describe('/identities', () => { + it('should call identity handler and return an identity with proof', async () => { + const result = await queryHandler({ + path: '/identities', + data: cbor.encode({ + id: 1, + }), + prove: 'true', + }); + + expect(identityQueryHandlerMock).to.have.been.calledOnceWithExactly( + {}, + { id: 1 }, + { + path: '/identities', + data: cbor.encode({ + id: 1, + }), + prove: 'true', + }, + ); + + expect(result).to.deep.equal({ + value: identity, + proof, + }); + }); + }); + + describe('/dataContracts', () => { + it('should call data contract handler and return data contract', async () => { + const result = await queryHandler({ + path: '/dataContracts', + data: cbor.encode({ + id: 1, + }), + }); + + expect(dataContractQueryHandlerMock).to.have.been.calledOnceWithExactly( + {}, + { id: 1 }, + { + path: '/dataContracts', + data: cbor.encode({ + id: 1, + }), + }, + ); + expect(result).to.deep.equal(dataContract); + }); + }); + + describe('/dataContracts/documents', () => { + it('should call documents handler and return documents', async () => { + const result = await queryHandler({ + path: '/dataContracts/documents', + data: cbor.encode({ + contractId: 1, + type: 'someType', + }), + }); + + expect(documentQueryHandlerMock).to.have.been.calledOnceWithExactly( + {}, + { contractId: 1, type: 'someType' }, + { + path: '/dataContracts/documents', + data: cbor.encode({ + contractId: 1, + type: 'someType', + }), + }, + ); + expect(result).to.deep.equal(documents); + }); + }); + + it('should throw an error if invalid path is submitted', async () => { + try { + await queryHandler({ + path: '/unknownPath', + data: Buffer.alloc(0), + }); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidArgumentAbciError); + expect(e.getMessage()).to.equal('Invalid path'); + } + }); +}); diff --git a/packages/js-drive/test/integration/blockExecution/BlockExecutionContextRepository.spec.js b/packages/js-drive/test/integration/blockExecution/BlockExecutionContextRepository.spec.js new file mode 100644 index 00000000000..0044355bc3c --- /dev/null +++ b/packages/js-drive/test/integration/blockExecution/BlockExecutionContextRepository.spec.js @@ -0,0 +1,121 @@ +const rimraf = require('rimraf'); +const cbor = require('cbor'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const Drive = require('@dashevo/rs-drive'); +const getBlockExecutionContextObjectFixture = require('../../../lib/test/fixtures/getBlockExecutionContextObjectFixture'); +const BlockExecutionContext = require('../../../lib/blockExecution/BlockExecutionContext'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const noopLogger = require('../../../lib/util/noopLogger'); +const BlockExecutionContextRepository = require('../../../lib/blockExecution/BlockExecutionContextRepository'); + +describe('BlockExecutionContextRepository', () => { + // let container; + let blockExecutionContextRepository; + let blockExecutionContext; + let rsDrive; + let store; + let options; + + beforeEach(async () => { + const dataContract = getDataContractFixture(); + delete dataContract.entropy; + + const plainObject = getBlockExecutionContextObjectFixture(dataContract); + + blockExecutionContext = new BlockExecutionContext(); + blockExecutionContext.fromObject(plainObject); + + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, noopLogger); + + blockExecutionContextRepository = new BlockExecutionContextRepository(store); + + options = {}; + }); + + afterEach(async () => { + await rsDrive.close(); + rimraf.sync('./db/grovedb_test'); + }); + + it('should store blockExecutionContext', async () => { + const result = await blockExecutionContextRepository.store(blockExecutionContext, options); + + expect(result).to.be.instanceOf(BlockExecutionContextRepository); + + const encodedResult = await store.getAux( + BlockExecutionContextRepository.EXTERNAL_STORE_KEY_NAME, + options, + ); + + const blockExecutionContextEncoded = encodedResult.getValue(); + + const rawBlockExecutionContext = cbor.decode(blockExecutionContextEncoded); + + expect(rawBlockExecutionContext).to.deep.equal(blockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })); + }); + + it('should fetch blockExecutionContext', async () => { + await store.putAux( + BlockExecutionContextRepository.EXTERNAL_STORE_KEY_NAME, + await cbor.encodeAsync(blockExecutionContext.toObject({ + skipContextLogger: true, + })), + options, + ); + + const fetchedBlockExecutionContext = await blockExecutionContextRepository.fetch(options); + + expect(fetchedBlockExecutionContext).to.be.instanceOf(BlockExecutionContext); + + expect(fetchedBlockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })).to.deep.equal(blockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })); + }); + + it('should fetch blockExecutionContext stored in transaction', async () => { + await store.startTransaction(); + + await blockExecutionContextRepository.store(blockExecutionContext, { useTransaction: true }); + + let fetchedBlockExecutionContext = await blockExecutionContextRepository.fetch(); + + expect(fetchedBlockExecutionContext).to.be.instanceOf(BlockExecutionContext); + + expect(fetchedBlockExecutionContext.isEmpty()).to.be.true(); + + await store.commitTransaction(); + + fetchedBlockExecutionContext = await blockExecutionContextRepository.fetch(); + + expect(fetchedBlockExecutionContext).to.be.instanceOf(BlockExecutionContext); + + expect(fetchedBlockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })).to.deep.equal(blockExecutionContext.toObject({ + skipContextLogger: true, + skipPrepareProposalResult: true, + })); + }); +}); diff --git a/packages/js-drive/test/integration/core/SimplifiedMasternodeList.spec.js b/packages/js-drive/test/integration/core/SimplifiedMasternodeList.spec.js new file mode 100644 index 00000000000..b9794d449e4 --- /dev/null +++ b/packages/js-drive/test/integration/core/SimplifiedMasternodeList.spec.js @@ -0,0 +1,103 @@ +const getSmlFixture = require('../../../lib/test/fixtures/getSmlFixture'); +const SimplifiedMasternodeList = require('../../../lib/core/SimplifiedMasternodeList'); + +describe('SimplifiedMasternodeList', function SimplifiedMasternodeListTest() { + let simplifiedMasternodeList; + let smlMaxListsLimit; + let initialSmlDiffs; + let updatedSmlDiffs; + + this.timeout(10000); + + beforeEach(() => { + simplifiedMasternodeList = new SimplifiedMasternodeList({ + smlMaxListsLimit, + }); + + initialSmlDiffs = getSmlFixture().slice(0, 16); + updatedSmlDiffs = getSmlFixture().slice(16, 17); + }); + + it('should set options', async () => { + expect(simplifiedMasternodeList.options).to.deep.equal({ maxListsLimit: smlMaxListsLimit }); + }); + + describe('#applyDiffs', () => { + it('should create simplifiedMNList', async () => { + let simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList).to.deep.equal(undefined); + + simplifiedMasternodeList.applyDiffs(initialSmlDiffs); + + simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList.baseSimplifiedMNList.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.baseSimplifiedMNList.blockHash).to.equal( + initialSmlDiffs[0].blockHash, + ); + expect(simplifiedMNList.currentSML.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.currentSML.blockHash).to.equal( + initialSmlDiffs[initialSmlDiffs.length - 1].blockHash, + ); + }); + + it('should add diff to simplifiedMNList', async () => { + let simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList).to.deep.equal(undefined); + + simplifiedMasternodeList.applyDiffs(initialSmlDiffs); + + simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList.baseSimplifiedMNList.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.baseSimplifiedMNList.blockHash).to.equal( + initialSmlDiffs[0].blockHash, + ); + expect(simplifiedMNList.currentSML.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.currentSML.blockHash).to.equal( + initialSmlDiffs[initialSmlDiffs.length - 1].blockHash, + ); + + simplifiedMasternodeList.applyDiffs(updatedSmlDiffs); + + simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList.baseSimplifiedMNList.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.baseSimplifiedMNList.blockHash).to.equal( + initialSmlDiffs[0].blockHash, + ); + expect(simplifiedMNList.currentSML.baseBlockHash).to.equal( + initialSmlDiffs[0].baseBlockHash, + ); + expect(simplifiedMNList.currentSML.blockHash).to.equal( + updatedSmlDiffs[0].blockHash, + ); + }); + }); + + describe('#getStore', () => { + it('should return simplifiedMNList', async () => { + let simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList).to.deep.equal(undefined); + + simplifiedMasternodeList.applyDiffs(initialSmlDiffs); + + simplifiedMNList = simplifiedMasternodeList.getStore(); + + expect(simplifiedMNList).to.deep.equal(simplifiedMasternodeList.store); + }); + }); +}); diff --git a/packages/js-drive/test/integration/core/updateSimplifiedMasternodeListFactory.spec.js b/packages/js-drive/test/integration/core/updateSimplifiedMasternodeListFactory.spec.js new file mode 100644 index 00000000000..f494a542d87 --- /dev/null +++ b/packages/js-drive/test/integration/core/updateSimplifiedMasternodeListFactory.spec.js @@ -0,0 +1,92 @@ +const { startDashCore } = require('@dashevo/dp-services-ctl'); +const SimplifiedMNListStore = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListStore'); + +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); + +describe('updateSimplifiedMasternodeListFactory', function main() { + this.timeout(190000); + + let container; + let dashCore; + let dashCoreOptions; + + after(async () => { + if (dashCore) { + await dashCore.remove(); + } + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should wait until SML will be retrieved', async () => { + dashCore = await startDashCore(dashCoreOptions); + + container = await createTestDIContainer(dashCore); + + const simplifiedMasternodeList = container.resolve('simplifiedMasternodeList'); + + expect(simplifiedMasternodeList.getStore()).to.equal(undefined); + + const { result: randomAddress } = await dashCore.getApi().getNewAddress({ wallet: 'main' }); + + await dashCore.getApi().generateToAddress(1000, randomAddress); + + const updateSimplifiedMasternodeList = container.resolve('updateSimplifiedMasternodeList'); + + await updateSimplifiedMasternodeList(1000); + + expect(simplifiedMasternodeList.getStore()) + .to.be.an.instanceOf(SimplifiedMNListStore); + }); + + it('should update SML Store so other consumers can use it', async () => { + dashCore = await startDashCore(dashCoreOptions); + + container = await createTestDIContainer(dashCore); + + // Create initial state + const groveDBStore = container.resolve('groveDBStore'); + await groveDBStore.startTransaction(); + + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(true); + + const simplifiedMasternodeList = container.resolve('simplifiedMasternodeList'); + const updateSimplifiedMasternodeList = container.resolve('updateSimplifiedMasternodeList'); + const synchronizeMasternodeIdentities = container.resolve('synchronizeMasternodeIdentities'); + const smlMaxListsLimit = container.resolve('smlMaxListsLimit'); + + const api = dashCore.getApi(); + const { result: randomAddress } = await api.getNewAddress({ wallet: 'main' }); + + await api.generateToAddress(600, randomAddress); + + let blockNumber = 500; + + await updateSimplifiedMasternodeList(blockNumber); + await synchronizeMasternodeIdentities(blockNumber); + + expect(simplifiedMasternodeList.getStore()) + .to.be.an.instanceOf(SimplifiedMNListStore); + + blockNumber += smlMaxListsLimit; + + await updateSimplifiedMasternodeList(blockNumber); + await synchronizeMasternodeIdentities(blockNumber); + + expect(simplifiedMasternodeList.getStore()) + .to.be.an.instanceOf(SimplifiedMNListStore); + + blockNumber += smlMaxListsLimit; + + await updateSimplifiedMasternodeList(blockNumber); + await synchronizeMasternodeIdentities(blockNumber); + + expect(simplifiedMasternodeList.getStore()) + .to.be.an.instanceOf(SimplifiedMNListStore); + }); +}); diff --git a/packages/js-drive/test/integration/core/waitForCoreSyncFactory.spec.js b/packages/js-drive/test/integration/core/waitForCoreSyncFactory.spec.js new file mode 100644 index 00000000000..9859e6d8c96 --- /dev/null +++ b/packages/js-drive/test/integration/core/waitForCoreSyncFactory.spec.js @@ -0,0 +1,54 @@ +const { startDashCore } = require('@dashevo/dp-services-ctl'); + +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); + +describe('waitForCoreSyncFactory', function main() { + this.timeout(90000); + + let firstDashCore; + let secondDashCore; + let container; + let waitForCoreSync; + + after(async () => { + if (firstDashCore) { + await firstDashCore.remove(); + } + + if (secondDashCore) { + await secondDashCore.remove(); + } + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should wait until Dash Core in regtest mode with peers is synced', async () => { + firstDashCore = await startDashCore(); + const { result: randomAddress } = await firstDashCore.getApi().getNewAddress({ wallet: 'main' }); + await firstDashCore.getApi().generateToAddress(1000, randomAddress); + + secondDashCore = await startDashCore(); + await secondDashCore.connect(firstDashCore); + + container = await createTestDIContainer(secondDashCore); + waitForCoreSync = container.resolve('waitForCoreSync'); + + await waitForCoreSync(() => {}); + + const secondApi = secondDashCore.getApi(); + + const { + result: { + blocks: currentBlockHeight, + headers: currentHeadersNumber, + }, + } = await secondApi.getBlockchainInfo(); + + expect(currentHeadersNumber).to.equal(1000); + expect(currentBlockHeight).to.equal(1000); + }); +}); diff --git a/packages/js-drive/test/integration/createDIContainer.spec.js b/packages/js-drive/test/integration/createDIContainer.spec.js new file mode 100644 index 00000000000..13112ee7935 --- /dev/null +++ b/packages/js-drive/test/integration/createDIContainer.spec.js @@ -0,0 +1,40 @@ +const { expect } = require('chai'); + +const createTestDIContainer = require('../../lib/test/createTestDIContainer'); + +describe('createDIContainer', function describeContainer() { + this.timeout(25000); + + let container; + + beforeEach(async () => { + container = await createTestDIContainer(); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should create DI container', async () => { + expect(container).to.respondTo('register'); + expect(container).to.respondTo('resolve'); + }); + + describe('container', () => { + it('should resolve abciHandlers', () => { + const abciHandlers = container.resolve('abciHandlers'); + + expect(abciHandlers).to.have.property('info'); + expect(abciHandlers).to.have.property('checkTx'); + expect(abciHandlers).to.have.property('finalizeBlock'); + expect(abciHandlers).to.have.property('extendVote'); + expect(abciHandlers).to.have.property('initChain'); + expect(abciHandlers).to.have.property('prepareProposal'); + expect(abciHandlers).to.have.property('processProposal'); + expect(abciHandlers).to.have.property('verifyVoteExtension'); + expect(abciHandlers).to.have.property('query'); + }); + }); +}); diff --git a/packages/js-drive/test/integration/dataContract/DataContractStoreRepository.spec.js b/packages/js-drive/test/integration/dataContract/DataContractStoreRepository.spec.js new file mode 100644 index 00000000000..a8be383ef27 --- /dev/null +++ b/packages/js-drive/test/integration/dataContract/DataContractStoreRepository.spec.js @@ -0,0 +1,438 @@ +const rimraf = require('rimraf'); +const Drive = require('@dashevo/rs-drive'); +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const DataContract = require('@dashevo/dpp/lib/dataContract/DataContract'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const DataContractStoreRepository = require('../../../lib/dataContract/DataContractStoreRepository'); +const noopLogger = require('../../../lib/util/noopLogger'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('DataContractStoreRepository', () => { + let rsDrive; + let store; + let repository; + let decodeProtocolEntity; + let dataContract; + let blockInfo; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, noopLogger); + + await rsDrive.createInitialStateStructure(); + + decodeProtocolEntity = decodeProtocolEntityFactory(); + + repository = new DataContractStoreRepository(store, decodeProtocolEntity, noopLogger); + + dataContract = getDataContractFixture(); + + blockInfo = new BlockInfo(1, 1, Date.now()); + }); + + afterEach(async () => { + await rsDrive.close(); + rimraf.sync('./db/grovedb_test'); + }); + + describe('#create', () => { + it('should store Data Contract', async () => { + const result = await repository.create( + dataContract, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const encodedDataContractResult = await store.get( + DataContractStoreRepository.TREE_PATH.concat([dataContract.getId().toBuffer()]), + DataContractStoreRepository.DATA_CONTRACT_KEY, + ); + + const [protocolVersion, rawDataContract] = decodeProtocolEntity( + encodedDataContractResult.getValue(), + ); + + rawDataContract.protocolVersion = protocolVersion; + + const fetchedDataContract = new DataContract(rawDataContract); + + expect(dataContract.toObject()).to.deep.equal(fetchedDataContract.toObject()); + }); + + it('should store Data Contract using transaction', async () => { + await store.startTransaction(); + + const result = await repository.create( + dataContract, + blockInfo, + { useTransaction: true }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const notFoundDataContractResult = await store.get( + DataContractStoreRepository.TREE_PATH, + dataContract.getId().toBuffer(), + { useTransaction: false }, + ); + + expect(notFoundDataContractResult.getValue()).to.be.null(); + + const dataFromTransactionResult = await store.get( + DataContractStoreRepository.TREE_PATH.concat([dataContract.getId().toBuffer()]), + DataContractStoreRepository.DATA_CONTRACT_KEY, + { useTransaction: true }, + ); + + let [protocolVersion, rawDataContract] = decodeProtocolEntity( + dataFromTransactionResult.getValue(), + ); + + rawDataContract.protocolVersion = protocolVersion; + + const fetchedDataContract = new DataContract(rawDataContract); + + expect(dataContract.toObject()).to.deep.equal(fetchedDataContract.toObject()); + + await store.commitTransaction(); + + const committedDataResult = await store.get( + DataContractStoreRepository.TREE_PATH.concat([dataContract.getId().toBuffer()]), + DataContractStoreRepository.DATA_CONTRACT_KEY, + ); + + [protocolVersion, rawDataContract] = decodeProtocolEntity(committedDataResult.getValue()); + + rawDataContract.protocolVersion = protocolVersion; + + const fetchedOneMoreDataContract = new DataContract(rawDataContract); + + expect(dataContract.toObject()).to.deep.equal(fetchedOneMoreDataContract.toObject()); + }); + + it('should not store Data Contract with dry run', async () => { + const result = await repository.create( + dataContract, + blockInfo, + { dryRun: true }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const encodedDataContractResult = await store.get( + DataContractStoreRepository.TREE_PATH, + dataContract.getId().toBuffer(), + ); + + expect(encodedDataContractResult.isNull()).to.be.true(); + }); + }); + + describe('#update', () => { + beforeEach(async () => { + await repository.create(dataContract, blockInfo); + }); + + it('should store Data Contract', async () => { + const result = await repository.update( + dataContract, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const encodedDataContractResult = await store.get( + DataContractStoreRepository.TREE_PATH.concat([dataContract.getId().toBuffer()]), + DataContractStoreRepository.DATA_CONTRACT_KEY, + ); + + const [protocolVersion, rawDataContract] = decodeProtocolEntity( + encodedDataContractResult.getValue(), + ); + + rawDataContract.protocolVersion = protocolVersion; + + const fetchedDataContract = new DataContract(rawDataContract); + + expect(dataContract.toObject()).to.deep.equal(fetchedDataContract.toObject()); + }); + + it('should store Data Contract using transaction', async () => { + await store.startTransaction(); + + dataContract.incrementVersion(); + + const result = await repository.update( + dataContract, + blockInfo, + { useTransaction: true }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const dataContractWithoutTransactionResult = await repository.fetch(dataContract.getId()); + + expect(dataContractWithoutTransactionResult.isNull()).to.be.false(); + expect(dataContractWithoutTransactionResult.getValue().getVersion()).to.equals(1); + + const dataContractWithTransactionResult = await repository.fetch(dataContract.getId(), { + useTransaction: true, + }); + + expect(dataContractWithTransactionResult.isNull()).to.be.false(); + + const fetchedDataContract = dataContractWithTransactionResult.getValue(); + + expect(fetchedDataContract.getVersion()).to.equals(2); + + expect(fetchedDataContract.toObject()).to.deep.equal(dataContract.toObject()); + }); + + it('should not store Data Contract with dry run', async () => { + dataContract.incrementVersion(); + + const result = await repository.update( + dataContract, + blockInfo, + { dryRun: true }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const dataContractResult = await repository.fetch(dataContract.getId()); + + expect(dataContractResult.isNull()).to.be.false(); + + const fetchedDataContract = dataContractResult.getValue(); + + expect(fetchedDataContract.getVersion()).to.equals(1); + + expect(fetchedDataContract.toObject()).to.not.deep.equal(dataContract.toObject()); + }); + }); + + describe('#fetch', () => { + it('should should fetch null if Data Contract not found', async () => { + const result = await repository.fetch(dataContract.getId()); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch Data Contract', async () => { + await store.getDrive().createContract(dataContract, blockInfo, false); + + const result = await repository.fetch(dataContract.getId()); + + expect(result).to.be.instanceOf(StorageResult); + + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + const storedDataContract = result.getValue(); + + expect(storedDataContract).to.be.an.instanceof(DataContract); + expect(storedDataContract.toObject()).to.deep.equal(storedDataContract.toObject()); + }); + + it('should fetch Data Contract using transaction', async () => { + await store.startTransaction(); + + await store.getDrive().createContract(dataContract, blockInfo, true); + + const notFoundDataContractResult = await repository.fetch(dataContract.getId(), { + useTransaction: false, + }); + + expect(notFoundDataContractResult.isNull()).to.be.true(); + + const transactionalDataContractResult = await repository.fetch(dataContract.getId(), { + useTransaction: true, + }); + + const transactionalDataContract = transactionalDataContractResult.getValue(); + + expect(transactionalDataContract).to.be.an.instanceof(DataContract); + expect(transactionalDataContract.toObject()).to.deep.equal(dataContract.toObject()); + + await store.commitTransaction(); + + const storedDataContractResult = await repository.fetch(dataContract.getId()); + + const storedDataContract = storedDataContractResult.getValue(); + + expect(storedDataContract).to.be.an.instanceof(DataContract); + expect(storedDataContract.toObject()).to.deep.equal(dataContract.toObject()); + }); + + it('should fetch null on dry run', async () => { + await store.getDrive().createContract(dataContract, blockInfo, false); + + const result = await repository.fetch(dataContract.getId(), { dryRun: true }); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + }); + + describe('#prove', () => { + it('should should return proof if Data Contract not found', async () => { + const result = await repository.prove(dataContract.getId()); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof', async () => { + await store.getDrive().createContract(dataContract, blockInfo, false); + + const result = await repository.prove(dataContract.getId()); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + // TODO enable this test when we support transactions + it.skip('should return proof using transaction', async () => { + await store.startTransaction(); + + await store.getDrive().createContract(dataContract, blockInfo, true); + + const notFoundDataContractResult = await repository.prove(dataContract.getId(), { + useTransaction: false, + }); + + expect(notFoundDataContractResult.isNull()).to.be.true(); + + const transactionalDataContractResult = await repository.prove(dataContract.getId(), { + useTransaction: true, + }); + + const transactionalDataContract = transactionalDataContractResult.getValue(); + + expect(transactionalDataContract).to.be.an.instanceof(Buffer); + + await store.commitTransaction(); + + const storedDataContractResult = await repository.prove(dataContract.getId()); + + const storedDataContract = storedDataContractResult.getValue(); + + expect(storedDataContract).to.be.an.instanceof(Buffer); + }); + }); + + describe('#proveMany', () => { + let dataContract2; + + beforeEach(async () => { + dataContract2 = new DataContract(dataContract.toObject()); + dataContract2.id = generateRandomIdentifier(); + }); + + it('should should return proof if Data Contract not found', async () => { + const result = await repository.proveMany([dataContract.getId(), dataContract2.getId()]); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof', async () => { + await store.getDrive().createContract(dataContract, blockInfo, false); + await store.getDrive().createContract(dataContract2, blockInfo, false); + + const result = await repository.proveMany([dataContract.getId(), dataContract2.getId()]); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equals(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + // TODO enable this test when we support transactions + it.skip('should return proof using transaction', async () => { + await store.startTransaction(); + + await store.getDrive().createContract(dataContract, blockInfo, true); + await store.getDrive().createContract(dataContract2, blockInfo, true); + + const notFoundDataContractResult = await repository.prove( + [dataContract.getId(), dataContract2.getId()], { + useTransaction: false, + }, + ); + + expect(notFoundDataContractResult.getValue()).to.be.null(); + + const transactionalDataContractResult = await repository.proveMany( + [dataContract.getId(), dataContract2.getId()], + { useTransaction: true }, + ); + + const transactionalDataContract = transactionalDataContractResult.getValue(); + + expect(transactionalDataContract).to.be.an.instanceof(Buffer); + + await store.commitTransaction(); + + const storedDataContractResult = await repository.proveMany( + [dataContract.getId(), dataContract2.getId()], + ); + + const storedDataContract = storedDataContractResult.getValue(); + + expect(storedDataContract).to.be.an.instanceof(Buffer); + }); + }); +}); diff --git a/packages/js-drive/test/integration/document/DocumentRepository.spec.js b/packages/js-drive/test/integration/document/DocumentRepository.spec.js new file mode 100644 index 00000000000..803e6673f77 --- /dev/null +++ b/packages/js-drive/test/integration/document/DocumentRepository.spec.js @@ -0,0 +1,3408 @@ +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const Document = require('@dashevo/dpp/lib/document/Document'); +const DataContractFactory = require('@dashevo/dpp/lib/dataContract/DataContractFactory'); +const createDPPMock = require('@dashevo/dpp/lib/test/mocks/createDPPMock'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); +const createDocumentTypeTreePath = require('../../../lib/document/groveDB/createDocumentTreePath'); +const InvalidQueryError = require('../../../lib/document/errors/InvalidQueryError'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +function ucFirst(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +const typesTestCases = { + number: { + type: 'number', + value: 1, + }, + boolean: { + type: 'boolean', + value: true, + }, + string: { + type: 'string', + value: 'test', + }, + null: { + type: 'null', + value: null, + }, + undefined: { + type: 'undefined', + value: undefined, + }, + object: { + type: 'object', + value: {}, + }, + buffer: { + type: 'buffer', + value: Buffer.alloc(32), + }, +}; + +const notObjectTestCases = [ + typesTestCases.number, + typesTestCases.boolean, + typesTestCases.string, + typesTestCases.null, +]; + +const notArrayTestCases = [ + typesTestCases.number, + typesTestCases.boolean, + typesTestCases.string, + typesTestCases.null, + typesTestCases.object, +]; + +const nonScalarTestCases = [ + typesTestCases.null, + typesTestCases.undefined, + typesTestCases.object, +]; + +const scalarTestCases = [ + typesTestCases.number, + typesTestCases.string, + typesTestCases.boolean, + typesTestCases.buffer, +]; + +const nonNumberTestCases = [ + typesTestCases.string, + typesTestCases.boolean, + typesTestCases.null, + typesTestCases.undefined, + typesTestCases.object, + typesTestCases.buffer, +]; + +const nonNumberNullAndUndefinedTestCases = [ + typesTestCases.string, + typesTestCases.boolean, + typesTestCases.object, + typesTestCases.buffer, +]; + +const validFieldNameTestCases = [ + 'a', + 'a.b', + 'a.b.c', + 'array.element', + 'a.0', + 'a.0.b', + 'a_._b', + 'a-b.c_', + '$id', + '$ownerId', + '$createdAt', + '$updatedAt', +]; + +const invalidFieldNameTestCases = [ + '$a', + '$#1321', + 'a...', + '.a', + 'a.b.c.', +]; + +const validOrderByOperators = [ + { + operator: '>', + value: 42, + documentType: 'documentNumber', + }, + { + operator: '<', + value: 42, + documentType: 'documentNumber', + }, + { + operator: 'startsWith', + value: 'rt-', + documentType: 'documentString', + }, + { + operator: 'in', + value: [1, 2], + documentType: 'documentNumber', + }, +]; + +const queryDocumentSchema = { + testDocument: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + lastName: { + type: 'string', + }, + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + d: { + type: 'integer', + }, + e: { + type: 'integer', + }, + }, + required: ['$createdAt'], + additionalProperties: false, + indices: [ + { + name: 'one', + properties: [ + { firstName: 'asc' }, + ], + }, + { + name: 'two', + properties: [ + { a: 'asc' }, + { b: 'asc' }, + { c: 'asc' }, + { d: 'asc' }, + { e: 'asc' }, + ], + }, + { + name: 'three', + properties: [ + { firstName: 'asc' }, + { lastName: 'asc' }, + ], + }, + ], + }, + documentA: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + }, + additionalProperties: false, + indices: [ + { + name: 'one', + properties: [{ $id: 'asc' }], + }, + ], + }, + documentB: { + type: 'object', + additionalProperties: false, + properties: { + firstName: { + type: 'string', + }, + }, + indices: [ + { + properties: [{ $id: 'asc' }], + unique: true, + }, + ], + }, + documentC: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + }, + indices: [ + { + properties: [{ a: 'asc' }, { b: 'asc' }], + }, + ], + }, + documentD: { + // no index + type: 'object', + additionalProperties: false, + properties: { + firstName: { + type: 'string', + }, + }, + }, + documentE: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'string', + }, + b: { + type: 'string', + }, + }, + indices: [ + { + properties: [{ a: 'asc' }, { b: 'asc' }], + }, + ], + }, + documentF: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + }, + indices: [ + { + properties: [{ a: 'asc' }, { b: 'asc' }, { c: 'asc' }], + }, + ], + }, + documentG: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + }, + indices: [ + { + properties: [{ b: 'asc' }, { a: 'asc' }], + }, + { + properties: [{ a: 'asc' }, { b: 'asc' }], + }, + ], + }, + documentH: { + type: 'object', + additionalProperties: false, + properties: { + firstName: { + type: 'string', + }, + }, + indices: [ + { + properties: [{ $updatedAt: 'asc' }], + }, + ], + required: ['$updatedAt'], + }, + documentI: { + type: 'object', + additionalProperties: false, + properties: { + firstName: { + type: 'string', + }, + }, + indices: [ + { + properties: [{ $createdAt: 'asc' }], + }, + ], + required: ['$createdAt'], + }, + documentJ: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + d: { + type: 'integer', + }, + e: { + type: 'integer', + }, + }, + indices: [ + { + name: 'index1', + properties: [ + { a: 'asc' }, + { b: 'asc' }, + { c: 'asc' }, + { d: 'asc' }, + { e: 'asc' }, + ], + unique: true, + }, + ], + }, + documentK: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'string', + }, + b: { + type: 'string', + }, + }, + indices: [ + { + properties: [{ b: 'asc' }], + }, + ], + }, + documentL: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'integer', + }, + b: { + type: 'integer', + }, + c: { + type: 'integer', + }, + d: { + type: 'integer', + }, + }, + indices: [ + { + name: 'index1', + properties: [ + { a: 'asc' }, + { b: 'asc' }, + { c: 'asc' }, + { d: 'asc' }, + ], + unique: true, + }, + ], + }, +}; + +for (const fieldName of validFieldNameTestCases) { + queryDocumentSchema[`document${fieldName}`] = { + type: 'object', + properties: { + [fieldName]: { + type: 'integer', + }, + }, + additionalProperties: false, + indices: [ + { + name: 'one', + properties: [{ [fieldName]: 'asc' }], + }, + ], + }; +} + +for (const type of ['number', 'string', 'boolean', 'buffer']) { + const properties = { + a: { + type, + }, + }; + + if (type === 'buffer') { + properties.a.type = 'array'; + properties.a.byteArray = true; + } + + queryDocumentSchema[`document${ucFirst(type)}`] = { + type: 'object', + properties, + additionalProperties: false, + indices: [ + { + name: 'one', + properties: [{ a: 'asc' }], + }, + ], + }; +} + +queryDocumentSchema.documentBig = { + type: 'object', + properties: Array(256).fill().map((v, i) => `a${i}`).reduce((res, key) => { + res[key] = { + type: 'integer', + }; + + return res; + }, {}), + additionalProperties: false, + indices: Array(256).fill().map((v, i) => ({ + properties: [{ [`a${i}`]: 'asc' }], + })), +}; + +const validQueries = [ + {}, + { + where: [['$id', 'in', [ + generateRandomIdentifier(), + generateRandomIdentifier(), + generateRandomIdentifier(), + ]]], + orderBy: [['$id', 'asc']], + }, + { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['c', '==', 3], + ['d', 'in', [1, 2]], + ], + orderBy: [ + ['d', 'desc'], + ['e', 'asc'], + ], + }, + { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['c', '==', 3], + ['d', 'in', [1, 2]], + ['e', '>', 3], + ], + orderBy: [ + ['d', 'desc'], + ['e', 'asc'], + ], + }, + { + where: [ + ['firstName', '>', 'Chris'], + ['firstName', '<=', 'Noellyn'], + ], + orderBy: [ + ['firstName', 'asc'], + ], + }, + { + where: [ + ['firstName', '==', '1'], + ['lastName', '==', '2'], + ], + limit: 1, + }, +]; + +const invalidQueries = [ + { + query: { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ], + }, + error: 'query is too far from index: query must better match an existing index', + }, + { + query: { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['c', 'in', [1, 2]], + ], + orderBy: [ + ['c', 'desc'], + ], + }, + error: 'where clause on non indexed property error: query must be for valid indexes', + }, + { + query: { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['b', 'in', [1, 2]], + ], + orderBy: [ + ['b', 'desc'], + ], + }, + error: 'duplicate non groupable clause on same field error: in clause has same field as an equality clause', + }, + { + query: { + where: [ + ['z', '==', 1], + ], + }, + error: 'where clause on non indexed property error: query must be for valid indexes', + }, + { + query: { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['c', '>', 3], + ['d', 'in', [1, 2]], + ['e', '>', 3], + ], + }, + error: 'multiple range clauses error: all ranges must be on same field', + }, + { + query: { + where: [ + ['a', '==', 1], + ['b', '==', 2], + ['c', '>', 3], + ['d', '>', 3], + ], + orderBy: [ + ['c', 'asc'], + ['d', 'desc'], + ], + }, + error: 'multiple range clauses error: all ranges must be on same field', + }, + { + query: { + where: [ + ['a', '==', 3], + ['b', '==', 2], + ['c', '>', 1], + ], + }, + error: 'missing order by for range error: query must have an orderBy field for each range element', + }, + { + query: { + where: [ + ['a', '==', 3], + ['b', '==', 2], + ['c', '==', 3], + ['d', 'in', [1, 2]], + ['e', '<', 1], + ], + orderBy: [ + ['e', 'asc'], + ['d', 'asc'], + ], + }, + error: 'where clause on non indexed property error: query must be for valid indexes', + }, + { + query: 'abc', + error: 'deserialization error: unable to decode query from cbor', + }, + { + query: [], + error: 'deserialization error: unable to decode query from cbor', + }, + { + query: { where: [1, 2, 3] }, + error: 'query invalid format for where clause error: where clause must be an array', + }, + { + query: { invalid: 'query' }, + error: 'unsupported error: unsupported syntax in where clause', + }, +]; + +const invalidOperators = ['<<', '<==', '===', '!>', '>>=']; + +async function createDocuments(documentRepository, documents, blockInfo) { + return Promise.all( + documents.map(async (o) => { + const result = await documentRepository.create(o, blockInfo); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + }), + ); +} + +describe('DocumentRepository', function main() { + this.timeout(30000); + + let documentRepository; + let dataContractRepository; + let container; + let dataContract; + let queryDataContract; + let documents; + let document; + let documentSchema; + let blockInfo; + + beforeEach(async function beforeEach() { + const now = 86400; + container = await createTestDIContainer(); + + dataContract = getDataContractFixture(); + documents = getDocumentsFixture(dataContract).slice(0, 5); + + [document] = documents; + + // Modify documents for the test cases + documents = documents.map((doc, i) => { + const currentDocument = doc; + // const arrayItem = { item: i + 1, flag: true }; + + currentDocument.set('order', i); + currentDocument.set('$createdAt', now); + // currentDocument.set('arrayWithScalar', Array(i + 1) + // .fill(1) + // .map((item, index) => i + index)); + // currentDocument.set('arrayWithObjects', Array(i + 1).fill(arrayItem)); + currentDocument.type = document.getType(); + + return currentDocument; + }); + + [document] = documents; + + dataContract.documents[document.getType()].properties = { + ...dataContract.documents[document.getType()].properties, + name: { + type: 'string', + maxLength: 63, + }, + order: { + type: 'number', + }, + lastName: { + type: 'string', + maxLength: 63, + }, + // arrayWithScalar: { + // type: 'array', + // items: [ + // { type: 'string' }, + // ], + // }, + // arrayWithObjects: { + // type: 'array', + // items: { + // type: 'object', + // properties: { + // flag: { + // type: 'string', + // }, + // }, + // }, + // }, + }; + + const documentsSchema = dataContract.getDocuments(); + + documentSchema = documentsSchema[document.getType()]; + + // redeclare indices + const indices = documentSchema.indices || []; + documentSchema.indices = indices.concat([ + { + name: 'index1', + properties: [{ name: 'asc' }], + }, + // { + // name: 'index2', + // + // properties: [{ name: 'asc' }, { 'arrayWithObjects.item': 'asc' }], + // }, + { + name: 'index3', + properties: [{ order: 'asc' }], + }, + { + name: 'index4', + properties: [{ lastName: 'asc' }], + }, + // { + // name: 'index5', + // properties: [{ arrayWithScalar: 'asc' }], + // }, + // { + // name: 'index6', + // properties: [{ arrayWithObjects: 'asc' }], + // }, + // { + // name: 'index7', + // properties: [{ 'arrayWithObjects.item': 'asc' }], + // }, + // { + // name: 'index8', + // properties: [{ 'arrayWithObjects.flag': 'asc' }], + // }, + // { + // name: 'index9', + // properties: [{ primaryOrder: 'asc' }, { order: 'desc' }], + // }, + { + name: 'index10', + properties: [{ $ownerId: 'asc' }], + }, + ]); + + const dpp = container.resolve('dpp'); + queryDataContract = dpp.dataContract.create(generateRandomIdentifier(), queryDocumentSchema); + + documentRepository = container.resolve('documentRepository'); + documentRepository.logger = { + info: this.sinon.stub(), + }; + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(); + + dataContractRepository = container.resolve('dataContractRepository'); + + blockInfo = new BlockInfo(1, 1, Date.now()); + + await dataContractRepository.create(dataContract, blockInfo); + await dataContractRepository.create(queryDataContract, blockInfo); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + describe('#create', () => { + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + }); + + it('should create Document', async () => { + const documentTypeTreePath = createDocumentTypeTreePath( + document.getDataContract(), + document.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + const result = await documentRepository + .storage + .db + .get(documentTreePath, document.getId().toBuffer(), false); + + expect(document.toBuffer()).to.deep.equal(result.value); + }); + + it('should create Document in transaction', async () => { + await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + ); + + await documentRepository + .storage + .startTransaction(); + + await documentRepository.create(document, blockInfo, { + useTransaction: true, + }); + + const documentTypeTreePath = createDocumentTypeTreePath( + document.getDataContract(), + document.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + const transactionDocument = await documentRepository + .storage + .db + .get(documentTreePath, document.getId().toBuffer(), true); + + try { + await documentRepository + .storage + .db + .get(documentTreePath, document.getId().toBuffer(), false); + + expect.fail('should fail with NotFoundError error'); + } catch (e) { + expect(e.message.startsWith('grovedb: path key not found: key not found in Merk')).to.be.true(); + } + + await documentRepository.storage.commitTransaction(); + + const createdDocument = await documentRepository + .storage + .db + .get(documentTreePath, document.getId().toBuffer(), false); + + expect(document.toBuffer()).to.deep.equal(transactionDocument.value); + expect(document.toBuffer()).to.deep.equal(createdDocument.value); + }); + + it('should not create Document on dry run', async () => { + await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + ); + + await documentRepository.create(document, blockInfo, { + dryRun: true, + }); + + const documentTypeTreePath = createDocumentTypeTreePath( + document.getDataContract(), + document.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + try { + await documentRepository + .storage + .db + .get(documentTreePath, document.getId().toBuffer()); + + expect.fail('should fail with NotFoundError error'); + } catch (e) { + expect(e.message.startsWith('grovedb: path key not found: key not found in Merk')).to.be.true(); + } + }); + }); + + describe('#update', () => { + let replaceDocument; + + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + + replaceDocument = new Document({ + ...documents[1].toObject(), + lastName: 'NotSoShiny', + }, dataContract); + }); + + it('should update Document', async () => { + const updateResult = await documentRepository.update(replaceDocument, blockInfo); + + expect(updateResult).to.be.instanceOf(StorageResult); + expect(updateResult.getOperations().length).to.be.greaterThan(0); + + const documentTypeTreePath = createDocumentTypeTreePath( + replaceDocument.getDataContract(), + replaceDocument.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + const result = await documentRepository + .storage + .db + .get(documentTreePath, replaceDocument.getId().toBuffer(), false); + + expect(replaceDocument.toBuffer()).to.deep.equal(result.value); + }); + + it('should store Document in transaction', async () => { + await documentRepository + .storage + .startTransaction(); + + const updateResult = await documentRepository.update(replaceDocument, blockInfo, { + useTransaction: true, + }); + + expect(updateResult).to.be.instanceOf(StorageResult); + expect(updateResult.getOperations().length).to.be.greaterThan(0); + + const documentTypeTreePath = createDocumentTypeTreePath( + replaceDocument.getDataContract(), + replaceDocument.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + const transactionDocument = await documentRepository + .storage + .db + .get(documentTreePath, replaceDocument.getId().toBuffer(), true); + + const notUpdatedDocument = await documentRepository + .storage + .db + .get(documentTreePath, replaceDocument.getId().toBuffer(), false); + + await documentRepository.storage.commitTransaction(); + + const createdDocument = await documentRepository + .storage + .db + .get(documentTreePath, replaceDocument.getId().toBuffer(), false); + + expect(replaceDocument.toBuffer()).to.deep.equal(transactionDocument.value); + expect(replaceDocument.toBuffer()).to.deep.equal(createdDocument.value); + expect(documents[1].toBuffer()).to.deep.equal(notUpdatedDocument.value); + }); + + it('should not update Document on dry run', async () => { + const updateResult = await documentRepository.update(replaceDocument, blockInfo, { + dryRun: true, + }); + + expect(updateResult).to.be.instanceOf(StorageResult); + expect(updateResult.getOperations().length).to.be.greaterThan(0); + + const documentTypeTreePath = createDocumentTypeTreePath( + replaceDocument.getDataContract(), + replaceDocument.getType(), + ); + + const documentTreePath = documentTypeTreePath.concat( + [Buffer.from([0])], + ); + + const notUpdatedDocument = await documentRepository + .storage + .db + .get(documentTreePath, replaceDocument.getId().toBuffer(), false); + + expect(documents[1].toBuffer()).to.deep.equal(notUpdatedDocument.value); + }); + }); + + describe('#find', () => { + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + }); + + it('should find all existing documents', async () => { + const result = await documentRepository.find(dataContract, document.getType()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(documents.length); + + const foundDocumentsBuffers = foundDocuments.map((doc) => doc.toBuffer()); + + expect(foundDocumentsBuffers).to.have.deep.members(documents.map((doc) => doc.toBuffer())); + }); + + it('should find all existing documents in transaction', async () => { + await documentRepository + .storage + .startTransaction(); + + const foundDocumentsResult = await documentRepository + .find(dataContract, document.getType(), { + useTransaction: true, + }); + + expect(foundDocumentsResult).to.be.instanceOf(StorageResult); + expect(foundDocumentsResult.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = foundDocumentsResult.getValue(); + + await documentRepository.storage.commitTransaction(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(documents.length); + + const foundDocumentsBuffers = foundDocuments.map((doc) => doc.toBuffer()); + + expect(foundDocumentsBuffers).to.have.deep.members(documents.map((doc) => doc.toBuffer())); + }); + + it('should fetch Documents with dry run', async () => { + await documentRepository + .storage + .startTransaction(); + + const foundDocumentsResult = await documentRepository + .find(dataContract, document.getType(), { + dryRun: true, + }); + + expect(foundDocumentsResult).to.be.instanceOf(StorageResult); + expect(foundDocumentsResult.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = foundDocumentsResult.getValue(); + + await documentRepository.storage.commitTransaction(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(documents.length); + + const foundDocumentsBuffers = foundDocuments.map((doc) => doc.toBuffer()); + + expect(foundDocumentsBuffers).to.have.deep.members(documents.map((doc) => doc.toBuffer())); + }); + + describe('queries', () => { + describe('valid queries', () => { + validQueries.forEach((query) => { + it(`should return valid result for query "${JSON.stringify(query)}"`, async () => { + const result = await documentRepository.find(queryDataContract, 'testDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + it('should return valid result if data contract has only system properties', async () => { + const schema = { + chat: { + type: 'object', + indices: [ + { + name: 'ownerAndCreatedAt', + properties: [ + { + $ownerId: 'asc', + }, + { + $createdAt: 'asc', + }, + ], + }, + ], + properties: { + test: { + type: 'string', + }, + }, + required: ['$createdAt'], + additionalProperties: false, + }, + }; + + const factory = new DataContractFactory(createDPPMock(), () => {}); + const ownerId = generateRandomIdentifier(); + const myDataContract = factory.create(ownerId, schema); + await dataContractRepository.create(myDataContract, blockInfo); + + const result = await documentRepository.find(myDataContract, 'chat', { + where: [ + ['$ownerId', '==', ownerId], + ['$createdAt', '>', new Date().getTime()], + ], + orderBy: [['$createdAt', 'asc']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return valid result for DPNS contract', async () => { + const schema = { + label: { + type: 'object', + properties: { + normalizedLabel: { + type: 'string', + }, + normalizedParentDomainName: { + type: 'string', + }, + }, + indices: [ + { + name: 'index1', + properties: [ + { + normalizedParentDomainName: 'asc', + }, + { + normalizedLabel: 'asc', + }, + ], + unique: true, + }, + ], + }, + }; + + const factory = new DataContractFactory(createDPPMock(), () => {}); + const ownerId = generateRandomIdentifier(); + const myDataContract = factory.create(ownerId, schema); + await dataContractRepository.create(myDataContract, blockInfo); + + const result = await documentRepository.find(myDataContract, 'label', { + where: [ + ['normalizedParentDomainName', '==', 'dash'], + ], + orderBy: [['normalizedLabel', 'asc']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + describe('invalid queries', () => { + invalidQueries.forEach(({ query, error }) => { + it(`should throw InvalidQueryError for query "${JSON.stringify(query)}"`, async () => { + try { + await documentRepository.find(queryDataContract, 'testDocument', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(error); + } + }); + }); + + notObjectTestCases.forEach(({ type, value: query }) => { + it(`should return invalid result if query is a ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentA', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('deserialization error: unable to decode query from cbor'); + } + }); + }); + }); + + describe('where', () => { + it('should return empty array if where clause conditions do not match', async () => { + const query = { + where: [['name', '==', 'Dash enthusiast']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.have.lengthOf(0); + }); + + it.skip('should find documents by nested object fields', async () => { + const query = { + where: [ + ['arrayWithObjects.item', '==', 2], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.an('array'); + expect(result).to.be.lengthOf(1); + + const [expectedDocument] = result; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[1].toBuffer()); + }); + + it.skip('should return documents by several conditions', async () => { + const query = { + where: [ + ['name', '==', 'Cutie'], + ['arrayWithObjects.item', '==', 1], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.an('array'); + expect(result).to.be.lengthOf(1); + + const [expectedDocument] = result; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[0].toBuffer()); + }); + + notArrayTestCases.forEach(({ type, value: query }) => { + it(`should return invalid result if "where" is not an array, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentA', { where: query }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('query invalid format for where clause error: where clause must be an array'); + } + }); + }); + + it('should return invalid result if "where" contains more than 10 conditions', async () => { + const where = Array(11).fill(['a', '<', 1]); + try { + await documentRepository.find(queryDataContract, 'documentA', { where }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('multiple range clauses error: there can only be at most 2 range clauses that must be on the same field'); + } + }); + + it('should return invalid result if "where" contains conflicting conditions', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '<', 1], + ['a', '>', 1], + ], + orderBy: [['a', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); + + it('should return invalid result if number of properties queried does not match number of indexed ones minus 2', async () => { + try { + await documentRepository.find(queryDataContract, 'documentL', { + where: [ + ['a', '==', 1], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('query is too far from index: query must better match an existing index'); + } + }); + + describe('condition', () => { + describe('property', () => { + it('should return valid result if condition contains "$id" field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentB', { + where: + [['$id', '==', generateRandomIdentifier()]], + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.isEmpty()).to.be.true(); + }); + + it('should return valid result if condition contains top-level field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentE', { + where: [ + ['a', '==', '1'], + ], + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.isEmpty()).to.be.true(); + }); + + it.skip('should return valid result if condition contains nested path field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentD', { + where: + [['a.b', '==', '1']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return invalid result if property is not specified in document indices', async () => { + try { + await documentRepository.find(queryDataContract, 'documentD', { + where: [ + ['a', '==', '1'], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + }); + + it('should return invalid result if condition array has less than 3 elements (field, operator, value)', async () => { + try { + await documentRepository.find(queryDataContract, 'documentA', { + where: + [['a', '==']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid where clause components error: where clauses should have at most 3 components'); + } + }); + + it('should return invalid result if condition array has more than 3 elements (field, operator, value)', async () => { + try { + await documentRepository.find(queryDataContract, 'documentA', { + where: [ + [['a', '==', '1', '2']], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid where clause components error: where clauses should have at most 3 components'); + } + }); + + describe('operators', () => { + describe('comparisons', () => { + invalidOperators.forEach((operator) => { + it('should return invalid result if condition contains invalid comparison operator', async () => { + const query = { where: [['a', operator, '1']] }; + if (operator !== '===') { + query.orderBy = [['a', 'asc']]; + } + + try { + await documentRepository.find(queryDataContract, 'documentE', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid where clause components error: second field of where component should be a known operator'); + } + }); + }); + + describe('<', () => { + it('should find documents with "<" operator', async () => { + const query = { + where: [['order', '<', documents[1].get('order')]], + orderBy: [['order', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[0].toBuffer()); + }); + + it('should return invalid result if "<" operator used with a string value longer than 255 bytes', async () => { + const longString = 't'.repeat(255); + + const result = await documentRepository.find( + queryDataContract, + 'documentString', + { + where: [['a', '<', longString]], + orderBy: [['a', 'asc']], + }, + ); + + expect(result).to.be.instanceOf(StorageResult); + + const veryLongString = 't'.repeat(256); + + try { + await documentRepository.find( + queryDataContract, + 'documentString', + { + where: [['a', '<', veryLongString]], + orderBy: [['a', 'asc']], + }, + ); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('field requirement unmet: value must be less than 256 bytes long'); + } + }); + + nonScalarTestCases.forEach(({ type, value }) => { + it(`should return invalid result if "<" operator used with a not scalar value, but ${type}`, async function it() { + if ((typeof value === 'object' && value === null) || typeof value === 'undefined') { + this.skip('will be implemented later'); + } + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', '<', value]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: structure error: value is not a float'); + } + }); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if "<" operator used with a scalar value ${type}`, async () => { + const docType = `document${ucFirst(type)}`; + + const result = await documentRepository.find(queryDataContract, docType, { where: [['a', '<', value]], orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + }); + + describe('<=', () => { + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if "<=" operator used with a scalar value ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, `document${ucFirst(type)}`, { where: [['a', '<=', value]], orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + it('should find Documents using "<=" operator', async () => { + const query = { + where: [['order', '<=', documents[1].get('order')]], + orderBy: [['order', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(2); + + const expectedDocuments = documents.slice(0, 2).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members( + expectedDocuments, + ); + }); + }); + + describe('==', () => { + it('should find existing documents using "==" operator', async () => { + const query = { + where: [['name', '==', document.get('name')]], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(document.toBuffer()); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if "==" operator used with a scalar value ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, `document${ucFirst(type)}`, { where: [['a', '==', value]] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + }); + + describe('=>', () => { + it('should find existing documents using ">=" operator', async () => { + const query = { + where: [['order', '>=', documents[1].get('order')]], + orderBy: [['order', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(documents.length - 1); + + documents.shift(); + const expectedDocuments = documents + .map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members( + expectedDocuments, + ); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if ">=" operator used with a scalar value ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, `document${ucFirst(type)}`, { where: [['a', '>=', value]], orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + }); + + describe('>', () => { + it('should find existing documents using ">" operator', async () => { + const query = { + where: [['order', '>', documents[1].get('order')]], + orderBy: [['order', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(documents.length - 2); + + const expectedDocuments = documents + .splice(2, documents.length) + .map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members( + expectedDocuments, + ); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if ">" operator used with a scalar value ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, `document${ucFirst(type)}`, { where: [['a', '>', value]], orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + }); + + ['>', '<', '<=', '>='].forEach((operator) => { + it(`should return invalid results if "${operator}" used not in the last 2 where conditions`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', operator, 1], + ['a', 'startsWith', 'rt-'], + ['a', 'startsWith', 'r-'], + ], + orderBy: [['a', 'asc']], + }); + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('range clauses not groupable error: clauses are not groupable'); + } + }); + }); + + describe('ranges', () => { + describe('multiple ranges', () => { + ['>', '<', '<=', '>='].forEach((firstOperator) => { + ['>', '<', '>=', '<='].forEach((secondOperator) => { + it(`should return invalid result if ${firstOperator} operator used with ${secondOperator} operator`, async () => { + const query = { where: [['a', firstOperator, '1'], ['b', secondOperator, 'a']] }; + + try { + await documentRepository.find(queryDataContract, 'documentE', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('multiple range clauses error: all ranges must be on same field'); + } + }); + }); + }); + + ['>', '<', '<=', '>='].forEach((firstOperator) => { + it(`should return invalid result if ${firstOperator} operator used with startsWith operator`, async () => { + const query = { where: [['a', firstOperator, '1'], ['b', 'startsWith', 'a']] }; + + try { + await documentRepository.find(queryDataContract, 'documentE', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('range clauses not groupable error: clauses are not groupable'); + } + }); + }); + + it('should return invalid result if startsWith operator used with startsWith operator', async () => { + const query = { where: [['a', 'startsWith', '1'], ['b', 'startsWith', 'a']] }; + + try { + await documentRepository.find(queryDataContract, 'documentE', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('multiple range clauses error: there can not be more than 1 non groupable range clause'); + } + }); + }); + + describe('conflicting operators', () => { + const conflictingOperators = [ + { + operators: ['>', '>'], + errorMessage: 'multiple range clauses error: there can only at most one range clause with a lower bound', + }, + { + operators: ['>', '=>'], + errorMessage: 'invalid where clause components error: second field of where component should be a known operator', + }, + { + operators: ['<', '<'], + errorMessage: 'range clauses not groupable error: lower and upper bounds must be passed if providing 2 ranges', + }, + { + operators: ['<', '<='], + errorMessage: 'range clauses not groupable error: lower and upper bounds must be passed if providing 2 ranges', + }, + ]; + + conflictingOperators.forEach(({ errorMessage, operators }) => { + it(`should return invalid result if ${operators[0]} operator used with ${operators[1]} operator`, async () => { + const query = { + where: [['a', operators[0], '1'], ['a', operators[1], 'a']], + orderBy: [['a', 'asc']], + }; + + try { + await documentRepository.find(queryDataContract, 'documentE', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(errorMessage); + } + }); + }); + }); + + it('should return invalid result if "in" operator is used before last two indexed conditions', async () => { + const query = { where: [['a', 'in', [1, 2]]] }; + + try { + await documentRepository.find(queryDataContract, 'documentF', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + // TODO is it correct ?????? + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + + ['>', '<', '>=', '<='].forEach((operator) => { + it(`should return invalid result if ${operator} operator is used before "=="`, async () => { + const query = { where: [['a', operator, 2], ['b', '==', 1]], orderBy: [['a', 'asc']] }; + + try { + await documentRepository.find(queryDataContract, 'documentF', query); + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + // TODO is it correct? + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + }); + + ['>', '<', '>=', '<='].forEach((operator) => { + it(`should return valid result if ${operator} operator is used before "in"`, async () => { + const query = { where: [['a', operator, 2], ['b', 'in', [1, 2]]], orderBy: [['a', 'asc'], ['b', 'asc']] }; + + const result = await documentRepository.find(queryDataContract, 'documentG', query); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + it('should return invalid result if "in" or range operators are not in orderBy', async () => { + const query = { + where: [ + ['a', '==', 1], + ['b', '>', 1], + ], + orderBy: [['b', 'asc']], + }; + + delete query.orderBy; + + try { + await documentRepository.find(queryDataContract, 'documentF', query); + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + }); + }); + + describe('timestamps', () => { + nonNumberNullAndUndefinedTestCases.forEach(({ type, value }) => { + it(`should return invalid result if $createdAt timestamp used with ${type} value`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentI', { where: [['$createdAt', '>', value]], orderBy: [['$createdAt', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e.message).to.startsWith( + 'value error: structure error: value is not an integer', + ); + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); + }); + + nonNumberNullAndUndefinedTestCases.forEach(({ type, value }) => { + it(`should return invalid result if $updatedAt timestamp used with ${type} value`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentH', { where: [['$updatedAt', '>', value]], orderBy: [['$updatedAt', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.startsWith( + 'value error: structure error: value is not an integer', + ); + } + }); + }); + + it('should return valid result if condition contains "$createdAt" field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentI', { where: [['$createdAt', '==', Date.now()]] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return valid result if condition contains "$updatedAt" field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentH', { where: [['$updatedAt', '==', Date.now()]] }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + describe('in', () => { + it('should return valid result if "in" operator used with an array value', async () => { + const query = { + where: [ + ['$id', 'in', [ + documents[0].getId(), + documents[1].getId(), + ]], + ], + orderBy: [['$id', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(2); + + const expectedDocuments = documents.slice(0, 2).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members( + expectedDocuments, + ); + }); + + notArrayTestCases.forEach(({ type, value }) => { + it(`should return invalid result if "in" operator used with not an array value, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', value]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid IN clause error: when using in operator you must provide an array of values'); + } + }); + }); + + it('should return invalid result if "in" operator used with an empty array value', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', []]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid IN clause error: in clause must at least 1 value'); + } + }); + + it('should return invalid result if "in" operator used with an array value which contains more than 100 elements', async () => { + const arr = []; + + for (let i = 0; i < 100; i++) { + arr.push(i); + } + + const result = await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', arr]], orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + + arr.push(101); + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', arr]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid IN clause error: in clause must at most 100 values'); + } + }); + + it('should return invalid result if "in" operator used with an array which contains not unique elements', async () => { + const arr = [1, 1]; + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', arr]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid IN clause error: there should be no duplicates values for In query'); + } + }); + + it('should return invalid results if "in" condition contains an array as an element', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where: [['a', 'in', [[]]]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: structure error: value is not a float'); + } + }); + }); + + describe('startsWith', () => { + it('should return valid result if "startsWith" operator used with a string value', async () => { + const query = { + where: [['lastName', 'startsWith', 'Swe']], + orderBy: [['lastName', 'asc']], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[2].toBuffer()); + }); + + it('should return invalid result if "startsWith" operator used with an empty string value', async () => { + try { + await documentRepository.find(queryDataContract, 'documentString', { where: [['a', 'startsWith', '']], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('starts with illegal string error: starts with can not start with an empty string'); + } + }); + + it('should return invalid result if "startsWith" operator used with a string value which is more than 255 bytes long', async () => { + const value = 'b'.repeat(256); + try { + await documentRepository.find(queryDataContract, 'documentString', { where: [['a', 'startsWith', value]], orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('field requirement unmet: value must be less than 256 bytes long'); + } + }); + + [ + typesTestCases.number, + typesTestCases.boolean, + typesTestCases.object, + typesTestCases.buffer, + ].forEach(({ type, value }) => { + it(`should return invalid result if "startWith" operator used with a not string value, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentString', { where: [['a', 'startsWith', value]], orderBy: [['a', 'asc']] }); + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value wrong type error: document field type doesn\'t match document value'); + } + }); + }); + + [ + typesTestCases.null, + typesTestCases.undefined, + ].forEach(({ type, value }) => { + it(`should return invalid result if "startWith" operator used with a not string value, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentString', { where: [['a', 'startsWith', value]], orderBy: [['a', 'asc']] }); + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid STARTSWITH clause error: starts with must have at least one character'); + } + }); + }); + }); + + describe.skip('elementMatch', () => { + it('should return valid result if "elementMatch" operator used with "where" conditions', async () => { + const query = { + where: [ + ['arrayWithObjects', 'elementMatch', [ + ['item', '==', 2], ['flag', '==', true], + ]], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[1].toBuffer()); + }); + + it('should return invalid result if "elementMatch" operator used with invalid "where" conditions', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['elem', 'startsWith', 1], ['elem', '<', 3]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if "elementMatch" operator used with less than 2 "where" conditions', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['elem', '>', 1]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if value contains conflicting conditions', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['elem', '>', 1], ['elem', '>', 1]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if $id field is specified', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['$id', '>', 1], ['$id', '<', 3]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if $ownerId field is specified', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['$ownerId', '>', 1], ['$ownerId', '<', 3]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if value contains nested "elementMatch" operator', async () => { + const query = { + where: [ + ['arr', 'elementMatch', + [['subArr', 'elementMatch', [ + ['subArrElem', '>', 1], ['subArrElem', '<', 3], + ]], ['subArr', '<', 3]], + ], + ], + }; + + try { + await documentRepository.find(queryDataContract, 'document', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + }); + + describe.skip('length', () => { + it('should return valid result if "length" operator used with a positive numeric value', async () => { + const query = { + where: [['arrayWithObjects', 'length', 2]], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[1].toBuffer()); + }); + + it('should return valid result if "length" operator used with zero', async () => { + const result = await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'length', 0], + ], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return invalid result if "length" operator used with a float numeric value', async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'length', 1.2], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if "length" operator used with a NaN', async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'length', NaN], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if "length" operator used with a numeric value which is less than 0', async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'length', -1], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + nonNumberTestCases.forEach(({ type, value }) => { + it(`should return invalid result if "length" operator used with a ${type} instead of numeric value`, async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'length', value], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + }); + }); + + describe.skip('contains', () => { + it('should find Documents using "contains" operator and array value', async () => { + const query = { + where: [ + ['arrayWithScalar', 'contains', [2, 3]], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(1); + + const [expectedDocument] = foundDocuments; + + expect(expectedDocument.toBuffer()).to.deep.equal(documents[2].toBuffer()); + }); + + it('should find Documents using "contains" operator and scalar value', async () => { + const query = { + where: [ + ['arrayWithScalar', 'contains', 2], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(2); + + const expectedDocuments = documents.slice(1, 3).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members( + expectedDocuments, + ); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if "contains" operator used with a scalar value ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', value], + ], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + scalarTestCases.forEach(({ type, value }) => { + it(`should return valid result if "contains" operator used with an array of scalar values ${type}`, async () => { + const result = await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', [value]], + ], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + it('should return invalid result if "contains" operator used with an array which has ' + + ' more than 100 elements', async () => { + const arr = []; + for (let i = 0; i < 100; i++) { + arr.push(i); + } + + const result = await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', arr], + ], + }); + + expect(result).to.be.instanceOf(StorageResult); + + arr.push(101); + + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', arr], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if "contains" operator used with an empty array', async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', []], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + it('should return invalid result if "contains" operator used with an array which contains not unique' + + ' elements', async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', [1, 1]], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + + nonScalarTestCases.forEach(({ type, value }) => { + it(`should return invalid result if used with non-scalar value ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', value], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + }); + + nonScalarTestCases.forEach(({ type, value }) => { + it(`should return invalid result if used with an array of non-scalar values ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'document', { + where: [ + ['arr', 'contains', [value]], + ], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(''); + } + }); + }); + }); + }); + }); + }); + + describe('limit', () => { + it('should limit return to 1 Document if limit is set', async () => { + const options = { + limit: 1, + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + options, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(1); + }); + + it('should limit result to 100 Documents if limit is not set', async () => { + // Store 101 document + for (let i = 0; i < 101; i++) { + const svDoc = document; + + svDoc.id = Identifier.from(Buffer.alloc(32, i + 1)); + await documentRepository.create(svDoc, blockInfo); + } + + const result = await documentRepository.find(dataContract, document.getType()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(100); + }); + + it('should return valid result if "limit" is a number', async () => { + const result = await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a', 'asc']], + limit: 1, + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return invalid result if "limit" is less than 0', async () => { + const where = [ + ['a', '>', 1], + ]; + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where, limit: -1, orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: integer out of bounds'); + } + }); + + it('should return invalid result if "limit" is 0', async () => { + const where = [ + ['a', '>', 1], + ]; + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where, limit: 0, orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('query invalid limit error: limit should be a integer from 1 to 100'); + } + }); + + it('should return invalid result if "limit" is bigger than 100', async () => { + const where = [ + ['a', '>', 1], + ]; + + const result = await documentRepository.find(queryDataContract, 'documentNumber', { where, limit: 100, orderBy: [['a', 'asc']] }); + + expect(result).to.be.instanceOf(StorageResult); + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where, limit: 101, orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('query invalid limit error: limit should be a integer from 1 to 100'); + } + }); + + it('should return invalid result if "limit" is a float number', async () => { + const where = [ + ['a', '>', 1], + ]; + + try { + await documentRepository.find(queryDataContract, 'documentNumber', { where, limit: 1.5, orderBy: [['a', 'asc']] }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: structure error: value is not an integer'); + } + }); + + nonNumberNullAndUndefinedTestCases.forEach(({ type, value }) => { + it(`should return invalid result if "limit" is not a number, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + limit: value, + orderBy: [['a', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: structure error: value is not an integer'); + } + }); + }); + }); + + describe('startAt', () => { + it('should return the second document using identifier', async () => { + const query = { + where: [ + ['order', '>=', 0], + ], + orderBy: [ + ['order', 'asc'], + ], + startAt: documents[1].getId(), + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + + const expectedDocuments = documents.splice(1).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members(expectedDocuments); + }); + + it('should return the second document using base58', async () => { + const query = { + where: [ + ['order', '>=', 0], + ], + orderBy: [ + ['order', 'asc'], + ], + startAt: documents[1].getId().toString(), + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + + const expectedDocuments = documents.splice(1).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members(expectedDocuments); + }); + + it('should throw InvalidQuery if document not found', async () => { + const options = { + startAt: Buffer.alloc(0), + }; + + try { + await documentRepository.find(dataContract, document.getType(), options); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidQueryError); + expect(e.message).to.equal('start document not found error: startAt document not found'); + } + }); + + [ + typesTestCases.boolean, + typesTestCases.null, + typesTestCases.object, + typesTestCases.number, + ].forEach(({ type, value }) => { + it(`should return invalid result if "startAt" is not a buffer, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + startAt: value, + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); + }); + }); + + describe('startAfter', () => { + it('should return Documents after 1 document', async () => { + const options = { + where: [ + ['order', '>=', 0], + ], + orderBy: [ + ['order', 'asc'], + ], + startAfter: documents[0].id, + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + options, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + + const expectedDocuments = documents.splice(1).map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.members(expectedDocuments); + }); + + it('should throw InvalidQuery if document not found', async () => { + const options = { + startAfter: Buffer.alloc(0), + }; + + try { + await documentRepository.find(dataContract, document.getType(), options); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidQueryError); + expect(e.message).to.equal('start document not found error: startAfter document not found'); + } + }); + + it('should return invalid result if both "startAt" and "startAfter" are present', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + startAfter: documents[1].getId(), + startAt: documents[1].getId(), + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('duplicate start conditions error: only one of startAt or startAfter should be provided'); + } + }); + + [ + typesTestCases.boolean, + typesTestCases.null, + typesTestCases.object, + typesTestCases.number, + ].forEach(({ type, value }) => { + it(`should return invalid result if "startAfter" is not a buffer, but ${type}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + startAfter: value, + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('value error: structure error: value are not bytes, a string, or an array of values representing bytes'); + } + }); + }); + }); + + describe('orderBy', () => { + it('should sort Documents in descending order', async () => { + const query = { + where: [ + ['order', '>=', 0], + ], + orderBy: [ + ['order', 'desc'], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + + const expectedDocuments = documents.reverse().map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.equal(expectedDocuments); + }); + + it('should sort Documents in ascending order', async () => { + const query = { + where: [ + ['order', '>=', 0], + ], + orderBy: [ + ['order', 'asc'], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + + const expectedDocuments = documents.map((doc) => doc.toBuffer()); + + expect(foundDocuments.map((doc) => doc.toBuffer())).to.deep.equal(expectedDocuments); + }); + + it('should sort Documents by $id', async () => { + await Promise.all( + documents.map((d) => documentRepository + .delete(dataContract, document.getType(), d.getId(), blockInfo)), + ); + + const createdIds = []; + let i = 0; + for (const svDoc of documents) { + svDoc.id = Identifier.from(Buffer.alloc(32, i + 1)); + await documentRepository.create(svDoc, blockInfo); + i++; + createdIds.push(svDoc.id); + } + + const query = { + where: [ + ['$id', 'in', createdIds], + ], + orderBy: [ + ['$id', 'desc'], + ], + }; + + const result = await documentRepository.find( + dataContract, + document.getType(), + query, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.be.lengthOf(documents.length); + + expect(foundDocuments[0].getId()).to.deep.equal(createdIds[4]); + expect(foundDocuments[1].getId()).to.deep.equal(createdIds[3]); + expect(foundDocuments[2].getId()).to.deep.equal(createdIds[2]); + expect(foundDocuments[3].getId()).to.deep.equal(createdIds[1]); + expect(foundDocuments[4].getId()).to.deep.equal(createdIds[0]); + }); + + it('should return valid result if "orderBy" contains 1 sorting field', async () => { + const result = await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a', 'asc']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return valid result if "orderBy" contains a second fields not used in where clause', async () => { + const result = await documentRepository.find(queryDataContract, 'documentC', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a', 'asc'], ['b', 'desc']], + }); + + expect(result).to.be.an.instanceOf(StorageResult); + }); + + it('should return invalid result if "orderBy" is an empty array', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + + it('should return invalid result if sorting applied to not range condition', async function it() { + this.skip('will be implemented later'); + + try { + await documentRepository.find(queryDataContract, 'documentString', { + where: [['a', '==', 'b']], + orderBy: [['a', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); + + it('should return valid result if there is no where conditions', async () => { + const result = await documentRepository.find(queryDataContract, 'documentNumber', { + orderBy: [['a', 'asc']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + + it('should return invalid result if the field inside an "orderBy" is an empty array', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [[]], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + + it('should return invalid result if order of three of two properties after indexed one is not preserved', async () => { + try { + await documentRepository.find(queryDataContract, 'documentL', { + where: [ + ['b', '>', 1], + ], + orderBy: [['b', 'desc'], ['e', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + + it('should return invalid result if order of properties does not match index', async () => { + try { + await documentRepository.find(queryDataContract, 'documentJ', { + where: [ + ['b', '>', 1], + ], + orderBy: [['b', 'desc'], ['d', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + + validFieldNameTestCases.forEach((fieldName) => { + it(`should return valid result if "orderBy" has valid field format, ${fieldName}`, async () => { + const result = await documentRepository.find(queryDataContract, `document${fieldName}`, { + where: [ + [fieldName, '>', fieldName.startsWith('$') && !fieldName.endsWith('At') ? generateRandomIdentifier() : 1], + ], + orderBy: [[fieldName, 'asc']], + }); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + invalidFieldNameTestCases.forEach((fieldName) => { + it(`should return invalid result if "orderBy" has invalid field format, ${fieldName}`, async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['$a', 'asc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + }); + + it('should return invalid result if "orderBy" has wrong direction', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a', 'a']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + + it('should return invalid result if "orderBy" field array has less than 2 elements (field, direction)', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + + it('should return invalid result if "orderBy" field array has more than 2 elements (field, direction)', async () => { + try { + await documentRepository.find(queryDataContract, 'documentNumber', { + where: [ + ['a', '>', 1], + ], + orderBy: [['a', 'asc', 'desc']], + }); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('missing order by for range error: query must have an orderBy field for each range element'); + } + }); + + validOrderByOperators.forEach(({ operator, value, documentType }) => { + it(`should return valid result if "orderBy" has valid field with valid operator (${operator}) and value (${value})" in "where" clause`, async () => { + const result = await documentRepository.find( + queryDataContract, + documentType, + { + where: [ + ['a', operator, value], + ], + orderBy: [['a', 'asc']], + }, + ); + + expect(result).to.be.instanceOf(StorageResult); + }); + }); + + it('should return invalid result if "orderBy" was not used with range operator', async () => { + const query = { + where: [['a', '==', 1]], + orderBy: [['b', 'asc']], + }; + + try { + await documentRepository.find(queryDataContract, 'documentK', query); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('where clause on non indexed property error: query must be for valid indexes'); + } + }); + }); + }); + }); + + describe('#delete', () => { + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + }); + + it('should delete Document', async () => { + let result = await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + result = await documentRepository.find(dataContract, document.getType(), { + where: [['$id', '==', document.getId()]], + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.have.lengthOf(0); + }); + + it('should delete Document in transaction', async () => { + await documentRepository + .storage + .startTransaction(); + + const result = await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + { + useTransaction: true, + }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const query = { + where: [['$id', '==', document.getId()]], + }; + + const removedDocumentResult = await documentRepository + .find( + dataContract, + document.getType(), + { + ...query, + useTransaction: true, + }, + ); + + const removedDocument = removedDocumentResult.getValue(); + + const notRemovedDocumentsResult = await documentRepository + .find(dataContract, document.getType(), query); + + const notRemovedDocuments = notRemovedDocumentsResult.getValue(); + + await documentRepository + .storage.commitTransaction(); + + const completelyRemovedDocumentResult = await documentRepository + .find(dataContract, document.getType(), query); + + const completelyRemovedDocument = completelyRemovedDocumentResult.getValue(); + + expect(removedDocument).to.have.lengthOf(0); + expect(notRemovedDocuments).to.be.not.null(); + expect(notRemovedDocuments[0].toBuffer()).to.deep.equal(document.toBuffer()); + expect(completelyRemovedDocument).to.have.lengthOf(0); + }); + + it('should restore document if transaction aborted', async () => { + await documentRepository + .storage + .startTransaction(); + + await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + { + useTransaction: true, + }, + ); + + const query = { + where: [['$id', '==', document.getId()]], + }; + + // Document should be removed in transaction + + const removedDocumentsResult = await documentRepository.find( + dataContract, + document.getType(), + { + ...query, + useTransaction: true, + }, + ); + + const removedDocuments = removedDocumentsResult.getValue(); + + expect(removedDocuments).to.have.lengthOf(0); + + // But still exists in main database + + const removedDocumentsWithoutTransactionResult = await documentRepository + .find(dataContract, document.getType(), query); + + const removedDocumentsWithoutTransaction = removedDocumentsWithoutTransactionResult + .getValue(); + + expect(removedDocumentsWithoutTransaction).to.not.have.lengthOf(0); + expect(removedDocumentsWithoutTransaction[0].toBuffer()).to.deep.equal(document.toBuffer()); + + await documentRepository + .storage + .abortTransaction(); + + const restoredDocumentsResult = await documentRepository + .find(dataContract, document.getType(), query); + + const restoredDocuments = restoredDocumentsResult.getValue(); + + expect(restoredDocuments).to.not.have.lengthOf(0); + expect(restoredDocuments[0].toBuffer()).to.deep.equal(document.toBuffer()); + }); + + it('should not delete Document on dry run', async () => { + const result = await documentRepository.delete( + dataContract, + document.getType(), + document.getId(), + blockInfo, + { + dryRun: true, + }, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const query = { + where: [['$id', '==', document.getId()]], + }; + + const removedDocumentsResult = await documentRepository + .find(dataContract, document.getType(), query); + + const removedDocuments = removedDocumentsResult + .getValue(); + + expect(removedDocuments).to.not.have.lengthOf(0); + expect(removedDocuments[0].toBuffer()).to.deep.equal(document.toBuffer()); + }); + }); + + describe('#prove', () => { + // TODO do we need to check prove result with every single find test request? + + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + }); + + it('should return proof for all existing documents', async () => { + const result = await documentRepository.prove(dataContract, document.getType()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + // TODO enable this test when we support transactions for proofs + it.skip('should return proof for all existing documents in transaction', async () => { + await documentRepository + .storage + .startTransaction(); + + const result = await documentRepository + .prove(dataContract, document.getType(), { + useTransaction: true, + }); + + await documentRepository + .storage + .abortTransaction(); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + }); + + describe('#proveManyDocumentsFromDifferentContracts', () => { + beforeEach(async () => { + await createDocuments(documentRepository, documents, blockInfo); + }); + + it('should return proof for all existing documents', async () => { + const documentsToProve = documents.map((doc) => ({ + dataContractId: doc.getDataContractId().toBuffer(), + documentId: doc.getId().toBuffer(), + type: doc.getType(), + })); + + const result = await documentRepository.proveManyDocumentsFromDifferentContracts( + documentsToProve, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof non existing documents', async () => { + const documentsToProve = [{ + dataContractId: generateRandomIdentifier().toBuffer(), + documentId: generateRandomIdentifier().toBuffer(), + type: 'unknownType', + }]; + + const result = await documentRepository.proveManyDocumentsFromDifferentContracts( + documentsToProve, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof for existing and non existing documents', async () => { + const documentsToProve = documents.map((doc) => ({ + dataContractId: doc.getDataContractId().toBuffer(), + documentId: doc.getId().toBuffer(), + type: doc.getType(), + })); + + documentsToProve.push({ + dataContractId: generateRandomIdentifier().toBuffer(), + documentId: generateRandomIdentifier().toBuffer(), + type: 'unknownType', + }); + + const result = await documentRepository.proveManyDocumentsFromDifferentContracts( + documentsToProve, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + }); +}); diff --git a/packages/js-drive/test/integration/document/fetchDataContractFactory.spec.js b/packages/js-drive/test/integration/document/fetchDataContractFactory.spec.js new file mode 100644 index 00000000000..ad2f04c9eb4 --- /dev/null +++ b/packages/js-drive/test/integration/document/fetchDataContractFactory.spec.js @@ -0,0 +1,83 @@ +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const InvalidQueryError = require('../../../lib/document/errors/InvalidQueryError'); + +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('fetchDataContractFactory', () => { + let fetchDataContract; + let contractId; + let dataContractRepository; + let dataContract; + let container; + let blockInfo; + + beforeEach(async () => { + container = await createTestDIContainer(); + + dataContractRepository = container.resolve('dataContractRepository'); + + dataContract = getDataContractFixture(); + + contractId = dataContract.getId(); + + blockInfo = new BlockInfo(1, 1, Date.now()); + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(); + + await dataContractRepository.create(dataContract, blockInfo); + + fetchDataContract = container.resolve('fetchDataContract'); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should fetch DataContract for specified contract ID and document type', async () => { + const result = await fetchDataContract(contractId); + + expect(result).to.be.instanceOf(StorageResult); + // TODO: Processing fees are ignored for v0.23 + expect(result.getOperations().length).to.equals(0); + + const foundDataContract = result.getValue(); + + expect(foundDataContract.toObject()).to.deep.equal(dataContract.toObject()); + }); + + it('should throw InvalidQueryError if contract ID is not valid', async () => { + contractId = 'something'; + + try { + await fetchDataContract(contractId); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('invalid data contract ID: Identifier expects Buffer'); + } + }); + + it('should throw InvalidQueryError if contract ID does not exist', async () => { + contractId = generateRandomIdentifier(); + + try { + await fetchDataContract(contractId); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal(`data contract ${contractId} not found`); + } + }); +}); diff --git a/packages/js-drive/test/integration/document/fetchDocumentsFactory.spec.js b/packages/js-drive/test/integration/document/fetchDocumentsFactory.spec.js new file mode 100644 index 00000000000..f10ec5f54dc --- /dev/null +++ b/packages/js-drive/test/integration/document/fetchDocumentsFactory.spec.js @@ -0,0 +1,221 @@ +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); + +const InvalidQueryError = require('../../../lib/document/errors/InvalidQueryError'); + +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('fetchDocumentsFactory', () => { + let fetchDocuments; + let documentType; + let contractId; + let document; + let dataContractRepository; + let documentRepository; + let dataContract; + let container; + let blockInfo; + + beforeEach(async () => { + container = await createTestDIContainer(); + + dataContractRepository = container.resolve('dataContractRepository'); + documentRepository = container.resolve('documentRepository'); + + dataContract = getDataContractFixture(); + + contractId = dataContract.getId(); + + [document] = getDocumentsFixture(dataContract); + + documentType = document.getType(); + + dataContract.documents[documentType].indices = [ + { + properties: [ + { name: 'asc' }, + ], + }, + ]; + + blockInfo = new BlockInfo(1, 0, Date.now()); + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(); + + await dataContractRepository.create(dataContract, blockInfo); + + fetchDocuments = container.resolve('fetchDocuments'); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should fetch Documents for specified contract ID and document type', async () => { + await documentRepository.create(document, blockInfo); + + const result = await fetchDocuments(contractId, documentType); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(1); + + const [actualDocument] = foundDocuments; + + expect(actualDocument.toObject()).to.deep.equal(document.toObject()); + }); + + it('should fetch Documents for specified contract id, document type and name', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['name', '==', document.get('name')]] }; + + const result = await fetchDocuments(contractId, documentType, query); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.be.an('array'); + expect(foundDocuments).to.have.lengthOf(1); + + const [actualDocument] = foundDocuments; + + expect(actualDocument.toObject()).to.deep.equal(document.toObject()); + }); + + it('should return empty array for specified contract ID, document type and name not exist', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['name', '==', 'unknown']] }; + + const result = await fetchDocuments(contractId, documentType, query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.deep.equal([]); + }); + + it('should fetch documents by an equal date', async () => { + const indexedDocument = getDocumentsFixture(dataContract)[3]; + + await documentRepository.create(indexedDocument, blockInfo); + + const query = { + where: [ + ['$createdAt', '==', indexedDocument.getCreatedAt().getTime()], + ], + }; + + const result = await fetchDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments[0].toObject()).to.deep.equal( + indexedDocument.toObject(), + ); + }); + + it('should fetch documents by a date range', async () => { + const [, , , indexedDocument] = getDocumentsFixture(dataContract); + + await documentRepository.create(indexedDocument, blockInfo); + + const startDate = new Date(); + startDate.setSeconds(startDate.getSeconds() - 10); + + const endDate = new Date(); + endDate.setSeconds(endDate.getSeconds() + 10); + + const query = { + where: [ + ['$createdAt', '>', startDate.getTime()], + ['$createdAt', '<=', endDate.getTime()], + ], + orderBy: [['$createdAt', 'asc']], + }; + + const result = await fetchDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments[0].toObject()).to.deep.equal( + indexedDocument.toObject(), + ); + }); + + it('should fetch empty array in case date is out of range', async () => { + const [, , , indexedDocument] = getDocumentsFixture(dataContract); + + await documentRepository.create(indexedDocument, blockInfo); + + const startDate = new Date(); + startDate.setSeconds(startDate.getSeconds() + 10); + + const endDate = new Date(); + endDate.setSeconds(endDate.getSeconds() + 20); + + const query = { + where: [ + ['$createdAt', '>', startDate.getTime()], + ['$createdAt', '<=', endDate.getTime()], + ], + orderBy: [['$createdAt', 'asc']], + }; + + const result = await fetchDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const foundDocuments = result.getValue(); + + expect(foundDocuments).to.have.length(0); + }); + + it('should throw InvalidQueryError if searching by non indexed fields', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['lastName', '==', 'unknown']] }; + + try { + await fetchDocuments(contractId, documentType, blockInfo, query); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); + + it('should throw InvalidQueryError if type does not exist', async () => { + documentType = 'Unknown'; + + try { + await fetchDocuments(contractId, documentType); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + expect(e.message).to.equal('document type Unknown is not defined in the data contract'); + } + }); +}); diff --git a/packages/js-drive/test/integration/document/proveDocumentsFactory.spec.js b/packages/js-drive/test/integration/document/proveDocumentsFactory.spec.js new file mode 100644 index 00000000000..cf4bea8abc3 --- /dev/null +++ b/packages/js-drive/test/integration/document/proveDocumentsFactory.spec.js @@ -0,0 +1,198 @@ +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const InvalidQueryError = require('../../../lib/document/errors/InvalidQueryError'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('proveDocumentsFactory', () => { + let proveDocuments; + let documentType; + let contractId; + let document; + let dataContractRepository; + let documentRepository; + let dataContract; + let container; + let blockInfo; + + beforeEach(async () => { + container = await createTestDIContainer(); + + dataContractRepository = container.resolve('dataContractRepository'); + documentRepository = container.resolve('documentRepository'); + + dataContract = getDataContractFixture(); + + contractId = dataContract.getId(); + + [document] = getDocumentsFixture(dataContract); + + documentType = document.getType(); + + dataContract.documents[documentType].indices = [ + { + properties: [ + { name: 'asc' }, + ], + }, + ]; + + blockInfo = new BlockInfo(1, 0, Date.now()); + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(); + + await dataContractRepository.create(dataContract, blockInfo); + + proveDocuments = container.resolve('proveDocuments'); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should return proof for specified contract ID and document type', async () => { + await documentRepository.create(document, blockInfo); + + const result = await proveDocuments(contractId, documentType); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof for specified contract id, document type and name', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['name', '==', document.get('name')]] }; + + const result = await proveDocuments(contractId, documentType, query); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof for specified contract ID, document type and name not exist', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['name', '==', 'unknown']] }; + + const result = await proveDocuments(contractId, documentType, query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof by an equal date', async () => { + const indexedDocument = getDocumentsFixture(dataContract)[3]; + + await documentRepository.create(indexedDocument, blockInfo); + + const query = { + where: [ + ['$createdAt', '==', indexedDocument.getCreatedAt().getTime()], + ], + }; + + const result = await proveDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof by a date range', async () => { + const [, , , indexedDocument] = getDocumentsFixture(dataContract); + + await documentRepository.create(indexedDocument, blockInfo); + + const startDate = new Date(); + startDate.setSeconds(startDate.getSeconds() - 10); + + const endDate = new Date(); + endDate.setSeconds(endDate.getSeconds() + 10); + + const query = { + where: [ + ['$createdAt', '>', startDate.getTime()], + ['$createdAt', '<=', endDate.getTime()], + ], + orderBy: [['$createdAt', 'asc']], + }; + + const result = await proveDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should fetch empty array in case date is out of range', async () => { + const [, , , indexedDocument] = getDocumentsFixture(dataContract); + + await documentRepository.create(indexedDocument, blockInfo); + + const startDate = new Date(); + startDate.setSeconds(startDate.getSeconds() + 10); + + const endDate = new Date(); + endDate.setSeconds(endDate.getSeconds() + 20); + + const query = { + where: [ + ['$createdAt', '>', startDate.getTime()], + ['$createdAt', '<=', endDate.getTime()], + ], + orderBy: [['$createdAt', 'asc']], + }; + + const result = await proveDocuments(contractId, 'indexedDocument', query); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.be.greaterThan(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceOf(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should throw InvalidQueryError if searching by non indexed fields', async () => { + await documentRepository.create(document, blockInfo); + + const query = { where: [['lastName', '==', 'unknown']] }; + + try { + await proveDocuments(contractId, documentType, query); + + expect.fail('should throw InvalidQueryError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidQueryError); + } + }); +}); diff --git a/packages/js-drive/test/integration/fee/feesPrediction.spec.js b/packages/js-drive/test/integration/fee/feesPrediction.spec.js new file mode 100644 index 00000000000..b50b8432b03 --- /dev/null +++ b/packages/js-drive/test/integration/fee/feesPrediction.spec.js @@ -0,0 +1,549 @@ +const crypto = require('crypto'); + +const Long = require('long'); + +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const getInstantAssetLockProofFixture = require('@dashevo/dpp/lib/test/fixtures/getInstantAssetLockProofFixture'); +const identityUpdateTransitionSchema = require('@dashevo/dpp/schema/identity/stateTransition/identityUpdate.json'); +const StateTransitionExecutionContext = require('@dashevo/dpp/lib/stateTransition/StateTransitionExecutionContext'); +const PrivateKey = require('@dashevo/dashcore-lib/lib/privatekey'); + +const BlsSignatures = require('@dashevo/dpp/lib/bls/bls'); + +const getBiggestPossibleIdentity = require('../../../lib/test/mock/getBiggestPossibleIdentity'); + +const createTestDIContainer = require('../../../lib/test/createTestDIContainer'); +const createDataContractDocuments = require('../../../lib/test/fixtures/createDataContractDocuments'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +/** + * @param {DashPlatformProtocol} dpp + * @param {AbstractStateTransition} stateTransition + * @return {Promise} + */ +async function validateStateTransition(dpp, stateTransition) { + const validateBasicResult = await dpp.stateTransition.validateBasic(stateTransition); + expect(validateBasicResult.isValid()).to.be.true(); + + const validateSignatureResult = await dpp.stateTransition.validateSignature(stateTransition); + expect(validateSignatureResult.isValid()).to.be.true(); + + const validateFeeResult = await dpp.stateTransition.validateFee(stateTransition); + expect(validateFeeResult.isValid()).to.be.true(); + + const validateStateResult = await dpp.stateTransition.validateState(stateTransition); + expect(validateStateResult.isValid()).to.be.true(); + + const applyResult = await dpp.stateTransition.validateState(stateTransition); + expect(applyResult.isValid()).to.be.true(); +} + +/** + * @param {DashPlatformProtocol} dpp + * @param {GroveDBStore} groveDBStore + * @param {AbstractStateTransition} stateTransition + * @return {Promise} + */ +async function expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition) { + // Execute state transition without dry run + + const actualExecutionContext = new StateTransitionExecutionContext(); + + stateTransition.setExecutionContext(actualExecutionContext); + + await validateStateTransition(dpp, stateTransition); + + // Execute state transition with dry run enabled + + const predictedExecutionContext = new StateTransitionExecutionContext(); + + predictedExecutionContext.enableDryRun(); + + stateTransition.setExecutionContext(predictedExecutionContext); + + const initialAppHash = await groveDBStore.getRootHash(); + + await validateStateTransition(dpp, stateTransition); + + // AppHash shouldn't be changed after dry run + const appHashAfterDryRun = await groveDBStore.getRootHash(); + + expect(appHashAfterDryRun).to.deep.equal(initialAppHash); + + // Compare operations + + // TODO: Processing fees are disabled for v0.23 + // const actualOperations = actualExecutionContext.getOperations(); + // const predictedOperations = predictedExecutionContext.getOperations(); + + // expect(predictedOperations).to.have.lengthOf(actualOperations.length); + + // Compare fees + + // stateTransition.setExecutionContext(actualExecutionContext); + // const actualFees = calculateStateTransitionFee(stateTransition); + // + // stateTransition.setExecutionContext(predictedExecutionContext); + // const predictedFees = calculateStateTransitionFee(stateTransition); + // + // expect(predictedFees).to.be.greaterThanOrEqual(actualFees); + // + // predictedOperations.forEach((predictedOperation, i) => { + // expect(predictedOperation.getStorageCost()).to.be.greaterThanOrEqual( + // actualOperations[i].getStorageCost(), + // ); + // + // expect(predictedOperation.getProcessingCost()).to.be.greaterThanOrEqual( + // actualOperations[i].getProcessingCost(), + // ); + // }); +} + +describe('feesPrediction', () => { + let dpp; + let container; + let stateRepository; + let identity; + let groveDBStore; + let blockInfo; + + beforeEach(async function beforeEach() { + container = await createTestDIContainer(); + + const latestBlockExecutionContext = container.resolve('latestBlockExecutionContext'); + + blockInfo = new BlockInfo(1, 0, Date.now()); + + latestBlockExecutionContext.getEpochInfo = this.sinon.stub().returns({ + currentEpochIndex: blockInfo.epoch, + }); + + latestBlockExecutionContext.getTimeMs = this.sinon.stub().returns(blockInfo.timeMs); + latestBlockExecutionContext.getHeight = this.sinon.stub().returns(Long.fromNumber(0)); + + dpp = container.resolve('dpp'); + + stateRepository = container.resolve('stateRepository'); + groveDBStore = container.resolve('groveDBStore'); + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.createInitialStateStructure(); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + describe('Identity', () => { + let assetLockPrivateKey; + let instantAssetLockProof; + let privateKeys; + + beforeEach(async function beforeEachFunction() { + assetLockPrivateKey = new PrivateKey(); + + instantAssetLockProof = getInstantAssetLockProofFixture(assetLockPrivateKey); + + identity = getBiggestPossibleIdentity(); + identity.id = instantAssetLockProof.createIdentifier(); + identity.setAssetLockProof(instantAssetLockProof); + + // Generate real keys + const { BasicSchemeMPL } = await BlsSignatures.getInstance(); + + privateKeys = identity.getPublicKeys().map((identityPublicKey) => { + const randomBytes = new Uint8Array(crypto.randomBytes(256)); + + const privateKey = BasicSchemeMPL.keyGen(randomBytes); + const publicKey = privateKey.getG1(); + const publicKeyBuffer = Buffer.from(publicKey.serialize()); + + identityPublicKey.setData(publicKeyBuffer); + + const result = Buffer.from(privateKey.serialize()); + + privateKey.delete(); + publicKey.delete(); + + return result; + }); + + stateRepository.verifyInstantLock = this.sinon.stub().resolves(true); + }); + + describe('IdentityCreateTransition', () => { + it('should have predicted fee more than actual fee', async () => { + const stateTransition = dpp.identity.createIdentityCreateTransition(identity); + + // Sign public keys + const publicKeys = stateTransition.getPublicKeys(); + + for (let i = 0; i < publicKeys.length; i++) { + await stateTransition.signByPrivateKey( + privateKeys[i], + IdentityPublicKey.TYPES.BLS12_381, + ); + + publicKeys[i].setSignature(stateTransition.getSignature()); + + stateTransition.setSignature(undefined); + } + + // Sign state transition + await stateTransition.signByPrivateKey( + assetLockPrivateKey, + IdentityPublicKey.TYPES.ECDSA_SECP256K1, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + + describe('IdentityTopUpTransition', () => { + it('should have predicted fee more than actual fee', async () => { + await stateRepository.createIdentity(identity); + + const stateTransition = dpp.identity.createIdentityTopUpTransition( + identity.getId(), + instantAssetLockProof, + ); + + await stateTransition.signByPrivateKey( + assetLockPrivateKey, + IdentityPublicKey.TYPES.ECDSA_SECP256K1, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + + describe('IdentityUpdateTransition', () => { + it('should have predicted fee more than actual fee', async () => { + await stateRepository.createIdentity(identity); + + const newIdentityPublicKeys = []; + const disableIdentityPublicKeys = []; + + const { BasicSchemeMPL } = await BlsSignatures.getInstance(); + + const newPrivateKeys = []; + for (let i = 0; i < identityUpdateTransitionSchema.properties.addPublicKeys.maxItems; i++) { + const randomBytes = new Uint8Array(crypto.randomBytes(256)); + const privateKey = BasicSchemeMPL.keyGen(randomBytes); + const publicKey = privateKey.getG1(); + const publicKeyBuffer = Buffer.from(publicKey.serialize()); + + newPrivateKeys.push(privateKey); + + newIdentityPublicKeys.push( + new IdentityPublicKey({ + id: i + identity.getPublicKeys().length, + type: IdentityPublicKey.TYPES.BLS12_381, + data: publicKeyBuffer, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: i === 0 + ? IdentityPublicKey.SECURITY_LEVELS.MASTER : IdentityPublicKey.SECURITY_LEVELS.HIGH, + readOnly: false, + }), + ); + + disableIdentityPublicKeys.push(identity.getPublicKeyById(i)); + + publicKey.delete(); + } + + const stateTransition = dpp.identity.createIdentityUpdateTransition( + identity, + { + add: newIdentityPublicKeys, + disable: disableIdentityPublicKeys, + }, + ); + + const [signerKey] = identity.getPublicKeys(); + + const starterPromise = Promise.resolve(null); + + await stateTransition.getPublicKeysToAdd().reduce( + (previousPromise, publicKey) => previousPromise.then(async () => { + const privateKey = newPrivateKeys[publicKey.getId() - identity.getPublicKeys().length]; + + if (!privateKey) { + throw new Error(`Private key for key ${publicKey.getId()} not found`); + } + + stateTransition.setSignaturePublicKeyId(signerKey.getId()); + + await stateTransition.signByPrivateKey(privateKey, publicKey.getType()); + + publicKey.setSignature(stateTransition.getSignature()); + + stateTransition.setSignature(undefined); + stateTransition.setSignaturePublicKeyId(undefined); + }), + starterPromise, + ); + + await stateTransition.sign( + identity.getPublicKeyById(0), + privateKeys[0], + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + }); + + describe('DataContract', () => { + let dataContract; + let privateKey; + + beforeEach(async () => { + // Create identity + + privateKey = new PrivateKey(); + + identity = new Identity({ + protocolVersion: 1, + id: generateRandomIdentifier().toBuffer(), + publicKeys: [ + { + id: 0, + type: IdentityPublicKey.TYPES.BLS12_381, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: false, + data: Buffer.alloc(48).fill(255), + }, + { + id: 1, + type: IdentityPublicKey.TYPES.ECDSA_SECP256K1, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.HIGH, + readOnly: false, + data: privateKey.toPublicKey().toBuffer(), + }, + ], + balance: Math.floor(9223372036854775807 / 10000), + revision: 0, + }); + + await stateRepository.createIdentity(identity); + + // Generate Data Contract + + const documents = createDataContractDocuments(); + + dataContract = dpp.dataContract.create(identity.getId(), documents); + }); + + describe('DataContractCreate', () => { + it('should have predicted fee more than actual fee', async () => { + const stateTransition = dpp.dataContract.createDataContractCreateTransition(dataContract); + + await stateTransition.sign( + identity.getPublicKeyById(1), + privateKey, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + + describe('DataContractUpdate', () => { + it('should have predicted fee more than actual fee', async () => { + await stateRepository.createDataContract(dataContract); + + dataContract.incrementVersion(); + + const documents = dataContract.getDocuments(); + + documents.newDoc = { + type: 'object', + indices: [ + { + name: 'onwerIdToUser', + properties: [ + { $ownerId: 'asc' }, + { user: 'asc' }, + ], + unique: true, + }, + ], + properties: { + user: { + type: 'string', + maxLength: 63, + }, + publicKey: { + type: 'array', + byteArray: true, + maxItems: 33, + }, + }, + required: ['user', 'publicKey'], + additionalProperties: false, + }; + + dataContract.setDocuments(documents); + + const stateTransition = dpp.dataContract.createDataContractUpdateTransition(dataContract); + + await stateTransition.sign( + identity.getPublicKeyById(1), + privateKey, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + }); + + describe('Document', () => { + let documents; + let dataContract; + let privateKey; + + beforeEach(async () => { + // Create Identity + + privateKey = new PrivateKey(); + + identity = new Identity({ + protocolVersion: 1, + id: generateRandomIdentifier().toBuffer(), + publicKeys: [ + { + id: 0, + type: IdentityPublicKey.TYPES.BLS12_381, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: false, + data: Buffer.alloc(48).fill(255), + }, + { + id: 1, + type: IdentityPublicKey.TYPES.ECDSA_SECP256K1, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.HIGH, + readOnly: false, + data: privateKey.toPublicKey().toBuffer(), + }, + ], + balance: Math.floor(9223372036854775807 / 10000), + revision: 0, + }); + + await stateRepository.createIdentity(identity); + + // Create Data Contract + + const documentTypes = createDataContractDocuments(); + + dataContract = dpp.dataContract.create(identity.getId(), documentTypes); + + await stateRepository.createDataContract(dataContract); + + // Create documents + + documents = []; + + let i = 0; + for (const documentType of Object.keys(documentTypes)) { + const data = {}; + + for (const propertyName of Object.keys(documentTypes[documentType].properties)) { + data[propertyName] = `${crypto.randomBytes(31).toString('hex')}a`; + } + + const document = dpp.document.create( + dataContract, + identity.getId(), + documentType, + data, + ); + + documents.push(document); + + i += 1; + + if (i === 10) { + break; + } + } + }); + + describe('DocumentsBatchTransition', () => { + context('create', () => { + it('should have predicted fee more than actual fee', async () => { + const stateTransition = dpp.document.createStateTransition({ + create: documents, + }); + + await stateTransition.sign( + identity.getPublicKeyById(1), + privateKey, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + + context('replace', () => { + it('should have predicted fee more than actual fee', async () => { + for (const document of documents) { + await stateRepository.createDocument(document); + } + + for (const document of documents) { + const data = document.getData(); + + for (const propertyName of Object.keys(data)) { + data[propertyName] = `${crypto.randomBytes(31).toString('hex')}b`; + } + + document.setData(data); + } + + const stateTransition = dpp.document.createStateTransition({ + replace: documents, + }); + + await stateTransition.sign( + identity.getPublicKeyById(1), + privateKey, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + + context('delete', () => { + it('should have predicted fee more than actual fee', async () => { + for (const document of documents) { + await stateRepository.createDocument(document); + } + + const stateTransition = dpp.document.createStateTransition({ + delete: documents, + }); + + await stateTransition.sign( + identity.getPublicKeyById(1), + privateKey, + ); + + await expectPredictedFeeHigherOrEqualThanActual(dpp, groveDBStore, stateTransition); + }); + }); + }); + }); +}); diff --git a/packages/js-drive/test/integration/groveDB/GroveDBStore.spec.js b/packages/js-drive/test/integration/groveDB/GroveDBStore.spec.js new file mode 100644 index 00000000000..162c155e7fd --- /dev/null +++ b/packages/js-drive/test/integration/groveDB/GroveDBStore.spec.js @@ -0,0 +1,423 @@ +const rimraf = require('rimraf'); + +const Drive = require('@dashevo/rs-drive'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const logger = require('../../../lib/util/noopLogger'); +const StorageResult = require('../../../lib/storage/StorageResult'); + +describe('GroveDBStore', () => { + let rsDrive; + let store; + let key; + let value; + let testTreePath; + let otherTreePath; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, logger); + + testTreePath = [Buffer.from('testTree')]; + otherTreePath = [Buffer.from('otherTree')]; + + await store.createTree([], testTreePath[0]); + await store.createTree([], otherTreePath[0]); + + key = Buffer.alloc(32).fill(1); + value = Buffer.alloc(32).fill(2); + }); + + afterEach(async () => { + await rsDrive.close(); + rimraf.sync('./db/grovedb_test'); + }); + + describe('#put', () => { + it('should store value', async () => { + const result = await store.put(testTreePath, key, value); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const actualValue = await rsDrive.getGroveDB().get(testTreePath, key); + + expect(actualValue).to.be.deep.equal({ + type: 'item', + value, + }); + }); + + it('should store value in transaction', async () => { + await store.startTransaction(); + + // store data in transaction + await store.put(testTreePath, key, value, { + useTransaction: true, + }); + + // check we don't have data in db before commit + try { + await rsDrive.getGroveDB().get(testTreePath, key); + + expect.fail('Should fail with NotFoundError error'); + } catch (e) { + expect(e.message.startsWith('grovedb: path key not found: key not found in Merk')).to.be.true(); + } + + // check we can't fetch data without transaction + const notFoundValueResult = await store.get(testTreePath, key); + + expect(notFoundValueResult.getValue()).to.be.null(); + + // check we can fetch data inside transaction + const valueFromTransactionResult = await store.get(testTreePath, key, { + useTransaction: true, + }); + + expect(valueFromTransactionResult).to.be.instanceOf(StorageResult); + expect(valueFromTransactionResult.getOperations().length).to.equal(0); + + expect(valueFromTransactionResult.getValue()).to.deep.equal(value); + + await store.commitTransaction(); + + // check we have data in db after commit + const storedValue = await rsDrive.getGroveDB().get(testTreePath, key); + + expect(storedValue).to.deep.equal({ + type: 'item', + value, + }); + }); + }); + + describe('#get', () => { + it('should return null if key was not found', async () => { + const result = await store.get(testTreePath, key); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should return stored value', async () => { + await rsDrive.getGroveDB().insert( + testTreePath, + key, + { type: 'item', value }, + false, + ); + + const result = await store.get(testTreePath, key); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.deep.equal(value); + }); + + it('should return stored value with transaction', async () => { + await store.put(testTreePath, key, value); + + await store.startTransaction(); + + const result = await store.get(testTreePath, key, { + useTransaction: true, + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.deep.equal(value); + }); + }); + + describe('#putReference', () => { + it('should put an item by reference', async () => { + await store.put(otherTreePath, key, value); + + const result = await store.putReference(testTreePath, key, [otherTreePath[0], key]); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const getResult = await store.get(testTreePath, key); + + expect(getResult.getValue()).to.deep.equal(value); + }); + + it('should put an item by reference in transaction', async () => { + await store.put(otherTreePath, key, value); + + await store.startTransaction(); + + const result = await store.putReference(testTreePath, key, [otherTreePath[0], key], { + useTransaction: true, + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const nonTxResult = await store.get(testTreePath, key); + + expect(nonTxResult.getValue()).to.be.null(); + + const txResult = await store.get(testTreePath, key, { + useTransaction: true, + }); + + expect(txResult.getValue()).to.deep.equal(value); + }); + }); + + describe('#query', () => { + it('should return results', async () => { + await store.put(testTreePath, key, value); + + const result = await store.query({ + path: testTreePath, + query: { + query: { + items: [ + { + type: 'rangeFull', + }, + ], + }, + }, + }); + + expect(result).to.have.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.have.lengthOf(1); + + const [item] = result.getValue(); + + expect(item).to.deep.equal(value); + }); + }); + + describe('#proveQuery', () => { + it('should return proof', async () => { + await store.put(testTreePath, key, value); + + const result = await store.proveQuery({ + path: testTreePath, + query: { + query: { + items: [ + { + type: 'rangeFull', + }, + ], + }, + }, + }); + + expect(result).to.have.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + }); + }); + + describe('#delete', () => { + it('should delete value', async () => { + await store.put(testTreePath, key, value); + + const result = await store.delete(testTreePath, key); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + try { + await rsDrive.getGroveDB().get(testTreePath, key); + + expect.fail('should throw no value found for key error'); + } catch (e) { + expect(e.message.startsWith('grovedb: path key not found: key not found in Merk')).to.be.true(); + } + }); + + it('should delete value in transaction', async () => { + await store.put(testTreePath, key, value); + + await store.startTransaction(); + + // Delete a value from transaction + const result = await store.delete(testTreePath, key, { + useTransaction: true, + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + // Now it should be absent there + const valueFromTransactionResult = await store.get(testTreePath, key, { + useTransaction: true, + }); + + expect(valueFromTransactionResult.getValue()).to.be.null(); + + // But should be still present in store + const valueFromStoreResult = await store.get(testTreePath, key); + expect(valueFromStoreResult.getValue()).to.deep.equal(value); + + await store.commitTransaction(); + + // When we commit transaction this key should disappear from store too + const valueFromStoreAfterCommitResult = await store.get(testTreePath, key); + expect(valueFromStoreAfterCommitResult.getValue()).to.be.null(); + }); + }); + + describe('#getAux', () => { + it('should get an auxiliary data from db', async () => { + await rsDrive.getGroveDB().putAux(key, value); + + const result = await store.getAux(key); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.deep.equal(value); + }); + + it('should get an auxiliary data from db with transaction', async () => { + await rsDrive.getGroveDB().putAux(key, value); + + await store.startTransaction(); + + const result = await store.getAux(key, { + useTransaction: true, + }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.deep.equal(value); + }); + }); + + describe('#putAux', () => { + it('should put an auxiliary data', async () => { + await store.putAux(key, value); + + const result = await rsDrive.getGroveDB().getAux(key); + + expect(result).to.deep.equal(value); + }); + + it('should put an auxiliary data using transaction', async () => { + await store.startTransaction(); + + await store.putAux(key, value, { + useTransaction: true, + }); + + const nonTxResult = await rsDrive.getGroveDB().getAux(key); + + expect(nonTxResult).to.be.null(); + + const txResult = await rsDrive.getGroveDB().getAux(key, true); + + expect(txResult).to.deep.equal(value); + }); + }); + + describe('#deleteAux', () => { + it('should delete an auxiliary data', async () => { + await store.putAux(key, value); + + const getResult = await store.getAux(key); + + expect(getResult.getValue()).to.deep.equal(value); + + const deleteResult = await store.deleteAux(key); + + expect(deleteResult).to.be.instanceOf(StorageResult); + expect(deleteResult.getOperations().length).to.equal(0); + + const deletedValue = await rsDrive.getGroveDB().getAux(key); + + expect(deletedValue).to.be.null(); + }); + + it('should delete an auxiliary data within transaction', async () => { + await store.putAux(key, value); + + await store.startTransaction(); + + const deleteResult = await store.deleteAux(key, { + useTransaction: true, + }); + + expect(deleteResult).to.be.instanceOf(StorageResult); + expect(deleteResult.getOperations().length).to.equal(0); + + const nonTxResult = await store.getAux(key); + + expect(nonTxResult.getValue()).to.deep.equal(value); + + const txResult = await store.getAux(key, { + useTransaction: true, + }); + + expect(txResult.getValue()).to.be.null(); + }); + }); + + describe('#getRootHash', () => { + it('should return a null hash for empty store', async () => { + await rsDrive.close(); + + rimraf.sync('./db/grovedb_test'); + + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, logger); + + const result = await store.getRootHash(); + + expect(result).to.deep.equal(Buffer.alloc(32).fill(0)); + }); + + it('should return a root hash for store with value', async () => { + await store.put(testTreePath, key, value); + + const valueHash = Buffer.from('9522321fe08ddbbd5a37cf875cdd7f7a104ac9b9e9246f1454b7360341b29124', 'hex'); + + const result = await store.getRootHash(); + + expect(result).to.deep.equal(valueHash); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/IdentityBalanceStoreRepository.spec.js b/packages/js-drive/test/integration/identity/IdentityBalanceStoreRepository.spec.js new file mode 100644 index 00000000000..86e79eca563 --- /dev/null +++ b/packages/js-drive/test/integration/identity/IdentityBalanceStoreRepository.spec.js @@ -0,0 +1,452 @@ +const fs = require('fs'); +const Drive = require('@dashevo/rs-drive'); +const { FeeResult } = require('@dashevo/rs-drive'); +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const IdentityBalanceStoreRepository = require('../../../lib/identity/IdentityBalanceStoreRepository'); +const IdentityStoreRepository = require('../../../lib/identity/IdentityStoreRepository'); +const logger = require('../../../lib/util/noopLogger'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('IdentityStoreRepository', () => { + let rsDrive; + let store; + let balanceRepository; + let identityRepository; + let decodeProtocolEntity; + let identity; + let blockInfo; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + await rsDrive.createInitialStateStructure(); + + store = new GroveDBStore(rsDrive, logger); + + decodeProtocolEntity = decodeProtocolEntityFactory(); + + balanceRepository = new IdentityBalanceStoreRepository(store, decodeProtocolEntity); + identityRepository = new IdentityStoreRepository(store, decodeProtocolEntity); + identity = getIdentityFixture(); + + blockInfo = new BlockInfo(1, 1, Date.now()); + }); + + afterEach(async () => { + await rsDrive.close(); + + fs.rmSync('./db/grovedb_test', { recursive: true, force: true }); + }); + + describe('#add', () => { + beforeEach(async () => { + await identityRepository.create( + identity, + blockInfo, + ); + }); + + it('should add to balance', async () => { + const amount = 100; + + const result = await balanceRepository.add( + identity.getId(), + amount, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const fetchedIdentity = fetchedIdentityResult.getValue(); + + expect(fetchedIdentity.getBalance()).to.equal(identity.getBalance() + amount); + }); + + it('should add to balance using transaction', async () => { + await store.startTransaction(); + + const amount = 100; + + await balanceRepository.add( + identity.getId(), + amount, + blockInfo, + { useTransaction: true }, + ); + + const previousIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const previousIdentity = previousIdentityResult.getValue(); + + expect(previousIdentity.getBalance()).to.equal(identity.getBalance()); + + const transactionalIdentityResult = await identityRepository.fetch( + identity.getId(), + { useTransaction: true }, + ); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity.getBalance()).to.equal(identity.getBalance() + amount); + + await store.commitTransaction(); + + const committedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const committedIdentity = committedIdentityResult.getValue(); + + expect(committedIdentity.getBalance()).to.equal(identity.getBalance() + amount); + }); + }); + + describe('#applyFees', () => { + beforeEach(async () => { + identity.setBalance(10000); + + await identityRepository.create( + identity, + blockInfo, + ); + }); + + it('should apply fees to balance', async () => { + const feeResult = FeeResult.create(1000, 100, []); + + const result = await balanceRepository.applyFees( + identity.getId(), + feeResult, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.instanceOf(FeeResult); + + const fetchedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const fetchedIdentity = fetchedIdentityResult.getValue(); + + expect(fetchedIdentity.getBalance()).to.equal( + identity.getBalance() - feeResult.storageFee - feeResult.processingFee, + ); + }); + + it('should add to balance using transaction', async () => { + await store.startTransaction(); + + const feeResult = FeeResult.create(1000, 100, []); + + await balanceRepository.applyFees( + identity.getId(), + feeResult, + { useTransaction: true }, + ); + + const previousIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const previousIdentity = previousIdentityResult.getValue(); + + expect(previousIdentity.getBalance()).to.equal(identity.getBalance()); + + const transactionalIdentityResult = await identityRepository.fetch( + identity.getId(), + { useTransaction: true }, + ); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity.getBalance()).to.equal( + identity.getBalance() - feeResult.storageFee - feeResult.processingFee, + ); + + await store.commitTransaction(); + + const committedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const committedIdentity = committedIdentityResult.getValue(); + + expect(committedIdentity.getBalance()).to.equal( + identity.getBalance() - feeResult.storageFee - feeResult.processingFee, + ); + }); + }); + + describe('#remove', () => { + beforeEach(async () => { + await identityRepository.create( + identity, + blockInfo, + ); + }); + + it('should remove from balance', async () => { + const amount = 5; + + const result = await balanceRepository.remove( + identity.getId(), + amount, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const fetchedIdentity = fetchedIdentityResult.getValue(); + + expect(fetchedIdentity.getBalance()).to.equal(identity.getBalance() - amount); + }); + + it('should remove from balance using transaction', async () => { + await store.startTransaction(); + + const amount = 5; + + await balanceRepository.remove( + identity.getId(), + amount, + blockInfo, + { useTransaction: true }, + ); + + const previousIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const previousIdentity = previousIdentityResult.getValue(); + + expect(previousIdentity.getBalance()).to.equal(identity.getBalance()); + + const transactionalIdentityResult = await identityRepository.fetch( + identity.getId(), + { useTransaction: true }, + ); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity.getBalance()).to.equal(identity.getBalance() - amount); + + await store.commitTransaction(); + + const committedIdentityResult = await identityRepository.fetch( + identity.getId(), + ); + + const committedIdentity = committedIdentityResult.getValue(); + + expect(committedIdentity.getBalance()).to.equal(identity.getBalance() - amount); + }); + }); + + describe('#fetch', () => { + context('without block info', () => { + it('should fetch null if identity not found', async () => { + const result = await balanceRepository.fetch(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch balance', async () => { + await identityRepository.create(identity, blockInfo); + + const result = await balanceRepository.fetch(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const balance = result.getValue(); + + expect(balance).to.equals(identity.getBalance()); + }); + + it('should fetch an identity using transaction', async () => { + await store.startTransaction(); + + await identityRepository.create(identity, blockInfo, { + useTransaction: true, + }); + + const notFoundBalanceResult = await balanceRepository.fetch(identity.getId(), { + useTransaction: false, + }); + + expect(notFoundBalanceResult.getValue()).to.be.null(); + + const transactionalBalanceResult = await balanceRepository.fetch(identity.getId(), { + useTransaction: true, + }); + + const transactionalBalance = transactionalBalanceResult.getValue(); + + expect(transactionalBalance).to.equals(identity.getBalance()); + + await store.commitTransaction(); + + const storedBalanceResult = await balanceRepository.fetch(identity.getId()); + + const storedBalance = storedBalanceResult.getValue(); + + expect(storedBalance).to.equals(identity.getBalance()); + }); + }); + + context('with block info', () => { + it('should fetch null if identity not found', async () => { + const result = await balanceRepository.fetch(identity.getId(), { blockInfo }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch an identity', async () => { + await identityRepository.create(identity, blockInfo); + + const result = await balanceRepository.fetch(identity.getId(), { blockInfo }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const storedBalance = result.getValue(); + + expect(storedBalance).to.equals(identity.getBalance()); + }); + + it('should fetch an identity using transaction', async () => { + await store.startTransaction(); + + await identityRepository.create(identity, blockInfo, { + useTransaction: true, + }); + + const notFoundBalanceResult = await balanceRepository.fetch(identity.getId(), { + blockInfo, + useTransaction: false, + }); + + expect(notFoundBalanceResult.getValue()).to.be.null(); + + const transactionalBalanceResult = await balanceRepository.fetch(identity.getId(), { + blockInfo, + useTransaction: true, + }); + + const transactionalBalance = transactionalBalanceResult.getValue(); + + expect(transactionalBalance).to.equals(identity.getBalance()); + + await store.commitTransaction(); + + const storedBalanceResult = await balanceRepository.fetch(identity.getId(), { + blockInfo, + }); + + const storedBalance = storedBalanceResult.getValue(); + + expect(storedBalance).to.equals(identity.getBalance()); + }); + }); + }); + + describe('#fetchWithDebt', () => { + it('should fetch null if identity not found', async () => { + const result = await balanceRepository.fetchWithDebt(identity.getId(), blockInfo); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch an identity', async () => { + await identityRepository.create(identity, blockInfo); + + const result = await balanceRepository.fetchWithDebt(identity.getId(), blockInfo); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const storedBalance = result.getValue(); + + expect(storedBalance).to.equals(identity.getBalance()); + }); + + it('should fetch an identity using transaction', async () => { + await store.startTransaction(); + + await identityRepository.create(identity, blockInfo, { + useTransaction: true, + }); + + const notFoundBalanceResult = await balanceRepository.fetchWithDebt( + identity.getId(), + blockInfo, + { + useTransaction: false, + }, + ); + + expect(notFoundBalanceResult.getValue()).to.be.null(); + + const transactionalBalanceResult = await balanceRepository.fetchWithDebt( + identity.getId(), + blockInfo, + { + useTransaction: true, + }, + ); + + const transactionalBalance = transactionalBalanceResult.getValue(); + + expect(transactionalBalance).to.equals(identity.getBalance()); + + await store.commitTransaction(); + + const storedBalanceResult = await balanceRepository.fetchWithDebt( + identity.getId(), + blockInfo, + ); + + const storedBalance = storedBalanceResult.getValue(); + + expect(storedBalance).to.equals(identity.getBalance()); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/IdentityPublicKeyStoreRepository.spec.js b/packages/js-drive/test/integration/identity/IdentityPublicKeyStoreRepository.spec.js new file mode 100644 index 00000000000..b51a68f8c25 --- /dev/null +++ b/packages/js-drive/test/integration/identity/IdentityPublicKeyStoreRepository.spec.js @@ -0,0 +1,177 @@ +const fs = require('fs'); +const Drive = require('@dashevo/rs-drive'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); +const IdentityPublicKeyStoreRepository = require('../../../lib/identity/IdentityPublicKeyStoreRepository'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const logger = require('../../../lib/util/noopLogger'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const IdentityStoreRepository = require('../../../lib/identity/IdentityStoreRepository'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('IdentityPublicKeyStoreRepository', () => { + let rsDrive; + let store; + let publicKeyRepository; + let identityRepository; + let identity; + let blockInfo; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, logger); + + await rsDrive.createInitialStateStructure(); + + const decodeProtocolEntity = decodeProtocolEntityFactory(); + + identityRepository = new IdentityStoreRepository(store, decodeProtocolEntity); + + publicKeyRepository = new IdentityPublicKeyStoreRepository(store, decodeProtocolEntity); + + identity = getIdentityFixture(); + + blockInfo = new BlockInfo(1, 1, Date.now()); + }); + + afterEach(async () => { + await rsDrive.close(); + + fs.rmSync('./db/grovedb_test', { recursive: true, force: true }); + }); + + describe('#add', () => { + it('should add public keys to identity', async () => { + const publicKey = identity.getPublicKeys().pop(); + + await identityRepository.create(identity, blockInfo); + + const result = await publicKeyRepository.add( + identity.getId(), + [publicKey], + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedIdentityResult = await identityRepository.fetch(identity.getId()); + + const fetchedIdentity = fetchedIdentityResult.getValue(); + + identity.getPublicKeys().push(publicKey); + + expect(fetchedIdentity.toObject()).to.deep.equals(identity.toObject()); + }); + + it('should store public key to identities using transaction', async () => { + const publicKey = identity.getPublicKeys().pop(); + + await identityRepository.create(identity, blockInfo); + + await store.startTransaction(); + + await publicKeyRepository.add( + identity.getId(), + [publicKey], + blockInfo, + { useTransaction: true }, + ); + + const noKeyIdentityResult = await identityRepository.fetch(identity.getId()); + + const noKeyIdentity = noKeyIdentityResult.getValue(); + + expect(noKeyIdentity.toObject()).to.deep.equals(identity.toObject()); + + const transactionalIdentityResult = await identityRepository.fetch(identity.getId(), { + useTransaction: true, + }); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + identity.getPublicKeys().push(publicKey); + + expect(transactionalIdentity.toObject()).to.deep.equals(identity.toObject()); + + await store.commitTransaction(); + + const committedIdentityResult = await identityRepository.fetch(identity.getId()); + + const committedIdentity = committedIdentityResult.getValue(); + + expect(committedIdentity.toObject()).to.deep.equals(identity.toObject()); + }); + }); + + describe('#disable', () => { + it('should disable public keys in identity', async () => { + await identityRepository.create(identity, blockInfo); + + const result = await publicKeyRepository.disable( + identity.getId(), + [0, 1], + Date.now(), + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedIdentityResult = await identityRepository.fetch(identity.getId()); + + const fetchedIdentity = fetchedIdentityResult.getValue(); + + expect(fetchedIdentity.toObject()).to.not.deep.equals(identity.toObject()); + }); + + it('should store public key to identities using transaction', async () => { + await identityRepository.create(identity, blockInfo); + + await store.startTransaction(); + + await publicKeyRepository.disable( + identity.getId(), + [0, 1], + Date.now(), + blockInfo, + { useTransaction: true }, + ); + + const noChangeIdentityResult = await identityRepository.fetch(identity.getId()); + + const noChangeIdentity = noChangeIdentityResult.getValue(); + + expect(noChangeIdentity.toObject()).to.deep.equals(identity.toObject()); + + const transactionalIdentityResult = await identityRepository.fetch(identity.getId(), { + useTransaction: true, + }); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity.toObject()).to.not.deep.equals(identity.toObject()); + + await store.commitTransaction(); + + const committedIdentityResult = await identityRepository.fetch(identity.getId()); + + const committedIdentity = committedIdentityResult.getValue(); + + expect(committedIdentity.toObject()).to.not.deep.equals(identity.toObject()); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/IdentityStoreRepository.spec.js b/packages/js-drive/test/integration/identity/IdentityStoreRepository.spec.js new file mode 100644 index 00000000000..dce6fe8f74b --- /dev/null +++ b/packages/js-drive/test/integration/identity/IdentityStoreRepository.spec.js @@ -0,0 +1,544 @@ +const fs = require('fs'); +const Drive = require('@dashevo/rs-drive'); +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const IdentityStoreRepository = require('../../../lib/identity/IdentityStoreRepository'); +const logger = require('../../../lib/util/noopLogger'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('IdentityStoreRepository', () => { + let rsDrive; + let store; + let repository; + let decodeProtocolEntity; + let identity; + let blockInfo; + let publicKeyHashes; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + await rsDrive.createInitialStateStructure(); + + store = new GroveDBStore(rsDrive, logger); + + decodeProtocolEntity = decodeProtocolEntityFactory(); + + repository = new IdentityStoreRepository(store, decodeProtocolEntity); + identity = getIdentityFixture(); + + blockInfo = new BlockInfo(1, 1, Date.now()); + + publicKeyHashes = identity.getPublicKeys().map((k) => k.hash()); + }); + + afterEach(async () => { + await rsDrive.close(); + + fs.rmSync('./db/grovedb_test', { recursive: true, force: true }); + }); + + describe('#create', () => { + it('should create an identity', async () => { + const result = await repository.create( + identity, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedResult = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(fetchedResult.toObject()).to.be.deep.equal(identity.toObject()); + }); + + it('should store identity using transaction', async () => { + await store.startTransaction(); + + await repository.create( + identity, + blockInfo, + { useTransaction: true }, + ); + + const notFoundIdentity = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(notFoundIdentity).to.be.null(); + + const identityTransaction = await rsDrive.fetchIdentity( + identity.getId(), + true, + ); + + expect(identityTransaction.toObject()).to.deep.equal(identity.toObject()); + + await store.commitTransaction(); + + const committedIdentity = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(committedIdentity.toObject()).to.deep.equal(identity.toObject()); + }); + }); + + describe('#updateRevision', () => { + beforeEach(async () => { + await repository.create( + identity, + blockInfo, + ); + }); + + it('should update revision', async () => { + const revision = 2; + + const result = await repository.updateRevision( + identity.getId(), + revision, + blockInfo, + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const fetchedIdentity = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(fetchedIdentity.getRevision()).to.equal(revision); + }); + + it('should remove from balance using transaction', async () => { + await store.startTransaction(); + + const revision = 2; + + await repository.updateRevision( + identity.getId(), + revision, + blockInfo, + { useTransaction: true }, + ); + + const previousIdentity = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(previousIdentity.getRevision()).to.equal(identity.getRevision()); + + const transactionalIdentity = await rsDrive.fetchIdentity( + identity.getId(), + true, + ); + + expect(transactionalIdentity.getRevision()).to.equal(revision); + + await store.commitTransaction(); + + const commitedIdentity = await rsDrive.fetchIdentity( + identity.getId(), + ); + + expect(commitedIdentity.getRevision()).to.equal(revision); + }); + }); + + describe('#fetch', () => { + context('without block info', () => { + it('should fetch null if identity not found', async () => { + const result = await repository.fetch(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch an identity', async () => { + await rsDrive.insertIdentity(identity, blockInfo); + + const result = await repository.fetch(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const storedIdentity = result.getValue(); + + expect(storedIdentity).to.be.an.instanceof(Identity); + expect(storedIdentity.toObject()).to.deep.equal(identity.toObject()); + }); + + it('should fetch an identity using transaction', async () => { + await store.startTransaction(); + + await rsDrive.insertIdentity(identity, blockInfo, true); + + const notFoundIdentityResult = await repository.fetch(identity.getId(), { + useTransaction: false, + }); + + expect(notFoundIdentityResult.getValue()).to.be.null(); + + const transactionalIdentityResult = await repository.fetch(identity.getId(), { + useTransaction: true, + }); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity).to.be.an.instanceof(Identity); + expect(transactionalIdentity.toObject()).to.deep.equal(identity.toObject()); + + await store.commitTransaction(); + + const storedIdentityResult = await repository.fetch(identity.getId()); + + const storedIdentity = storedIdentityResult.getValue(); + + expect(storedIdentity).to.be.an.instanceof(Identity); + expect(storedIdentity.toObject()).to.deep.equal(identity.toObject()); + }); + }); + + context('with block info', () => { + it('should fetch null if identity not found', async () => { + const result = await repository.fetch(identity.getId(), { blockInfo }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + expect(result.getValue()).to.be.null(); + }); + + it('should fetch an identity', async () => { + await rsDrive.insertIdentity(identity, blockInfo); + + const result = await repository.fetch(identity.getId(), { blockInfo }); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(1); + + const storedIdentity = result.getValue(); + + expect(storedIdentity).to.be.an.instanceof(Identity); + expect(storedIdentity.toObject()).to.deep.equal(identity.toObject()); + }); + + it('should fetch an identity using transaction', async () => { + await store.startTransaction(); + + await rsDrive.insertIdentity(identity, blockInfo, true); + + const notFoundIdentityResult = await repository.fetch(identity.getId(), { + blockInfo, + useTransaction: false, + }); + + expect(notFoundIdentityResult.getValue()).to.be.null(); + + const transactionalIdentityResult = await repository.fetch(identity.getId(), { + blockInfo, + useTransaction: true, + }); + + const transactionalIdentity = transactionalIdentityResult.getValue(); + + expect(transactionalIdentity).to.be.an.instanceof(Identity); + expect(transactionalIdentity.toObject()).to.deep.equal(identity.toObject()); + + await store.commitTransaction(); + + const storedIdentityResult = await repository.fetch(identity.getId(), { + blockInfo, + }); + + const storedIdentity = storedIdentityResult.getValue(); + + expect(storedIdentity).to.be.an.instanceof(Identity); + expect(storedIdentity.toObject()).to.deep.equal(identity.toObject()); + }); + }); + }); + + describe('#fetchByPublicKeyHashes', () => { + it('should fetch an identities by public key hashes', async () => { + await rsDrive.insertIdentity(identity, blockInfo); + + const result = await repository.fetchManyByPublicKeyHashes( + publicKeyHashes.concat([Buffer.alloc(20)]), + ); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const fetchedIdentities = result.getValue(); + + for (let i = 0; i < identity.getPublicKeys().length; i++) { + const fetchedIdentity = fetchedIdentities[i]; + + expect(fetchedIdentity).to.be.instanceOf(Identity); + expect(fetchedIdentity).to.deep.equal(identity.toObject()); + } + }); + }); + + describe('#proveManyByPublicKeyHashes', () => { + it('should fetch proof if public key to identities map not found', async () => { + const result = await repository.proveManyByPublicKeyHashes([ + Buffer.alloc(20, 1), + Buffer.alloc(20, 2), + ]); + + expect(result).to.be.instanceOf(StorageResult); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + }); + + it('should return proof', async () => { + await repository.create(identity, blockInfo); + + const result = await repository.proveManyByPublicKeyHashes(publicKeyHashes); + + expect(result).to.be.instanceOf(StorageResult); + + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + }); + + // TODO: Enable when transactions will be supported for queries with proofs + it.skip('should return proof map using transaction', async () => { + await store.startTransaction(); + + await repository.create(identity, blockInfo, { useTransaction: true }); + + // Should return proof of non-existence + let result = await repository.proveManyByPublicKeyHashes(publicKeyHashes); + + expect(result).to.be.instanceOf(StorageResult); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + + // Should return proof of existence + result = await repository.proveManyByPublicKeyHashes( + publicKeyHashes, + { useTransaction: true }, + ); + + expect(result).to.be.instanceOf(StorageResult); + + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + + await store.commitTransaction(); + + // Should return proof of existence + result = await repository.proveManyByPublicKeyHashes(publicKeyHashes); + + expect(result).to.be.instanceOf(StorageResult); + + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.an.instanceOf(Buffer); + expect(result.getValue().length).to.be.greaterThan(0); + }); + }); + + describe('#prove', () => { + it('should return prove if identity does not exist', async () => { + const result = await repository.prove(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof', async () => { + await repository.create(identity, blockInfo); + + const result = await repository.prove(identity.getId()); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + // TODO enable this test when we support transactions + it.skip('should return proof using transaction', async () => { + await store.startTransaction(); + + await store.createTree( + IdentityStoreRepository.TREE_PATH, + identity.getId().toBuffer(), + { useTransaction: true }, + ); + + await store.put( + IdentityStoreRepository.TREE_PATH.concat([identity.getId().toBuffer()]), + IdentityStoreRepository.IDENTITY_KEY, + identity.toBuffer(), + { useTransaction: true }, + ); + + const notFoundProof = await repository.prove(identity.getId(), { + useTransaction: false, + }); + + expect(notFoundProof.getValue()).to.be.null(); + + const transactionalIdentityResult = await repository.prove(identity.getId(), { + useTransaction: true, + }); + + const transactionalProof = transactionalIdentityResult.getValue(); + + expect(transactionalProof).to.be.an.instanceof(Buffer); + expect(transactionalProof.length).to.be.greaterThan(0); + + await store.commitTransaction(); + + const storedIdentityResult = await repository.prove(identity.getId()); + + const storedProof = storedIdentityResult.getValue(); + + expect(storedProof).to.be.an.instanceof(Buffer); + expect(storedProof.length).to.be.greaterThan(0); + }); + }); + + describe('#proveMany', () => { + let identity2; + + beforeEach(async () => { + // Set correct but unique public key data + const data = Buffer.from(identity.getPublicKeys()[0].getData()); + data[data.length - 1] = 2; + + identity2 = new Identity({ + protocolVersion: 1, + id: generateRandomIdentifier().toBuffer(), + publicKeys: [ + { + id: 0, + type: IdentityPublicKey.TYPES.ECDSA_SECP256K1, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: false, + data, + }, + ], + balance: 10, + revision: 0, + }); + }); + + it('should return proof if identity does not exist', async () => { + await repository.create(identity, blockInfo); + + const result = await repository.proveMany([identity.getId(), identity2.getId()]); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + it('should return proof', async () => { + await repository.create(identity, blockInfo); + await repository.create(identity2, blockInfo); + + const result = await repository.proveMany([identity.getId(), identity2.getId()]); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const proof = result.getValue(); + + expect(proof).to.be.an.instanceof(Buffer); + expect(proof.length).to.be.greaterThan(0); + }); + + // TODO enable this test when we support transactions + it.skip('should return proof using transaction', async () => { + await store.startTransaction(); + + await store.createTree( + IdentityStoreRepository.TREE_PATH, + identity.getId().toBuffer(), + { useTransaction: true }, + ); + + await store.put( + IdentityStoreRepository.TREE_PATH.concat([identity.getId().toBuffer()]), + IdentityStoreRepository.IDENTITY_KEY, + identity.toBuffer(), + { useTransaction: true }, + ); + + const notFoundProof = await repository.proveMany([identity.getId(), identity2.getId()], { + useTransaction: false, + }); + + expect(notFoundProof.getValue()).to.be.null(); + + const transactionalIdentityResult = await repository.proveMany( + [identity.getId(), identity2.getId()], + { useTransaction: true }, + ); + + const transactionalProof = transactionalIdentityResult.getValue(); + + expect(transactionalProof).to.be.an.instanceof(Buffer); + expect(transactionalProof.length).to.be.greaterThan(0); + + await store.commitTransaction(); + + const storedIdentityResult = await repository.proveMany( + [identity.getId(), identity2.getId()], + ); + + const storedProof = storedIdentityResult.getValue(); + + expect(storedProof).to.be.an.instanceof(Buffer); + expect(storedProof.length).to.be.greaterThan(0); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/SpentAssetLockTransactionsRepository.spec.js b/packages/js-drive/test/integration/identity/SpentAssetLockTransactionsRepository.spec.js new file mode 100644 index 00000000000..b21ab1687fc --- /dev/null +++ b/packages/js-drive/test/integration/identity/SpentAssetLockTransactionsRepository.spec.js @@ -0,0 +1,85 @@ +const Drive = require('@dashevo/rs-drive'); +const fs = require('fs'); + +const SpentAssetLockTransactionsRepository = require('../../../lib/identity/SpentAssetLockTransactionsRepository'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const GroveDBStore = require('../../../lib/storage/GroveDBStore'); +const logger = require('../../../lib/util/noopLogger'); + +describe('SpentAssetLockTransactionsRepository', () => { + let outPointBuffer; + let repository; + let store; + let rsDrive; + + beforeEach(async () => { + outPointBuffer = Buffer.from([42]); + + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, logger); + + await rsDrive.createInitialStateStructure(); + + repository = new SpentAssetLockTransactionsRepository(store); + }); + + afterEach(async () => { + await rsDrive.close(); + fs.rmSync('./db/grovedb_test', { recursive: true }); + }); + + describe('#store', () => { + it('should store outpoint', async () => { + const result = await repository.store(outPointBuffer); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const placeholderResult = await store.get( + SpentAssetLockTransactionsRepository.TREE_PATH, + outPointBuffer, + ); + + expect(placeholderResult.getValue()).to.deep.equal(Buffer.from([0])); + }); + }); + + describe('#fetch', () => { + it('should return null if outpoint is not present', async () => { + const result = await repository.fetch(outPointBuffer); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should return buffer containing [0]', async () => { + await store.put( + SpentAssetLockTransactionsRepository.TREE_PATH, + outPointBuffer, + Buffer.from([0]), + ); + + const result = await repository.fetch(outPointBuffer); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.deep.equal(Buffer.from([0])); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/masternode/LastSyncedCoreHeightRepository.spec.js b/packages/js-drive/test/integration/identity/masternode/LastSyncedCoreHeightRepository.spec.js new file mode 100644 index 00000000000..d9c75f95503 --- /dev/null +++ b/packages/js-drive/test/integration/identity/masternode/LastSyncedCoreHeightRepository.spec.js @@ -0,0 +1,89 @@ +const Drive = require('@dashevo/rs-drive'); +const fs = require('fs'); + +const LastSyncedSmlHeightRepository = require('../../../../lib/identity/masternode/LastSyncedCoreHeightRepository'); +const StorageResult = require('../../../../lib/storage/StorageResult'); +const GroveDBStore = require('../../../../lib/storage/GroveDBStore'); +const logger = require('../../../../lib/util/noopLogger'); + +describe('LastSyncedSmlHeightRepository', () => { + let repository; + let store; + let rsDrive; + + beforeEach(async () => { + rsDrive = new Drive('./db/grovedb_test', { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + store = new GroveDBStore(rsDrive, logger); + + repository = new LastSyncedSmlHeightRepository(store); + + // Create initial structure + await rsDrive.createInitialStateStructure(false); + }); + + afterEach(async () => { + await rsDrive.close(); + fs.rmSync('./db/grovedb_test', { recursive: true }); + }); + + describe('#store', () => { + it('should store last synced height', async () => { + const result = await repository.store(1); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + const placeholderResult = await store.get( + LastSyncedSmlHeightRepository.TREE_PATH, + LastSyncedSmlHeightRepository.KEY, + ); + + const encodedValue = placeholderResult.getValue(); + + expect(encodedValue.readUInt32BE()).to.deep.equal(1); + }); + }); + + describe('#fetch', () => { + it('should return null if last synced height is not present', async () => { + const result = await repository.fetch(); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.null(); + }); + + it('should return last synced height', async () => { + const encodedValue = Buffer.alloc(4); + + encodedValue.writeUInt32BE(1); + + await store.put( + LastSyncedSmlHeightRepository.TREE_PATH, + LastSyncedSmlHeightRepository.KEY, + encodedValue, + ); + + const result = await repository.fetch(); + + expect(result).to.be.instanceOf(StorageResult); + expect(result.getOperations().length).to.equal(0); + + expect(result.getValue()).to.be.deep.equal(1); + }); + }); +}); diff --git a/packages/js-drive/test/integration/identity/masternode/synchronizeMasternodeIdentitiesFactory.spec.js b/packages/js-drive/test/integration/identity/masternode/synchronizeMasternodeIdentitiesFactory.spec.js new file mode 100644 index 00000000000..78a548b476c --- /dev/null +++ b/packages/js-drive/test/integration/identity/masternode/synchronizeMasternodeIdentitiesFactory.spec.js @@ -0,0 +1,1093 @@ +const { + asValue, +} = require('awilix'); + +const SimplifiedMNListEntry = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListEntry'); +const { hash } = require('@dashevo/dpp/lib/util/hash'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); + +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const createTestDIContainer = require('../../../../lib/test/createTestDIContainer'); +const createOperatorIdentifier = require('../../../../lib/identity/masternode/createOperatorIdentifier'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); +const createVotingIdentifier = require('../../../../lib/identity/masternode/createVotingIdentifier'); +const getSystemIdentityPublicKeysFixture = require('../../../../lib/test/fixtures/getSystemIdentityPublicKeysFixture'); + +/** + * @param {IdentityStoreRepository} identityRepository + * @param {IdentityPublicKeyStoreRepository} identityPublicKeyRepository + * @param {getWithdrawPubKeyTypeFromPayoutScript} getWithdrawPubKeyTypeFromPayoutScript + * @param {getPublicKeyFromPayoutScript} getPublicKeyFromPayoutScript + * @returns {expectOperatorIdentity} + */ +function expectOperatorIdentityFactory( + identityRepository, + identityPublicKeyRepository, + getWithdrawPubKeyTypeFromPayoutScript, + getPublicKeyFromPayoutScript, +) { + /** + * @typedef {expectOperatorIdentity} + * @param {SimplifiedMNListEntry} smlEntry + * @param {Address} [previousPayoutAddress] + * @param {Address} [payoutAddress] + * @returns {Promise} + */ + async function expectOperatorIdentity( + smlEntry, + previousPayoutAddress, + payoutAddress, + ) { + // Validate operator identity + + const operatorIdentifier = createOperatorIdentifier(smlEntry); + + const operatorIdentityResult = await identityRepository.fetch( + operatorIdentifier, + { useTransaction: true }, + ); + + const operatorIdentity = operatorIdentityResult.getValue(); + + expect(operatorIdentity).to.exist(); + + // Validate operator public keys + + const operatorPubKey = Buffer.from(smlEntry.pubKeyOperator, 'hex'); + + let publicKeysNum = 1; + if (payoutAddress) { + publicKeysNum += 1; + } + if (previousPayoutAddress) { + publicKeysNum += 1; + } + + expect(operatorIdentity.getPublicKeys()) + .to + .have + .lengthOf(publicKeysNum); + + const firstOperatorMasternodePublicKey = operatorIdentity.getPublicKeyById(0); + expect(firstOperatorMasternodePublicKey.getType()) + .to + .equal(IdentityPublicKey.TYPES.BLS12_381); + expect(firstOperatorMasternodePublicKey.getData()) + .to + .deep + .equal(operatorPubKey); + + const firstOperatorIdentityByPublicKeyHashResult = await identityRepository + .fetchByPublicKeyHash(firstOperatorMasternodePublicKey.hash(), { useTransaction: true }); + + const firstOperatorIdentityByPublicKeyHash = firstOperatorIdentityByPublicKeyHashResult + .getValue(); + + expect(firstOperatorIdentityByPublicKeyHash).to.be.not.null(); + expect(firstOperatorIdentityByPublicKeyHash.getId()) + .to + .deep + .equal(operatorIdentifier); + + let i = 0; + + if (previousPayoutAddress) { + i += 1; + const payoutScript = new Script(previousPayoutAddress); + const publicKeyType = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + const payoutPublicKey = operatorIdentity.getPublicKeyById(i); + expect(payoutPublicKey.getType()).to.equal(publicKeyType); + expect(payoutPublicKey.getData()).to.deep.equal( + getPublicKeyFromPayoutScript(payoutScript, publicKeyType), + ); + + const masternodeIdentityByPayoutPublicKeyHashResult = await identityPublicKeyRepository + .fetch(payoutPublicKey.hash(), { useTransaction: true }); + + const masternodeIdentityByPayoutPublicKeyHash = masternodeIdentityByPayoutPublicKeyHashResult + .getValue(); + + expect(masternodeIdentityByPayoutPublicKeyHash).to.have.lengthOf(1); + expect(masternodeIdentityByPayoutPublicKeyHash[0].toBuffer()) + .to.deep.equal(operatorIdentifier); + } + + if (payoutAddress) { + i += 1; + const payoutScript = new Script(payoutAddress); + const publicKeyType = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + const payoutPublicKey = operatorIdentity.getPublicKeyById(i); + expect(payoutPublicKey.getType()).to.equal(publicKeyType); + expect(payoutPublicKey.getData()).to.deep.equal( + getPublicKeyFromPayoutScript(payoutScript, publicKeyType), + ); + + const masternodeIdentityByPayoutPublicKeyHashResult = await identityRepository + .fetchByPublicKeyHash(payoutPublicKey.hash(), { useTransaction: true }); + + const masternodeIdentityByPayoutPublicKeyHash = masternodeIdentityByPayoutPublicKeyHashResult + .getValue(); + + expect(masternodeIdentityByPayoutPublicKeyHash).to.be.not.null(); + expect(masternodeIdentityByPayoutPublicKeyHash.getId()) + .to.deep.equal(operatorIdentifier); + } + } + + return expectOperatorIdentity; +} + +/** + * @param {IdentityStoreRepository} identityRepository + * @returns {expectVotingIdentity} + */ +function expectVotingIdentityFactory( + identityRepository, +) { + /** + * @typedef {expectVotingIdentity} + * @param {SimplifiedMNListEntry} smlEntry + * @param {Buffer} proRegTx + * @returns {Promise} + */ + async function expectVotingIdentity( + smlEntry, + proRegTx, + ) { + // Validate voting identity + + const votingIdentifier = createVotingIdentifier(smlEntry); + + const votingIdentityResult = await identityRepository.fetch(votingIdentifier, { + useTransaction: true, + }); + + const votingIdentity = votingIdentityResult.getValue(); + + expect(votingIdentity) + .to + .exist(); + + // Validate voting public keys + + expect(votingIdentity.getPublicKeys()) + .to + .have + .lengthOf(1); + + const masternodePublicKey = votingIdentity.getPublicKeyById(0); + expect(masternodePublicKey.getType()).to.equal(IdentityPublicKey.TYPES.ECDSA_HASH160); + expect(masternodePublicKey.getData()).to.deep.equal( + Buffer.from(proRegTx.extraPayload.keyIDVoting, 'hex').reverse(), + ); + + const masternodeIdentityByPublicKeyHashResult = await identityRepository + .fetchByPublicKeyHash(masternodePublicKey.hash(), { + useTransaction: true, + }); + + const masternodeIdentityByPublicKeyHash = masternodeIdentityByPublicKeyHashResult.getValue(); + + expect(masternodeIdentityByPublicKeyHash).to.be.not.null(); + expect(masternodeIdentityByPublicKeyHash.getId()) + .to.deep.equal(votingIdentifier); + } + + return expectVotingIdentity; +} + +/** + * @param {IdentityStoreRepository} identityRepository + * @param {IdentityPublicKeyStoreRepository} identityPublicKeyRepository + * @param {getWithdrawPubKeyTypeFromPayoutScript} getWithdrawPubKeyTypeFromPayoutScript + * @param {getPublicKeyFromPayoutScript} getPublicKeyFromPayoutScript + * @returns {expectMasternodeIdentity} + */ +function expectMasternodeIdentityFactory( + identityRepository, + identityPublicKeyRepository, + getWithdrawPubKeyTypeFromPayoutScript, + getPublicKeyFromPayoutScript, +) { + /** + * @typedef {expectMasternodeIdentity} + * @param {SimplifiedMNListEntry} smlEntry + * @param {Object} proRegTx + * @param {Address} [previousPayoutAddress] + * @param {Address} [payoutAddress] + * @returns {Promise} + */ + async function expectMasternodeIdentity( + smlEntry, + proRegTx, + previousPayoutAddress, + payoutAddress, + ) { + const masternodeIdentifier = Identifier.from( + Buffer.from(smlEntry.proRegTxHash, 'hex'), + ); + + const masternodeIdentityResult = await identityRepository.fetch( + masternodeIdentifier, + { useTransaction: true }, + ); + + const masternodeIdentity = masternodeIdentityResult.getValue(); + + expect(masternodeIdentity).to.be.not.null(); + + // Validate masternode identity public keys + let publicKeysNum = 1; + if (payoutAddress) { + publicKeysNum += 1; + } + if (previousPayoutAddress) { + publicKeysNum += 1; + } + + expect(masternodeIdentity.getPublicKeys()).to.have.lengthOf(publicKeysNum); + + const masternodePublicKey = masternodeIdentity.getPublicKeyById(0); + expect(masternodePublicKey.getType()).to.equal(IdentityPublicKey.TYPES.ECDSA_HASH160); + expect(masternodePublicKey.getData()).to.deep.equal( + Buffer.from(proRegTx.extraPayload.keyIDOwner, 'hex').reverse(), + ); + + const masternodeIdentityByPublicKeyHashResult = await identityRepository + .fetchManyByPublicKeyHashes([masternodePublicKey.hash()], { useTransaction: true }); + + const masternodeIdentityByPublicKeyHash = masternodeIdentityByPublicKeyHashResult.getValue(); + + expect(masternodeIdentityByPublicKeyHash).to.have.lengthOf(1); + expect(masternodeIdentityByPublicKeyHash[0].getId()) + .to.deep.equal(masternodeIdentifier); + + let i = 0; + + if (previousPayoutAddress) { + i += 1; + const payoutScript = new Script(previousPayoutAddress); + const publicKeyType = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + const payoutPublicKey = masternodeIdentity.getPublicKeyById(i); + expect(payoutPublicKey.getType()).to.equal(publicKeyType); + expect(payoutPublicKey.getData()).to.deep.equal( + getPublicKeyFromPayoutScript(payoutScript, publicKeyType), + ); + + const masternodeIdentityByPayoutPublicKeyHashResult = await identityRepository + .fetchByPublicKeyHash(payoutPublicKey.hash(), { useTransaction: true }); + + const masternodeIdentityByPayoutPublicKeyHash = masternodeIdentityByPayoutPublicKeyHashResult + .getValue(); + + expect(masternodeIdentityByPayoutPublicKeyHash).to.not.be.null(); + expect(masternodeIdentityByPayoutPublicKeyHash.getId()) + .to.deep.equal(masternodeIdentifier); + } + + if (payoutAddress) { + i += 1; + const payoutScript = new Script(payoutAddress); + const publicKeyType = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + const payoutPublicKey = masternodeIdentity.getPublicKeyById(i); + expect(payoutPublicKey.getType()).to.equal(publicKeyType); + expect(payoutPublicKey.getData()).to.deep.equal( + getPublicKeyFromPayoutScript(payoutScript, publicKeyType), + ); + + const masternodeIdentityByPayoutPublicKeyHashResult = await identityRepository + .fetchByPublicKeyHash(payoutPublicKey.hash(), { useTransaction: true }); + + const masternodeIdentityByPayoutPublicKeyHash = masternodeIdentityByPayoutPublicKeyHashResult + .getValue(); + + expect(masternodeIdentityByPayoutPublicKeyHash).to.not.be.null(); + expect(masternodeIdentityByPayoutPublicKeyHash.getId()) + .to.deep.equal(masternodeIdentifier); + } + } + + return expectMasternodeIdentity; +} + +/** + * @param {GroveDBStore} groveDBStore + * @returns {expectDeterministicAppHash} + */ +function expectDeterministicAppHashFactory(groveDBStore) { + /** + * @typedef {expectDeterministicAppHash} + * @param {string} appHash + * @returns {Promise} + */ + async function expectDeterministicAppHash(appHash) { + const actualAppHash = await groveDBStore.getRootHash({ useTransaction: true }); + + const actualAppHashHex = actualAppHash.toString('hex'); + + expect(actualAppHashHex).to.deep.equal(appHash); + } + + return expectDeterministicAppHash; +} + +// TODO: Enable keys when we have support of non unique keys in DPP +describe.skip('synchronizeMasternodeIdentitiesFactory', function main() { + this.timeout(10000); + let container; + let coreHeight; + let fetchSimplifiedMNListMock; + let fetchedSimplifiedMNList; + let fetchTransactionMock; + let smlStoreMock; + let smlFixture; + let newSmlFixture; + let transaction1; + let transaction2; + let transaction3; + let synchronizeMasternodeIdentities; + let rewardsDataContract; + let identityRepository; + let documentRepository; + let identityPublicKeyRepository; + let expectOperatorIdentity; + let expectVotingIdentity; + let expectMasternodeIdentity; + let expectDeterministicAppHash; + let firstSyncAppHash; + let blockInfo; + + beforeEach(async function beforeEach() { + coreHeight = 3; + firstSyncAppHash = 'c55de453e3ea4481f20225efdc12d671f715f0618cf3084bb32e56e75123bfdd'; + blockInfo = new BlockInfo(10, 0, 1668702100799); + + container = await createTestDIContainer(); + + // Mock Core RPC + + fetchedSimplifiedMNList = { + mnList: [], + }; + + fetchSimplifiedMNListMock = this.sinon.stub().resolves(fetchedSimplifiedMNList); + + container.register('fetchSimplifiedMNList', asValue(fetchSimplifiedMNListMock)); + + // Mock SML + + smlFixture = [ + new SimplifiedMNListEntry({ + proRegTxHash: 'a2c9b34ef525271d84f70a0d4d2c107e8a2f81cd4d8256dc7b3911ed253d5611', + confirmedHash: '29ff8afb463604ba7d984b483e92dfefa4e80e12de3acae6d75f9b910df9eab6', + service: '192.168.65.2:20201', + pubKeyOperator: 'a5ad6d8cad7b233210b718a5fc9ec3cea18aeebe38b2e3122deb581e430aa28875fe7336c283871db42808f8d4107745', + votingAddress: 'yRXtaRmQ7LCmT5XcgzQdLwPEf31dycBaeY', + isValid: true, + payoutAddress: 'yR843jN58m5dubmQjfUmKDDJMJzNatFV9M', + payoutOperatorAddress: 'yNjsnYM16J5NZPA2P8BKJG3MKfUD7XHAFE', + nType: 0, + }), + new SimplifiedMNListEntry({ + proRegTxHash: 'f5ec54aed788c434da2fc535ea6b125ec6fc54e58bc0a00a005d1a8d5e477a90', + confirmedHash: '53125505b0e9d11b371cf3e12c92d164296dfa215fde6201d28ea44bed992187', + service: '192.168.65.2:20101', + pubKeyOperator: '951a3208ba531ea75aedd2dc0a9efc75f2c4d9492f1ee0a989b593bcd9722b1a101774d80a426552a9f91d24eb55af6e', + votingAddress: 'yYH1rgZsgvkmT8bSSSw1cKCjyVPnFpTBCw', + isValid: true, + payoutAddress: 'ycL7L4mhYoaZdm9TH85svvpfeKtdfo249u', + nType: 0, + }), + ]; + + newSmlFixture = [ + new SimplifiedMNListEntry({ + proRegTxHash: '1c81a5faa2c0e0d96eb59c58a10fcbc87f431bb6cd880d960b43b269e682d2d2', + confirmedHash: '03cc2acc135ab51304d3cff42215c7a8041902fa3f19451d5562a03b38143e8f', + service: '192.168.65.2:20001', + pubKeyOperator: '96f83eedc8a7b87663e591987f051ce341a6fb88989322c64bbbf56d205e4e77d2cb7d839d8b4106a8a1f5d5cf7cfa57', + votingAddress: 'ybJfuKs59MJWkPEnS8qNmtvdisHrCy7Njn', + isValid: true, + payoutAddress: '7UkJidhNjEPJCQnCTXeaJKbJmL4JuyV66w', + payoutOperatorAddress: 'yPDBTHAjPwJfZSSQYczccA78XRS2tZ5fZF', + nType: 0, + }), + ]; + + smlStoreMock = { + getSMLbyHeight: this.sinon.stub().returns({ mnList: smlFixture }), + }; + + const simplifiedMasternodeListMock = { + getStore: this.sinon.stub().returns(smlStoreMock), + }; + + container.register('simplifiedMasternodeList', asValue(simplifiedMasternodeListMock)); + + // Mock fetchTransaction + + fetchTransactionMock = this.sinon.stub(); + + transaction1 = { + extraPayload: { + operatorReward: 100, + keyIDOwner: Buffer.alloc(20).fill('a').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('b').toString('hex'), + }, + }; + + transaction2 = { + extraPayload: { + operatorReward: 0, + keyIDOwner: Buffer.alloc(20).fill('c').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('d').toString('hex'), + }, + }; + + transaction3 = { + extraPayload: { + operatorReward: 200, + keyIDOwner: Buffer.alloc(20).fill('e').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('f').toString('hex'), + }, + }; + + fetchTransactionMock.withArgs('a2c9b34ef525271d84f70a0d4d2c107e8a2f81cd4d8256dc7b3911ed253d5611').resolves(transaction1); + fetchTransactionMock.withArgs('f5ec54aed788c434da2fc535ea6b125ec6fc54e58bc0a00a005d1a8d5e477a90').resolves(transaction2); + fetchTransactionMock.withArgs('1c81a5faa2c0e0d96eb59c58a10fcbc87f431bb6cd880d960b43b269e682d2d2').resolves(transaction3); + + container.register('fetchTransaction', asValue(fetchTransactionMock)); + + const groveDBStore = container.resolve('groveDBStore'); + await groveDBStore.startTransaction(); + + /** + * @type {Drive} + */ + const rsDrive = container.resolve('rsDrive'); + await rsDrive.getAbci().initChain({ + genesisTimeMs: 0, + systemIdentityPublicKeys: getSystemIdentityPublicKeysFixture(), + }, true); + + const masternodeRewardSharesContractId = container.resolve('masternodeRewardSharesContractId'); + + [rewardsDataContract] = await rsDrive.fetchContract(masternodeRewardSharesContractId, 0, true); + + /** + * @type {synchronizeMasternodeIdentities} + */ + synchronizeMasternodeIdentities = container.resolve('synchronizeMasternodeIdentities'); + + identityRepository = container.resolve('identityRepository'); + documentRepository = container.resolve('documentRepository'); + identityPublicKeyRepository = container.resolve('identityPublicKeyRepository'); + const getWithdrawPubKeyTypeFromPayoutScript = container.resolve('getWithdrawPubKeyTypeFromPayoutScript'); + const getPublicKeyFromPayoutScript = container.resolve('getPublicKeyFromPayoutScript'); + + expectOperatorIdentity = expectOperatorIdentityFactory( + identityRepository, + identityPublicKeyRepository, + getWithdrawPubKeyTypeFromPayoutScript, + getPublicKeyFromPayoutScript, + ); + + expectVotingIdentity = expectVotingIdentityFactory( + identityRepository, + ); + + expectMasternodeIdentity = expectMasternodeIdentityFactory( + identityRepository, + identityPublicKeyRepository, + getWithdrawPubKeyTypeFromPayoutScript, + getPublicKeyFromPayoutScript, + ); + + expectDeterministicAppHash = expectDeterministicAppHashFactory( + container.resolve('groveDBStore'), + ); + }); + + afterEach(async () => { + if (container) { + await container.dispose(); + } + }); + + it('should create identities for all masternodes on the first sync', async () => { + const result = await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + expect(result.fromHeight).to.be.equal(0); + expect(result.toHeight).to.be.equal(3); + expect(result.createdEntities).to.have.lengthOf(6); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + await expectDeterministicAppHash(firstSyncAppHash); + + /** + * Validate first masternode + */ + + // Masternode identity should be created + + await expectMasternodeIdentity( + smlFixture[0], + transaction1, + undefined, + Address.fromString(smlFixture[0].payoutAddress), + ); + + // voting identity should be created + await expectVotingIdentity( + smlFixture[0], + transaction1, + ); + + // Operator identity should be created + + await expectOperatorIdentity(smlFixture[0]); + + // Masternode reward shares should be created + + const firstMasternodeIdentifier = Identifier.from( + Buffer.from(smlFixture[0].proRegTxHash, 'hex'), + ); + + const firstOperatorIdentifier = createOperatorIdentifier(smlFixture[0]); + + let documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', firstMasternodeIdentifier], + ['payToId', '==', firstOperatorIdentifier], + ], + useTransaction: true, + }, + ); + + let documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(1); + + const expectedDocumentId = Identifier.from( + hash( + Buffer.concat([ + firstMasternodeIdentifier, + firstOperatorIdentifier, + ]), + ), + ); + + expect(documents[0].getId()).to.deep.equal(expectedDocumentId); + expect(documents[0].getOwnerId()).to.deep.equal(firstMasternodeIdentifier); + expect(documents[0].get('percentage')).to.equal(100); + expect(documents[0].get('payToId')).to.deep.equal(firstOperatorIdentifier); + + /** + * Validate second masternode + */ + + // Masternode identity should be created + + await expectMasternodeIdentity( + smlFixture[1], + transaction2, + undefined, + Address.fromString(smlFixture[1].payoutAddress), + ); + + // Voting identity should be created + await expectVotingIdentity( + smlFixture[1], + transaction2, + ); + + // Operator identity shouldn't be created + + const secondOperatorPubKey = Buffer.from(smlFixture[1].pubKeyOperator, 'hex'); + + const secondOperatorIdentifier = Identifier.from( + hash( + Buffer.concat([ + Buffer.from(smlFixture[1].proRegTxHash, 'hex'), + secondOperatorPubKey, + ]), + ), + ); + + const secondOperatorIdentityResult = await identityRepository.fetch( + secondOperatorIdentifier, + { useTransaction: true }, + ); + + const secondOperatorIdentity = secondOperatorIdentityResult.getValue(); + + expect(secondOperatorIdentity).to.be.null(); + + // Masternode reward shares shouldn't be created + + const secondMasternodeIdentifier = Identifier.from( + Buffer.from(smlFixture[1].proRegTxHash, 'hex'), + ); + + documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', secondMasternodeIdentifier], + ['payToId', '==', secondOperatorIdentifier], + ], + useTransaction: true, + }, + ); + + documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(0); + }); + + it('should sync identities if the gap between coreHeight and lastSyncedCoreHeight > smlMaxListsLimit', async () => { + // Sync initial list + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + await expectDeterministicAppHash(firstSyncAppHash); + + const nextCoreHeight = coreHeight + 42; + + // Mock SML + + smlStoreMock.getSMLbyHeight.withArgs(nextCoreHeight).returns({ + mnList: smlFixture.concat(newSmlFixture), + }); + + fetchedSimplifiedMNList.mnList = smlFixture; + + // Second call + + const result = await synchronizeMasternodeIdentities(nextCoreHeight, blockInfo); + + expect(result.fromHeight).to.be.equal(3); + expect(result.toHeight).to.be.equal(45); + expect(result.createdEntities).to.have.lengthOf(4); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + // Nothing happened + + await expectDeterministicAppHash('a789fe73ceea6769634b98ae82dddad5013c4711b2a8353d2150b813fb953cb3'); + + // Core RPC should be called + + expect(fetchSimplifiedMNListMock).to.have.been.calledOnceWithExactly(1, coreHeight); + }); + + it('should create masternode identities if new masternode appeared', async () => { + // Sync initial list + + const result = await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + expect(result.fromHeight).to.be.equal(0); + expect(result.toHeight).to.be.equal(coreHeight); + expect(result.createdEntities).to.have.lengthOf(6); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + await expectDeterministicAppHash(firstSyncAppHash); + + // Mock SML + + smlStoreMock.getSMLbyHeight.withArgs(coreHeight + 1).returns( + { mnList: smlFixture.concat(newSmlFixture) }, + ); + + // Second call + + const result2 = await synchronizeMasternodeIdentities(coreHeight + 1, blockInfo); + + expect(result2.fromHeight).to.be.equal(3); + expect(result2.toHeight).to.be.equal(4); + expect(result2.createdEntities).to.have.lengthOf(4); + expect(result2.updatedEntities).to.have.lengthOf(0); + expect(result2.removedEntities).to.have.lengthOf(0); + + await expectDeterministicAppHash('b58bc0499156caea9b930ab0e357f56b20bb191bc7ba97a2eb8941d9ba7a8183'); + + // New masternode identity should be created + + await expectMasternodeIdentity( + newSmlFixture[0], + transaction3, + undefined, + Address.fromString(newSmlFixture[0].payoutAddress), + ); + + // New voting identity should be created + + await expectVotingIdentity( + newSmlFixture[0], + transaction3, + ); + + // New operator should be created + + await expectOperatorIdentity(newSmlFixture[0]); + + // Masternode reward shares should be created + + const newMasternodeIdentifier = Identifier.from( + Buffer.from(newSmlFixture[0].proRegTxHash, 'hex'), + ); + + const newOperatorIdentifier = createOperatorIdentifier(newSmlFixture[0]); + + const documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', newMasternodeIdentifier], + ['payToId', '==', newOperatorIdentifier], + ], + useTransaction: true, + }, + ); + + const documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(1); + + const expectedDocumentId = Identifier.from( + hash( + Buffer.concat([ + newMasternodeIdentifier, + newOperatorIdentifier, + ]), + ), + ); + + expect(documents[0].getId()).to.deep.equal(expectedDocumentId); + expect(documents[0].getOwnerId()).to.deep.equal(newMasternodeIdentifier); + expect(documents[0].get('percentage')).to.equal(200); + expect(documents[0].get('payToId')).to.deep.equal(newOperatorIdentifier); + }); + + it('should remove reward shares if masternode disappeared', async () => { + // Sync initial list + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + await expectDeterministicAppHash(firstSyncAppHash); + + // Mock SML + + smlStoreMock.getSMLbyHeight.withArgs(coreHeight + 1).returns( + { mnList: [smlFixture[1]] }, + ); + + // Second call + + const result = await synchronizeMasternodeIdentities(coreHeight + 1, blockInfo); + + expect(result.fromHeight).to.be.equal(3); + expect(result.toHeight).to.be.equal(4); + expect(result.createdEntities).to.have.lengthOf(0); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(1); + + await expectDeterministicAppHash('ea28d7339efd80984e53bd06b0d3708611f24862f401997e9cd69328af6a54c2'); + + // Masternode identity should stay + + await expectMasternodeIdentity( + smlFixture[0], + transaction1, + undefined, + Address.fromString(smlFixture[0].payoutAddress), + ); + + // Voting identity should stay + + await expectVotingIdentity( + smlFixture[0], + transaction1, + ); + + // Operator identity should stay + + await expectOperatorIdentity(smlFixture[0]); + + // Masternode reward shares should be removed + + const removedMasternodeIdentifier = Buffer.from(smlFixture[0].proRegTxHash, 'hex'); + + const documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', removedMasternodeIdentifier], + ], + useTransaction: true, + }, + ); + + const documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(0); + }); + + it('should remove reward shares if masternode is not valid', async () => { + // Sync initial list + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + await expectDeterministicAppHash(firstSyncAppHash); + + // Mock SML + + const invalidSmlEntry = smlFixture[0].copy(); + invalidSmlEntry.isValid = false; + + smlStoreMock.getSMLbyHeight.withArgs(coreHeight + 1).returns( + { mnList: [smlFixture[1], invalidSmlEntry] }, + ); + + // Second call + + const result = await synchronizeMasternodeIdentities(coreHeight + 1, blockInfo); + + expect(result.fromHeight).to.be.equal(3); + expect(result.toHeight).to.be.equal(4); + expect(result.createdEntities).to.have.lengthOf(0); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(1); + + await expectDeterministicAppHash('ea28d7339efd80984e53bd06b0d3708611f24862f401997e9cd69328af6a54c2'); + + const invalidMasternodeIdentifier = Identifier.from( + Buffer.from(invalidSmlEntry.proRegTxHash, 'hex'), + ); + + // Masternode reward shares should be removed + + const documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', invalidMasternodeIdentifier], + ], + useTransaction: true, + }, + ); + + const documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(0); + }); + + it('should create operator identity and reward shares if PubKeyOperator was changed', async () => { + // Initial sync + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + await expectDeterministicAppHash(firstSyncAppHash); + + // Mock SML + + const changedSmlEntry = smlFixture[0].copy(); + changedSmlEntry.pubKeyOperator = '96f83eedc8a7b87663e591987f051ce341a6fb88989322c64bbbf56d205e4e77d2cb7d839d8b4106a8a1f5d5cf7cfa57'; + + smlStoreMock.getSMLbyHeight.withArgs(coreHeight + 1).returns( + { mnList: [smlFixture[1], changedSmlEntry] }, + ); + + // Second call + + const result = await synchronizeMasternodeIdentities(coreHeight + 1, blockInfo); + + expect(result.fromHeight).to.be.equal(3); + expect(result.toHeight).to.be.equal(4); + expect(result.createdEntities).to.have.lengthOf(2); + expect(result.updatedEntities).to.have.lengthOf(1); + expect(result.removedEntities).to.have.lengthOf(1); + + await expectDeterministicAppHash('d80a3bbf5e699a0295e4ba734e3729d12bef3001bdd9cdd295673832d05454cf'); + + // Masternode identity should stay + + await expectMasternodeIdentity( + smlFixture[0], + transaction1, + undefined, + Address.fromString(smlFixture[0].payoutAddress), + ); + + // Previous voting identity should stay + + await expectVotingIdentity( + smlFixture[0], + transaction1, + ); + + // Previous operator identity should stay + + await expectOperatorIdentity(smlFixture[0]); + + // New operator identity should be created + + await expectOperatorIdentity(changedSmlEntry); + + // Only new masternode reward shares should exist + + const changedMasternodeIdentifier = Identifier.from( + Buffer.from(changedSmlEntry.proRegTxHash, 'hex'), + ); + + const documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', changedMasternodeIdentifier], + ], + useTransaction: true, + }, + ); + + const documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(1); + + const [document] = documents; + + const newOperatorIdentifier = createOperatorIdentifier(changedSmlEntry); + + expect(document.get('payToId')).to.deep.equal(newOperatorIdentifier); + }); + + it('should handle changed payout, voting and operator payout addresses', async () => { + // Sync initial list + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + + await expectDeterministicAppHash(firstSyncAppHash); + + // Mock SML + + const changedSmlEntry = smlFixture[0].copy(); + changedSmlEntry.payoutAddress = 'yMLrhooXyJtpV3R2ncsxvkrh6wRennNPoG'; + changedSmlEntry.operatorPayoutAddress = 'yT8DDY5NkX4ZtBkUVz7y1RgzbakCnMPogh'; + + smlStoreMock.getSMLbyHeight.withArgs(coreHeight + 1).returns( + { mnList: [smlFixture[1], changedSmlEntry] }, + ); + + // Second call + + await synchronizeMasternodeIdentities(coreHeight + 1, blockInfo); + + await expectDeterministicAppHash('cfc8c439d00c2afab01594dc699ef71c8b23cff66b0302794d3c6b88c44d687c'); + + // Masternode identity should contain new public key + + await expectMasternodeIdentity( + smlFixture[0], + transaction1, + Address.fromString(smlFixture[0].payoutAddress), + Address.fromString(changedSmlEntry.payoutAddress), + ); + + // Previous voting identity should stay + + await expectVotingIdentity( + smlFixture[0], + transaction1, + ); + + // Previous operator identity should stay + + await expectOperatorIdentity( + smlFixture[0], + undefined, + Address.fromString(changedSmlEntry.operatorPayoutAddress), + ); + + // New operator identity should be created + + await expectOperatorIdentity( + changedSmlEntry, + undefined, + Address.fromString(changedSmlEntry.operatorPayoutAddress), + ); + + // new voting Identity should exist + await expectVotingIdentity( + changedSmlEntry, + transaction1, + ); + + // Only new masternode reward shares should exist + + const changedMasternodeIdentifier = Identifier.from( + Buffer.from(changedSmlEntry.proRegTxHash, 'hex'), + ); + + const documentsResult = await documentRepository.find( + rewardsDataContract, + 'rewardShare', + { + where: [ + ['$ownerId', '==', changedMasternodeIdentifier], + ], + useTransaction: true, + }, + ); + + const documents = documentsResult.getValue(); + + expect(documents).to.have.lengthOf(1); + + const [document] = documents; + + const newOperatorIdentifier = createOperatorIdentifier(changedSmlEntry); + + expect(document.get('payToId')).to.deep.equal(newOperatorIdentifier); + }); + + it('should not create voting Identity if owner and voting keys are the same', async () => { + transaction1 = { + extraPayload: { + operatorReward: 100, + keyIDOwner: Buffer.alloc(20).fill('a').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('a').toString('hex'), + }, + }; + + fetchTransactionMock.withArgs('a2c9b34ef525271d84f70a0d4d2c107e8a2f81cd4d8256dc7b3911ed253d5611').resolves(transaction1); + + // Initial sync + + await synchronizeMasternodeIdentities(coreHeight, blockInfo); + await expectDeterministicAppHash('7a5729e3511c5cc98e8452faa6132f0d600e04fef85df0f95f56e59c776de170'); + const votingIdentifier = createVotingIdentifier(smlFixture[0]); + + const votingIdentityResult = await identityRepository.fetch( + votingIdentifier, + { useTransaction: true }, + ); + + expect(votingIdentityResult.isNull()).to.be.true(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/closeAbciServerFactory.spec.js b/packages/js-drive/test/unit/abci/closeAbciServerFactory.spec.js new file mode 100644 index 00000000000..37f0f6b4d0e --- /dev/null +++ b/packages/js-drive/test/unit/abci/closeAbciServerFactory.spec.js @@ -0,0 +1,29 @@ +const closeAbciServerFactory = require('../../../lib/abci/closeAbciServerFactory'); + +describe('closeAbciServerFactory', () => { + let closeAbciServer; + let abciServerMock; + + beforeEach(function beforeEach() { + abciServerMock = { + close: this.sinon.spy((resolve) => { + resolve(); + }), + listening: true, + }; + + closeAbciServer = closeAbciServerFactory(abciServerMock); + }); + + it('should close server if it\'s listening', async () => { + await closeAbciServer(); + + expect(abciServerMock.close).to.be.calledOnce(); + }); + + it('should not close server if not listening', async () => { + abciServerMock.listening = false; + + expect(abciServerMock.close).to.not.be.called(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/errors/enrichErrorWithContextLoggerFactory.spec.js b/packages/js-drive/test/unit/abci/errors/enrichErrorWithContextLoggerFactory.spec.js new file mode 100644 index 00000000000..d182a4caf3b --- /dev/null +++ b/packages/js-drive/test/unit/abci/errors/enrichErrorWithContextLoggerFactory.spec.js @@ -0,0 +1,38 @@ +const { AsyncLocalStorage } = require('node:async_hooks'); +const enrichErrorWithContextLoggerFactory = require('../../../../lib/abci/errors/enrichErrorWithContextLoggerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); + +describe('enrichErrorWithContextLoggerFactory', () => { + let enrichErrorWithContextLogger; + let loggerMock; + let asyncLocalStorage; + + beforeEach(function beforeEach() { + loggerMock = new LoggerMock(this.sinon); + + asyncLocalStorage = new AsyncLocalStorage(); + + enrichErrorWithContextLogger = enrichErrorWithContextLoggerFactory(asyncLocalStorage); + }); + + it('should add contextLogger from BlockExecutionContext to thrown error', async () => { + const error = new Error('my error'); + + const method = async () => { + asyncLocalStorage.getStore().set('logger', loggerMock); + + throw error; + }; + + const methodHandler = enrichErrorWithContextLogger(method); + + try { + await methodHandler(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.equal(error); + expect(e.contextLogger).to.equal(loggerMock); + } + }); +}); diff --git a/packages/js-drive/test/unit/abci/errors/wrapInErrorHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/errors/wrapInErrorHandlerFactory.spec.js new file mode 100644 index 00000000000..cfda8a6f1d8 --- /dev/null +++ b/packages/js-drive/test/unit/abci/errors/wrapInErrorHandlerFactory.spec.js @@ -0,0 +1,109 @@ +const SomeConsensusError = require('@dashevo/dpp/lib/test/mocks/SomeConsensusError'); +const wrapInErrorHandlerFactory = require('../../../../lib/abci/errors/wrapInErrorHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const InternalAbciError = require('../../../../lib/abci/errors/InternalAbciError'); +const InvalidArgumentAbciError = require('../../../../lib/abci/errors/InvalidArgumentAbciError'); +const VerboseInternalAbciError = require('../../../../lib/abci/errors/VerboseInternalAbciError'); +const DPPValidationAbciError = require('../../../../lib/abci/errors/DPPValidationAbciError'); + +describe('wrapInErrorHandlerFactory', () => { + let loggerMock; + let methodMock; + let request; + let handler; + let wrapInErrorHandler; + + beforeEach(function beforeEach() { + request = { + tx: Buffer.alloc(0), + }; + + loggerMock = new LoggerMock(this.sinon); + + wrapInErrorHandler = wrapInErrorHandlerFactory(loggerMock, true); + methodMock = this.sinon.stub(); + + handler = wrapInErrorHandler( + methodMock, + ); + }); + + it('should respond with internal error code if any Error is thrown in handler and respondWithInternalError enabled', async () => { + handler = wrapInErrorHandler( + methodMock, { respondWithInternalError: true }, + ); + + const error = new Error('Custom error'); + + methodMock.throws(error); + + const response = await handler(request); + + const expectedError = new InternalAbciError(error); + + expect(response).to.deep.equal(expectedError.getAbciResponse()); + }); + + it('should respond with internal error code if an InternalAbciError is thrown in handler and respondWithInternalError enabled', async () => { + handler = wrapInErrorHandler( + methodMock, { respondWithInternalError: true }, + ); + + const data = { sample: 'data' }; + const error = new InternalAbciError(new Error(), data); + + methodMock.throws(error); + + const response = await handler(request); + + expect(response).to.deep.equal(error.getAbciResponse()); + }); + + it('should respond with invalid argument error if it is thrown in handler', async () => { + const data = { sample: 'data' }; + const error = new InvalidArgumentAbciError('test', data); + + methodMock.throws(error); + + const response = await handler(request); + + expect(response).to.deep.equal(error.getAbciResponse()); + }); + + it('should respond with verbose error containing message and stack in debug mode', async () => { + wrapInErrorHandler = wrapInErrorHandlerFactory(loggerMock, false); + + const error = new Error('Custom error'); + + methodMock.throws(error); + + handler = wrapInErrorHandler( + methodMock, { respondWithInternalError: true }, + ); + + const response = await handler(request); + + const expectedError = new VerboseInternalAbciError( + new InternalAbciError(error), + ); + + expect(response).to.deep.equal(expectedError.getAbciResponse()); + }); + + it('should respond with error if method throws DPPValidationAbciError', async () => { + const dppValidationError = new DPPValidationAbciError( + 'Some error', + new SomeConsensusError('Consensus error'), + ); + + methodMock.throws(dppValidationError); + + handler = wrapInErrorHandler( + methodMock, { respondWithInternalError: true }, + ); + + const response = await handler(request); + + expect(response).to.deep.equal(dppValidationError.getAbciResponse()); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/checkTxHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/checkTxHandlerFactory.spec.js new file mode 100644 index 00000000000..fd4a5621278 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/checkTxHandlerFactory.spec.js @@ -0,0 +1,54 @@ +const { + tendermint: { + abci: { + ResponseCheckTx, + }, + }, +} = require('@dashevo/abci/types'); + +const getIdentityCreateTransitionFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityCreateTransitionFixture'); + +const checkTxHandlerFactory = require('../../../../lib/abci/handlers/checkTxHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); + +describe('checkTxHandlerFactory', () => { + let checkTxHandler; + let request; + let stateTransitionFixture; + let unserializeStateTransitionMock; + let loggerMock; + let createContextLoggerMock; + + beforeEach(function beforeEach() { + stateTransitionFixture = getIdentityCreateTransitionFixture(); + + request = { + tx: stateTransitionFixture.toBuffer(), + }; + + unserializeStateTransitionMock = this.sinon.stub() + .resolves(stateTransitionFixture); + + loggerMock = new LoggerMock(this.sinon); + createContextLoggerMock = this.sinon.stub(); + + checkTxHandler = checkTxHandlerFactory( + unserializeStateTransitionMock, + createContextLoggerMock, + loggerMock, + ); + }); + + it('should validate a State Transition and return response', async () => { + const response = await checkTxHandler(request); + + expect(response).to.be.an.instanceOf(ResponseCheckTx); + expect(response.code).to.equal(0); + + expect(unserializeStateTransitionMock).to.be.calledOnceWith(request.tx); + + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + abciMethod: 'checkTx', + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/extendVoteHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/extendVoteHandlerFactory.spec.js new file mode 100644 index 00000000000..3d646e40a31 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/extendVoteHandlerFactory.spec.js @@ -0,0 +1,68 @@ +const { + tendermint: { + abci: { + ResponseExtendVote, + }, + }, +} = require('@dashevo/abci/types'); + +const { hash } = require('@dashevo/dpp/lib/util/hash'); + +const extendVoteHandlerFactory = require('../../../../lib/abci/handlers/extendVoteHandlerFactory'); + +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); + +describe('extendVoteHandlerFactory', () => { + let extendVoteHandler; + let blockExecutionContextMock; + let createContextLoggerMock; + let loggerMock; + + beforeEach(function beforeEach() { + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + loggerMock = new LoggerMock(this.sinon); + + blockExecutionContextMock.getContextLogger.returns(loggerMock); + + blockExecutionContextMock.getWithdrawalTransactionsMap.returns({}); + + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + extendVoteHandler = extendVoteHandlerFactory( + blockExecutionContextMock, + createContextLoggerMock, + ); + }); + + it('should return ResponseExtendVote with vote extensions if withdrawal transactions are present', async () => { + const [txOneBytes, txTwoBytes] = [ + Buffer.alloc(32, 0), + Buffer.alloc(32, 1), + ]; + + blockExecutionContextMock.getWithdrawalTransactionsMap.returns({ + [hash(txOneBytes).toString('hex')]: txOneBytes, + [hash(txTwoBytes).toString('hex')]: txTwoBytes, + }); + + const result = await extendVoteHandler(); + + expect(result).to.be.an.instanceOf(ResponseExtendVote); + expect(result.voteExtensions).to.deep.equal([ + { + type: 1, + extension: hash(txOneBytes), + }, + { + type: 1, + extension: hash(txTwoBytes), + }, + ]); + + expect(createContextLoggerMock).to.be.calledOnceWith(loggerMock, { + abciMethod: 'extendVote', + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/finalizeBlockHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/finalizeBlockHandlerFactory.spec.js new file mode 100644 index 00000000000..fe254bd9555 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/finalizeBlockHandlerFactory.spec.js @@ -0,0 +1,217 @@ +const { + tendermint: { + abci: { + ResponseFinalizeBlock, + RequestProcessProposal, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const { hash } = require('@dashevo/dpp/lib/util/hash'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const finalizeBlockHandlerFactory = require('../../../../lib/abci/handlers/finalizeBlockHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); +const GroveDBStoreMock = require('../../../../lib/test/mock/GroveDBStoreMock'); +const BlockExecutionContextRepositoryMock = require('../../../../lib/test/mock/BlockExecutionContextRepositoryMock'); + +describe('finalizeBlockHandlerFactory', () => { + let finalizeBlockHandler; + let executionTimerMock; + let latestBlockExecutionContextMock; + let loggerMock; + let requestMock; + let appHash; + let groveDBStoreMock; + let blockExecutionContextRepositoryMock; + let dataContract; + let proposalBlockExecutionContextMock; + let round; + let block; + let processProposalMock; + let broadcastWithdrawalTransactions; + let createContextLoggerMock; + + beforeEach(function beforeEach() { + round = 0; + appHash = Buffer.alloc(0); + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + latestBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + loggerMock = new LoggerMock(this.sinon); + executionTimerMock = { + clearTimer: this.sinon.stub(), + startTimer: this.sinon.stub(), + stopTimer: this.sinon.stub(), + }; + + const commit = {}; + + const height = new Long(42); + + const time = { + seconds: Math.ceil(new Date().getTime() / 1000), + }; + + const coreChainLockedHeight = 10; + + block = { + header: { + time, + version: { + app: Long.fromInt(1), + }, + proposerProTxHash: Uint8Array.from([1, 2, 3, 4]), + coreChainLockedHeight, + }, + data: { + txs: new Array(3).fill(Buffer.alloc(5, 0)), + }, + }; + + requestMock = { + commit, + height, + time, + coreChainLockedHeight, + round, + block, + }; + + dataContract = getDataContractFixture(); + + proposalBlockExecutionContextMock.getHeight.returns(new Long(42)); + proposalBlockExecutionContextMock.getRound.returns(round); + proposalBlockExecutionContextMock.getDataContracts.returns([dataContract]); + proposalBlockExecutionContextMock.getEpochInfo.returns({ + currentEpochIndex: 1, + }); + proposalBlockExecutionContextMock.getTimeMs.returns((new Date()).getTime()); + + groveDBStoreMock = new GroveDBStoreMock(this.sinon); + groveDBStoreMock.getRootHash.resolves(appHash); + + blockExecutionContextRepositoryMock = new BlockExecutionContextRepositoryMock( + this.sinon, + ); + + processProposalMock = this.sinon.stub(); + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + broadcastWithdrawalTransactions = this.sinon.stub(); + + finalizeBlockHandler = finalizeBlockHandlerFactory( + groveDBStoreMock, + blockExecutionContextRepositoryMock, + loggerMock, + executionTimerMock, + latestBlockExecutionContextMock, + proposalBlockExecutionContextMock, + processProposalMock, + broadcastWithdrawalTransactions, + createContextLoggerMock, + ); + }); + + it('should commit db transactions, create document dbs and return ResponseFinalizeBlock', async () => { + const result = await finalizeBlockHandler(requestMock); + + expect(result).to.be.an.instanceOf(ResponseFinalizeBlock); + + expect(executionTimerMock.stopTimer).to.be.calledOnceWithExactly('blockExecution'); + + expect(proposalBlockExecutionContextMock.reset).to.be.calledOnce(); + + expect(blockExecutionContextRepositoryMock.store).to.be.calledOnceWithExactly( + proposalBlockExecutionContextMock, + { + useTransaction: true, + }, + ); + + expect(groveDBStoreMock.commitTransaction).to.be.calledOnceWithExactly(); + + expect(latestBlockExecutionContextMock.populate).to.be.calledOnce(); + expect(processProposalMock).to.be.not.called(); + + expect(broadcastWithdrawalTransactions).to.have.been.calledOnceWith( + proposalBlockExecutionContextMock, + undefined, + undefined, + ); + + expect(createContextLoggerMock).to.be.calledOnceWithExactly( + loggerMock, { + height: '42', + round, + abciMethod: 'finalizeBlock', + }, + ); + }); + + it('should send withdrawal transaction if vote extensions are present', async () => { + const [txOneBytes, txTwoBytes] = [ + Buffer.alloc(32, 0), + Buffer.alloc(32, 1), + ]; + + proposalBlockExecutionContextMock.getWithdrawalTransactionsMap.returns({ + [hash(txOneBytes).toString('hex')]: txOneBytes, + [hash(txTwoBytes).toString('hex')]: txTwoBytes, + }); + + const thresholdVoteExtensions = [ + { + extension: hash(txOneBytes), + signature: Buffer.alloc(96, 3), + }, + { + extension: hash(txTwoBytes), + signature: Buffer.alloc(96, 4), + }, + ]; + + requestMock.commit = { thresholdVoteExtensions }; + + await finalizeBlockHandler(requestMock); + + expect(processProposalMock).to.be.not.called(); + expect(createContextLoggerMock).to.be.calledOnceWithExactly( + loggerMock, { + height: '42', + round, + abciMethod: 'finalizeBlock', + }, + ); + }); + + it('should call processProposal if round is not equal to execution context', async () => { + proposalBlockExecutionContextMock.getRound.returns(round + 1); + + const result = await finalizeBlockHandler(requestMock); + + expect(result).to.be.an.instanceOf(ResponseFinalizeBlock); + + const processProposalRequest = new RequestProcessProposal({ + height: requestMock.height, + txs: block.data.txs, + coreChainLockedHeight: block.header.coreChainLockedHeight, + version: block.header.version, + proposedLastCommit: requestMock.commit, + time: block.header.time, + proposerProTxHash: block.header.proposerProTxHash, + round, + }); + + expect(processProposalMock).to.be.calledOnceWithExactly(processProposalRequest, loggerMock); + expect(createContextLoggerMock).to.be.calledOnceWithExactly( + loggerMock, { + height: '42', + round, + abciMethod: 'finalizeBlock', + }, + ); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/infoHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/infoHandlerFactory.spec.js new file mode 100644 index 00000000000..c97411142b7 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/infoHandlerFactory.spec.js @@ -0,0 +1,129 @@ +const Long = require('long'); + +const { + tendermint: { + abci: { + ResponseInfo, + }, + }, +} = require('@dashevo/abci/types'); + +const infoHandlerFactory = require('../../../../lib/abci/handlers/infoHandlerFactory'); + +const packageJson = require('../../../../package.json'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); + +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); +const GroveDBStoreMock = require('../../../../lib/test/mock/GroveDBStoreMock'); +const BlockExecutionContextRepositoryMock = require('../../../../lib/test/mock/BlockExecutionContextRepositoryMock'); + +describe('infoHandlerFactory', () => { + let protocolVersion; + let lastBlockHeight; + let lastBlockAppHash; + let infoHandler; + let updateSimplifiedMasternodeListMock; + let lastCoreChainLockedHeight; + let loggerMock; + let blockExecutionContextMock; + let blockExecutionContextRepositoryMock; + let groveDBStoreMock; + let createContextLoggerMock; + + beforeEach(function beforeEach() { + lastBlockHeight = Long.fromInt(0); + lastBlockAppHash = Buffer.alloc(0); + protocolVersion = Long.fromInt(1); + lastCoreChainLockedHeight = 0; + + updateSimplifiedMasternodeListMock = this.sinon.stub(); + + loggerMock = new LoggerMock(this.sinon); + + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + blockExecutionContextMock.getHeight.returns(lastBlockHeight); + blockExecutionContextMock.getCoreChainLockedHeight.returns(lastCoreChainLockedHeight); + blockExecutionContextRepositoryMock = new BlockExecutionContextRepositoryMock( + this.sinon, + ); + groveDBStoreMock = new GroveDBStoreMock(this.sinon); + + blockExecutionContextRepositoryMock.fetch.resolves(blockExecutionContextMock); + + groveDBStoreMock.getRootHash.resolves(lastBlockAppHash); + + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + infoHandler = infoHandlerFactory( + blockExecutionContextMock, + blockExecutionContextRepositoryMock, + protocolVersion, + updateSimplifiedMasternodeListMock, + loggerMock, + groveDBStoreMock, + createContextLoggerMock, + ); + }); + + it('should return respond with genesis heights and app hash on the first run', async () => { + blockExecutionContextRepositoryMock.fetch.resolves(blockExecutionContextMock); + blockExecutionContextMock.getHeight.returns(null); + blockExecutionContextMock.isEmpty.returns(true); + + const response = await infoHandler(); + + expect(response).to.be.an.instanceOf(ResponseInfo); + + expect(response).to.deep.include({ + version: packageJson.version, + appVersion: protocolVersion, + lastBlockHeight, + lastBlockAppHash, + }); + + expect(blockExecutionContextRepositoryMock.fetch).to.be.calledOnce(); + expect(blockExecutionContextMock.populate).to.not.be.called(); + expect(blockExecutionContextMock.getHeight).to.not.be.called(); + expect(blockExecutionContextMock.getCoreChainLockedHeight).to.not.be.called(); + expect(updateSimplifiedMasternodeListMock).to.not.be.called(); + expect(groveDBStoreMock.getRootHash).to.be.calledOnce(); + expect(createContextLoggerMock).to.be.calledOnceWithExactly( + loggerMock, { + abciMethod: 'info', + }, + ); + }); + + it('should populate context and update SML on subsequent runs', async () => { + const response = await infoHandler(); + + expect(response).to.be.an.instanceOf(ResponseInfo); + + expect(ResponseInfo.toObject(response)).to.deep.equal({ + version: packageJson.version, + appVersion: protocolVersion, + lastBlockHeight, + lastBlockAppHash, + }); + + expect(blockExecutionContextMock.getHeight).to.be.calledOnce(); + + expect(updateSimplifiedMasternodeListMock).to.be.calledOnceWithExactly( + lastCoreChainLockedHeight, + { + logger: loggerMock, + }, + ); + expect(createContextLoggerMock).to.be.calledTwice(); + expect(createContextLoggerMock.getCall(0)).to.be.calledWithExactly( + loggerMock, { + abciMethod: 'info', + }, + ); + expect(createContextLoggerMock.getCall(1)).to.be.calledWithExactly( + loggerMock, { + height: lastBlockHeight.toString(), + }, + ); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/initChainHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/initChainHandlerFactory.spec.js new file mode 100644 index 00000000000..99c3807aea8 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/initChainHandlerFactory.spec.js @@ -0,0 +1,164 @@ +const Long = require('long'); + +const { + tendermint: { + abci: { + ResponseInitChain, + ValidatorSetUpdate, + }, + types: { + CoreChainLock, + }, + }, +} = require('@dashevo/abci/types'); + +const initChainHandlerFactory = require('../../../../lib/abci/handlers/initChainHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const GroveDBStoreMock = require('../../../../lib/test/mock/GroveDBStoreMock'); +const millisToProtoTimestamp = require('../../../../lib/util/millisToProtoTimestamp'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); +const getSystemIdentityPublicKeysFixture = require('../../../../lib/test/fixtures/getSystemIdentityPublicKeysFixture'); +const protoTimestampToMillis = require('../../../../lib/util/protoTimestampToMillis'); + +describe('initChainHandlerFactory', () => { + let initChainHandler; + let updateSimplifiedMasternodeListMock; + let initialCoreChainLockedHeight; + let validatorSetMock; + let createValidatorSetUpdateMock; + let loggerMock; + let validatorSetUpdate; + let synchronizeMasternodeIdentitiesMock; + let groveDBStoreMock; + let appHashFixture; + let rsAbciMock; + let createCoreChainLockUpdateMock; + let coreChainLockUpdate; + let createContextLoggerMock; + let genesisTimeMs; + let systemIdentityPublicKeysFixture; + + beforeEach(function beforeEach() { + initialCoreChainLockedHeight = 1; + + appHashFixture = Buffer.alloc(0); + + genesisTimeMs = Date.now(); + + updateSimplifiedMasternodeListMock = this.sinon.stub(); + + const quorumHash = Buffer.alloc(64).fill(1).toString('hex'); + validatorSetMock = { + initialize: this.sinon.stub(), + getQuorum: this.sinon.stub().returns({ + quorumHash, + }), + }; + + validatorSetUpdate = new ValidatorSetUpdate(); + + createValidatorSetUpdateMock = this.sinon.stub().returns(validatorSetUpdate); + synchronizeMasternodeIdentitiesMock = this.sinon.stub().resolves({ + createdEntities: [], + updatedEntities: [], + removedEntities: [], + fromHeight: 1, + toHeight: 42, + }); + + loggerMock = new LoggerMock(this.sinon); + + rsAbciMock = { + initChain: this.sinon.stub(), + }; + + groveDBStoreMock = new GroveDBStoreMock(this.sinon); + groveDBStoreMock.getRootHash.resolves(appHashFixture); + + coreChainLockUpdate = new CoreChainLock({ + coreBlockHeight: 42, + coreBlockHash: '1528e523f4c20fa84ba70dd96372d34e00ce260f357d53ad1a8bc892ebf20e2d', + signature: '1897ce8f54d2070f44ca5c29983b68b391e8137c25e44f67416e579f3e3bdfef7b4fd22db7818399147e52907998857b0fbc8edfdc40a64f2c7df0e88544d31d12ca8c15e73d50dda25ca23f754ed3f789ed4bcb392161995f464017c10df404', + }); + + createCoreChainLockUpdateMock = this.sinon.stub().resolves(coreChainLockUpdate); + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + systemIdentityPublicKeysFixture = getSystemIdentityPublicKeysFixture(); + + initChainHandler = initChainHandlerFactory( + updateSimplifiedMasternodeListMock, + initialCoreChainLockedHeight, + validatorSetMock, + createValidatorSetUpdateMock, + synchronizeMasternodeIdentitiesMock, + loggerMock, + groveDBStoreMock, + rsAbciMock, + createCoreChainLockUpdateMock, + createContextLoggerMock, + systemIdentityPublicKeysFixture, + ); + }); + + it('should initialize the chain', async () => { + const request = { + initialHeight: Long.fromInt(1), + chainId: 'test', + time: millisToProtoTimestamp(genesisTimeMs), + }; + + const blockInfo = new BlockInfo(0, 0, protoTimestampToMillis(request.time)); + + const response = await initChainHandler(request); + + expect(response).to.be.an.instanceOf(ResponseInitChain); + expect(response.validatorSetUpdate).to.be.equal(validatorSetUpdate); + expect(response.initialCoreHeight).to.be.equal(initialCoreChainLockedHeight); + expect(response.appHash).to.deep.equal(appHashFixture); + + // Update SML + + expect(updateSimplifiedMasternodeListMock).to.be.calledOnceWithExactly( + initialCoreChainLockedHeight, + { + logger: loggerMock, + }, + ); + + // Create initial state + + expect(groveDBStoreMock.startTransaction).to.be.calledOnce(); + + expect(rsAbciMock.initChain).to.be.calledOnceWithExactly({ + genesisTimeMs: protoTimestampToMillis(request.time), + systemIdentityPublicKeys: systemIdentityPublicKeysFixture, + }, true); + + expect(synchronizeMasternodeIdentitiesMock).to.be.calledOnceWithExactly( + initialCoreChainLockedHeight, + blockInfo, + ); + + expect(groveDBStoreMock.commitTransaction).to.be.calledOnce(); + + expect(groveDBStoreMock.getRootHash).to.be.calledOnce(); + + // Initialize VS + + expect(validatorSetMock.initialize).to.be.calledOnceWithExactly( + initialCoreChainLockedHeight, + ); + + expect(validatorSetMock.getQuorum).to.be.calledOnce(); + + expect(createValidatorSetUpdateMock).to.be.calledOnceWithExactly(validatorSetMock); + + expect(createCoreChainLockUpdateMock) + .to.be.calledOnceWithExactly(initialCoreChainLockedHeight, 0, loggerMock); + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + height: request.initialHeight.toString(), + abciMethod: 'initChain', + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/prepareProposalHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/prepareProposalHandlerFactory.spec.js new file mode 100644 index 00000000000..61b9a4bf56a --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/prepareProposalHandlerFactory.spec.js @@ -0,0 +1,232 @@ +const { + tendermint: { + abci: { + ResponsePrepareProposal, + ValidatorSetUpdate, + }, + types: { + ConsensusParams, + CoreChainLock, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const prepareProposalHandlerFactory = require('../../../../lib/abci/handlers/prepareProposalHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); + +describe('prepareProposalHandlerFactory', () => { + let prepareProposalHandler; + let request; + let deliverTxMock; + let loggerMock; + let beginBlockMock; + let endBlockMock; + let updateCoreChainLockMock; + let appHash; + let consensusParamUpdates; + let validatorSetUpdate; + let coreChainLockUpdate; + let endBlockResult; + let proposalBlockExecutionContextMock; + let round; + let executionTimerMock; + let createContextLoggerMock; + let quorumHash; + + beforeEach(function beforeEach() { + round = 1; + appHash = Buffer.alloc(1, 1); + coreChainLockUpdate = new CoreChainLock({ + coreBlockHeight: 42, + coreBlockHash: '1528e523f4c20fa84ba70dd96372d34e00ce260f357d53ad1a8bc892ebf20e2d', + signature: '1897ce8f54d2070f44ca5c29983b68b391e8137c25e44f67416e579f3e3bdfef7b4fd22db7818399147e52907998857b0fbc8edfdc40a64f2c7df0e88544d31d12ca8c15e73d50dda25ca23f754ed3f789ed4bcb392161995f464017c10df404', + }); + + consensusParamUpdates = new ConsensusParams({ + block: { + maxBytes: 1, + maxGas: 2, + }, + evidence: { + maxAgeDuration: null, + maxAgeNumBlocks: 1, + maxBytes: 2, + }, + version: { + appVersion: 1, + }, + }); + + validatorSetUpdate = new ValidatorSetUpdate(); + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + loggerMock = new LoggerMock(this.sinon); + + endBlockResult = { + consensusParamUpdates, + appHash, + validatorSetUpdate, + }; + + beginBlockMock = this.sinon.stub(); + + deliverTxMock = this.sinon.stub().resolves({ + code: 0, + fees: { + processingFee: 10, + storageFee: 100, + refundsPerEpoch: { + 1: 15, + }, + }, + }); + + endBlockMock = this.sinon.stub().resolves( + endBlockResult, + ); + + updateCoreChainLockMock = this.sinon.stub().resolves(coreChainLockUpdate); + + executionTimerMock = { + getTimer: this.sinon.stub().returns(0.1), + }; + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + prepareProposalHandler = prepareProposalHandlerFactory( + deliverTxMock, + loggerMock, + proposalBlockExecutionContextMock, + beginBlockMock, + endBlockMock, + updateCoreChainLockMock, + executionTimerMock, + createContextLoggerMock, + ); + + const maxTxBytes = 42; + const txs = new Array(3).fill(Buffer.alloc(5, 0)); + + const height = new Long(42); + + const time = { + seconds: Math.ceil(new Date().getTime() / 1000), + }; + + const version = { + app: Long.fromInt(1), + }; + + const proposerProTxHash = Uint8Array.from([1, 2, 3, 4]); + + const proposedAppVersion = Long.fromInt(1); + + const coreChainLockedHeight = 10; + + const localLastCommit = {}; + + quorumHash = Buffer.alloc(32, 0); + + request = { + height, + maxTxBytes, + txs, + coreChainLockedHeight, + version, + localLastCommit, + time, + proposerProTxHash, + proposedAppVersion, + round, + quorumHash, + }; + }); + + it('should return proposal', async () => { + const result = await prepareProposalHandler(request); + + expect(result).to.be.an.instanceOf(ResponsePrepareProposal); + + const txRecords = request.txs.map((tx) => ({ + tx, + action: 1, + })); + + expect(result).to.be.an.instanceOf(ResponsePrepareProposal); + expect(result.appHash).to.be.equal(appHash); + expect(result.txResults).to.be.deep.equal(new Array(3).fill({ code: 0 })); + expect(result.consensusParamUpdates).to.be.equal(consensusParamUpdates); + expect(result.validatorSetUpdate).to.be.equal(validatorSetUpdate); + expect(result.coreChainLockUpdate).to.be.equal(coreChainLockUpdate); + expect(result.txRecords).to.be.deep.equal(txRecords); + + expect(beginBlockMock).to.be.calledOnceWithExactly( + { + lastCommitInfo: request.localLastCommit, + height: request.height, + coreChainLockedHeight: request.coreChainLockedHeight, + version: request.version, + time: request.time, + proposerProTxHash: Buffer.from(request.proposerProTxHash), + proposedAppVersion: request.proposedAppVersion, + round, + quorumHash, + }, + loggerMock, + ); + + expect(deliverTxMock).to.be.calledThrice(); + + expect(updateCoreChainLockMock).to.be.calledOnceWithExactly( + request.coreChainLockedHeight, + round, + loggerMock, + ); + + expect(endBlockMock).to.be.calledOnceWithExactly( + { + height: request.height, + round, + fees: { + processingFee: 10 * 3, + storageFee: 100 * 3, + refundsPerEpoch: { + 1: 15 * 3, + }, + }, + coreChainLockedHeight: request.coreChainLockedHeight, + }, + loggerMock, + ); + + expect(proposalBlockExecutionContextMock.setPrepareProposalResult).to.be.calledOnceWithExactly({ + appHash, + txResults: new Array(3).fill({ code: 0 }), + consensusParamUpdates, + validatorSetUpdate, + }); + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + height: '42', + round, + abciMethod: 'prepareProposal', + }); + }); + + it('should cut txs that are not fit into the size limit', async () => { + request.maxTxBytes = 9; + + const result = await prepareProposalHandler(request); + + expect(result).to.be.an.instanceOf(ResponsePrepareProposal); + expect(result.txRecords).to.have.lengthOf(1); + expect(result.txRecords).to.deep.equal( + [{ + tx: request.txs[0], + action: 1, + }], + ); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/processProposalHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/processProposalHandlerFactory.spec.js new file mode 100644 index 00000000000..ccb4b0deb5f --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/processProposalHandlerFactory.spec.js @@ -0,0 +1,160 @@ +const { + tendermint: { + abci: { + ResponseProcessProposal, + ValidatorSetUpdate, + }, + types: { + ConsensusParams, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const processProposalHandlerFactory = require('../../../../lib/abci/handlers/processProposalHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); + +describe('processProposalHandlerFactory', () => { + let processProposalHandler; + let request; + let loggerMock; + let verifyChainLockMock; + let coreChainLockUpdate; + let processProposalMock; + let round; + let appHash; + let proposalBlockExecutionContextMock; + let consensusParamUpdates; + let validatorSetUpdate; + let createContextLoggerMock; + + beforeEach(function beforeEach() { + round = 0; + + appHash = Buffer.alloc(1, 1); + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + consensusParamUpdates = new ConsensusParams({ + block: { + maxBytes: 1, + maxGas: 2, + }, + evidence: { + maxAgeDuration: null, + maxAgeNumBlocks: 1, + maxBytes: 2, + }, + version: { + appVersion: 1, + }, + }); + validatorSetUpdate = new ValidatorSetUpdate(); + + loggerMock = new LoggerMock(this.sinon); + + verifyChainLockMock = this.sinon.stub().resolves(true); + + processProposalMock = this.sinon.stub().resolves(new ResponseProcessProposal({ status: 1 })); + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + processProposalHandler = processProposalHandlerFactory( + loggerMock, + verifyChainLockMock, + processProposalMock, + proposalBlockExecutionContextMock, + createContextLoggerMock, + ); + + const txs = new Array(3).fill(Buffer.alloc(5, 0)); + + const height = new Long(42); + + const time = { + seconds: Math.ceil(new Date().getTime() / 1000), + }; + const version = { + app: Long.fromInt(1), + }; + const proposerProTxHash = Uint8Array.from([1, 2, 3, 4]); + const coreChainLockedHeight = 10; + const proposedLastCommit = {}; + + coreChainLockUpdate = { + coreBlockHeight: 42, + coreBlockHash: '1528e523f4c20fa84ba70dd96372d34e00ce260f357d53ad1a8bc892ebf20e2d', + signature: '1897ce8f54d2070f44ca5c29983b68b391e8137c25e44f67416e579f3e3bdfef7b4fd22db7818399147e52907998857b0fbc8edfdc40a64f2c7df0e88544d31d12ca8c15e73d50dda25ca23f754ed3f789ed4bcb392161995f464017c10df404', + }; + + request = { + round, + height, + txs, + coreChainLockedHeight, + version, + proposedLastCommit, + time, + proposerProTxHash, + coreChainLockUpdate, + }; + }); + + it('should return ResponseProcessProposal', async () => { + const result = await processProposalHandler(request); + + expect(result).to.be.an.instanceOf(ResponseProcessProposal); + + expect(result.status).to.equal(1); + + expect(processProposalMock).to.be.calledOnceWithExactly(request, loggerMock); + + expect(verifyChainLockMock).to.be.calledOnceWithExactly( + coreChainLockUpdate, + ); + }); + + it('should return rejected ResponseProcessProposal if chainlock can\'t be verified', async () => { + verifyChainLockMock.resolves(false); + + const result = await processProposalHandler(request); + + expect(result).to.be.an.instanceOf(ResponseProcessProposal); + expect(result.status).to.equal(2); + + expect(processProposalMock).to.not.be.called(); + }); + + it('should return already prepared result for this height and round', async () => { + proposalBlockExecutionContextMock.getHeight.returns(request.height); + proposalBlockExecutionContextMock.getRound.returns(request.round); + + proposalBlockExecutionContextMock.getPrepareProposalResult.returns({ + appHash, + txResults: new Array(3).fill({ code: 0 }), + consensusParamUpdates, + validatorSetUpdate, + }); + + const result = await processProposalHandler(request); + + expect(proposalBlockExecutionContextMock.getPrepareProposalResult).to.be.calledOnce(); + + expect(result).to.be.an.instanceOf(ResponseProcessProposal); + expect(result.status).to.equal(1); + expect(result.appHash).to.equal(appHash); + expect(result.txResults).to.be.deep.equal(new Array(3).fill({ code: 0 })); + expect(result.consensusParamUpdates).to.be.equal(consensusParamUpdates); + expect(result.validatorSetUpdate).to.be.equal(validatorSetUpdate); + + expect(processProposalMock).to.not.be.called(); + + expect(verifyChainLockMock).to.not.be.called(); + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + height: '42', + round, + abciMethod: 'processProposal', + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/beginBlockFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/beginBlockFactory.spec.js new file mode 100644 index 00000000000..c91301cc649 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/beginBlockFactory.spec.js @@ -0,0 +1,278 @@ +const Long = require('long'); +const { + tendermint: { + version: { + Consensus, + }, + }, +} = require('@dashevo/abci/types'); + +const { hash } = require('@dashevo/dpp/lib/util/hash'); + +const beginBlockFactory = require('../../../../../lib/abci/handlers/proposal/beginBlockFactory'); + +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const NotSupportedNetworkProtocolVersionError = require('../../../../../lib/abci/handlers/errors/NotSupportedNetworkProtocolVersionError'); +const NetworkProtocolVersionIsNotSetError = require('../../../../../lib/abci/handlers/errors/NetworkProtocolVersionIsNotSetError'); +const GroveDBStoreMock = require('../../../../../lib/test/mock/GroveDBStoreMock'); +const millisToProtoTimestamp = require('../../../../../lib/util/millisToProtoTimestamp'); +const protoTimestampToMillis = require('../../../../../lib/util/protoTimestampToMillis'); +const BlockInfo = require('../../../../../lib/blockExecution/BlockInfo'); + +describe('beginBlockFactory', () => { + let protocolVersion; + let beginBlock; + let request; + let blockHeight; + let coreChainLockedHeight; + let updateSimplifiedMasternodeListMock; + let waitForChainLockedHeightMock; + let loggerMock; + let lastCommitInfo; + let dppMock; + let transactionalDppMock; + let synchronizeMasternodeIdentitiesMock; + let groveDBStoreMock; + let version; + let rsAbciMock; + let proposerProTxHash; + let proposedAppVersion; + let round; + let executionTimerMock; + let latestBlockExecutionContextMock; + let proposalBlockExecutionContextMock; + let rsResponseMock; + let blockInfo; + let timeMs; + let epochInfo; + let time; + let lastSyncedCoreHeightRepositoryMock; + let simplifyMasternodeListMock; + let validMasternodesListLength; + + beforeEach(function beforeEach() { + round = 0; + protocolVersion = Long.fromInt(1); + proposedAppVersion = Long.fromInt(1); + blockHeight = Long.fromNumber(1); + time = millisToProtoTimestamp(Date.now()); + timeMs = protoTimestampToMillis(time); + epochInfo = { + currentEpochIndex: 1, + isEpochChange: false, + }; + blockInfo = new BlockInfo( + blockHeight.toNumber(), + epochInfo.currentEpochIndex, + timeMs, + ); + + latestBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + proposalBlockExecutionContextMock.isEmpty.returns(true); + proposalBlockExecutionContextMock.getHeight.returns(blockHeight); + proposalBlockExecutionContextMock.getEpochInfo.returns(epochInfo); + proposalBlockExecutionContextMock.getTimeMs.returns(timeMs); + + loggerMock = new LoggerMock(this.sinon); + + dppMock = { + setProtocolVersion: this.sinon.stub(), + }; + transactionalDppMock = { + setProtocolVersion: this.sinon.stub(), + }; + + executionTimerMock = { + clearTimer: this.sinon.stub(), + startTimer: this.sinon.stub(), + stopTimer: this.sinon.stub(), + }; + + updateSimplifiedMasternodeListMock = this.sinon.stub().resolves(false); + waitForChainLockedHeightMock = this.sinon.stub(); + synchronizeMasternodeIdentitiesMock = this.sinon.stub().resolves({ + createdEntities: [], + updatedEntities: [], + removedEntities: [], + fromHeight: 1, + toHeight: 42, + }); + + groveDBStoreMock = new GroveDBStoreMock(this.sinon); + + rsResponseMock = { + epochInfo, + }; + + rsAbciMock = { + blockBegin: this.sinon.stub().resolves(rsResponseMock), + }; + + lastSyncedCoreHeightRepositoryMock = { + fetch: this.sinon.stub().resolves({ + getValue: () => undefined, + }), + }; + + validMasternodesListLength = 400; + + simplifyMasternodeListMock = { + getStore() { + return { + getCurrentSML() { + return { + getValidMasternodesList() { + return validMasternodesListLength; + }, + }; + }, + }; + }, + }; + + beginBlock = beginBlockFactory( + groveDBStoreMock, + latestBlockExecutionContextMock, + proposalBlockExecutionContextMock, + protocolVersion, + dppMock, + transactionalDppMock, + updateSimplifiedMasternodeListMock, + waitForChainLockedHeightMock, + synchronizeMasternodeIdentitiesMock, + rsAbciMock, + executionTimerMock, + lastSyncedCoreHeightRepositoryMock, + simplifyMasternodeListMock, + ); + + lastCommitInfo = {}; + + version = Consensus.fromObject({ + app: protocolVersion, + }); + + proposerProTxHash = Buffer.alloc(32, 1); + + request = { + height: blockHeight, + lastCommitInfo, + coreChainLockedHeight, + version, + time: millisToProtoTimestamp(timeMs), + proposerProTxHash, + proposedAppVersion, + round, + }; + }); + + it('should reset previous block state and prepare everything for for a next one', async () => { + await beginBlock(request, loggerMock); + + // Wait for chain locked core block height + expect(waitForChainLockedHeightMock).to.be.calledOnceWithExactly(coreChainLockedHeight); + + // Set current protocol version + expect(dppMock.setProtocolVersion).to.have.been.calledOnceWithExactly( + protocolVersion.toNumber(), + ); + expect(transactionalDppMock.setProtocolVersion).to.have.been.calledOnceWithExactly( + protocolVersion.toNumber(), + ); + + // Start new transaction + expect(groveDBStoreMock.startTransaction).to.be.calledOnceWithExactly(); + + // Update SML + expect(updateSimplifiedMasternodeListMock).to.be.calledOnceWithExactly( + coreChainLockedHeight, { logger: loggerMock }, + ); + + expect(synchronizeMasternodeIdentitiesMock).to.not.been.called(); + + expect(executionTimerMock.clearTimer).to.be.calledTwice(); + expect(executionTimerMock.clearTimer.getCall(1)).to.be.calledWithExactly('roundExecution'); + expect(executionTimerMock.clearTimer.getCall(0)).to.be.calledWithExactly('blockExecution'); + + expect(executionTimerMock.startTimer).to.be.calledTwice(); + expect(executionTimerMock.startTimer.getCall(1)).to.be.calledWithExactly('roundExecution'); + expect(executionTimerMock.startTimer.getCall(0)).to.be.calledWithExactly('blockExecution'); + + expect(proposalBlockExecutionContextMock.setContextLogger) + .to.be.calledOnceWithExactly(loggerMock); + expect(proposalBlockExecutionContextMock.setHeight) + .to.be.calledOnceWithExactly(blockHeight); + expect(proposalBlockExecutionContextMock.setVersion) + .to.be.calledOnceWithExactly(version); + expect(proposalBlockExecutionContextMock.setProposedAppVersion) + .to.be.calledOnceWithExactly(proposedAppVersion); + expect(proposalBlockExecutionContextMock.setTimeMs) + .to.be.calledOnceWithExactly(timeMs); + expect(proposalBlockExecutionContextMock.setCoreChainLockedHeight) + .to.be.calledOnceWithExactly(coreChainLockedHeight); + expect(proposalBlockExecutionContextMock.setLastCommitInfo) + .to.be.calledOnceWithExactly(lastCommitInfo); + expect(proposalBlockExecutionContextMock.setEpochInfo) + .to.be.calledOnceWithExactly(epochInfo); + }); + + it('should synchronize masternode identities if SML is updated', async () => { + updateSimplifiedMasternodeListMock.resolves(true); + + await beginBlock(request, loggerMock); + + expect(synchronizeMasternodeIdentitiesMock).to.have.been.calledOnceWithExactly( + coreChainLockedHeight, + blockInfo, + ); + }); + + it('should throw NotSupportedNetworkProtocolVersionError if protocol version is not supported', async () => { + request.version.app = Long.fromInt(42); + + try { + await beginBlock(request, loggerMock); + + expect.fail('should throw NotSupportedNetworkProtocolVersionError'); + } catch (e) { + expect(e).to.be.instanceOf(NotSupportedNetworkProtocolVersionError); + expect(e.getNetworkProtocolVersion()).to.equal(request.version.app); + expect(e.getLatestProtocolVersion()).to.equal(protocolVersion); + } + }); + + it('should throw an NetworkProtocolVersionIsNotSetError if network protocol version is not set', async () => { + request.version.app = Long.fromInt(0); + + try { + await beginBlock(request, loggerMock); + + expect.fail('should throw NetworkProtocolVersionIsNotSetError'); + } catch (err) { + expect(err).to.be.an.instanceOf(NetworkProtocolVersionIsNotSetError); + } + }); + + it('should set withdrawal transactions map if present', async () => { + const [txOneBytes, txTwoBytes] = [ + Buffer.alloc(32, 0), + Buffer.alloc(32, 1), + ]; + + rsAbciMock.blockBegin.resolves({ + unsignedWithdrawalTransactions: [txOneBytes, txTwoBytes], + }); + + await beginBlock(request, loggerMock); + + expect( + proposalBlockExecutionContextMock.setWithdrawalTransactionsMap, + ).to.have.been.calledOnceWithExactly({ + [hash(txOneBytes).toString('hex')]: txOneBytes, + [hash(txTwoBytes).toString('hex')]: txTwoBytes, + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.spec.js new file mode 100644 index 00000000000..11e775a082c --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory.spec.js @@ -0,0 +1,69 @@ +const Long = require('long'); + +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); + +const broadcastWithdrawalTransactionsFactory = require('../../../../../lib/abci/handlers/proposal/broadcastWithdrawalTransactionsFactory'); +const BlockInfo = require('../../../../../lib/blockExecution/BlockInfo'); + +describe('broadcastWithdrawalTransactionsFactory', () => { + let broadcastWithdrawalTransactions; + let proposalBlockExecutionContextMock; + let coreRpcMock; + let updateWithdrawalTransactionIdAndStatusMock; + + beforeEach(function beforeEach() { + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + proposalBlockExecutionContextMock.getEpochInfo.returns({ + currentEpochIndex: 1, + }); + proposalBlockExecutionContextMock.getHeight.returns(new Long(1)); + proposalBlockExecutionContextMock.getTimeMs.returns(1); + proposalBlockExecutionContextMock.getCoreChainLockedHeight.returns(42); + + coreRpcMock = { + sendRawTransaction: this.sinon.stub(), + }; + + updateWithdrawalTransactionIdAndStatusMock = this.sinon.stub(); + + broadcastWithdrawalTransactions = broadcastWithdrawalTransactionsFactory( + coreRpcMock, + updateWithdrawalTransactionIdAndStatusMock, + ); + }); + + it('should call Core RPC and call document update function', async () => { + const extension = Buffer.alloc(32, 2); + const signature = Buffer.alloc(32, 3); + + const txBytes = Buffer.alloc(32, 1); + + const thresholdVoteExtensions = [ + { extension, signature }, + ]; + const unsignedWithdrawalTransactionsMap = { + [extension.toString('hex')]: txBytes, + }; + + await broadcastWithdrawalTransactions( + proposalBlockExecutionContextMock, + thresholdVoteExtensions, + unsignedWithdrawalTransactionsMap, + ); + + const expectedMap = { [txBytes.toString('hex')]: Buffer.concat([txBytes, signature]) }; + + expect(coreRpcMock.sendRawTransaction).to.have.been.calledOnceWithExactly( + Buffer.concat([txBytes, signature]).toString('hex'), + ); + expect(updateWithdrawalTransactionIdAndStatusMock).to.have.been.calledOnceWithExactly( + BlockInfo.createFromBlockExecutionContext(proposalBlockExecutionContextMock), + 42, + expectedMap, + { + useTransaction: true, + }, + ); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/createConsensusParamUpdateFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/createConsensusParamUpdateFactory.spec.js new file mode 100644 index 00000000000..0e9482be77f --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/createConsensusParamUpdateFactory.spec.js @@ -0,0 +1,90 @@ +const { + tendermint: { + types: { + ConsensusParams, + }, + }, +} = require('@dashevo/abci/types'); +const Long = require('long'); +const createConsensusParamUpdateFactory = require('../../../../../lib/abci/handlers/proposal/createConsensusParamUpdateFactory'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); + +describe('createConsensusParamUpdateFactory', () => { + let createConsensusParamUpdate; + let getFeatureFlagForHeightMock; + let loggerMock; + let height; + let version; + let getLatestFeatureFlagGetMock; + let proposalBlockExecutionContextMock; + let round; + + beforeEach(function beforeEach() { + round = 42; + loggerMock = new LoggerMock(this.sinon); + height = Long.fromInt(15); + version = { + app: Long.fromInt(1), + }; + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + proposalBlockExecutionContextMock.getVersion.returns(version); + + getFeatureFlagForHeightMock = this.sinon.stub().resolves(null); + getLatestFeatureFlagGetMock = this.sinon.stub(); + + createConsensusParamUpdate = createConsensusParamUpdateFactory( + proposalBlockExecutionContextMock, + getFeatureFlagForHeightMock, + ); + }); + + it('should return consensusParamUpdates if request contains update consensus features flag', async () => { + getLatestFeatureFlagGetMock.withArgs('block').returns({ + maxBytes: 1, + maxGas: 2, + }); + getLatestFeatureFlagGetMock.withArgs('evidence').returns({ + maxAgeNumBlocks: 1, + maxAgeDuration: null, + maxBytes: 2, + }); + getLatestFeatureFlagGetMock.withArgs('version').returns({ + appVersion: 1, + }); + + getFeatureFlagForHeightMock.resolves({ + get: getLatestFeatureFlagGetMock, + }); + + const response = await createConsensusParamUpdate(height, round, loggerMock); + + expect(response).to.deep.equal(new ConsensusParams({ + block: { + maxBytes: 1, + maxGas: 2, + }, + evidence: { + maxAgeDuration: null, + maxAgeNumBlocks: 1, + maxBytes: 2, + }, + version: { + appVersion: 1, + }, + })); + + expect(getFeatureFlagForHeightMock).to.be.calledOnce(); + }); + + it('should return undefined', async () => { + getFeatureFlagForHeightMock.resolves(null); + + const response = await createConsensusParamUpdate(height, round, loggerMock); + + expect(response).to.be.undefined(); + + expect(getFeatureFlagForHeightMock).to.be.calledOnce(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/createCoreChainLockUpdateFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/createCoreChainLockUpdateFactory.spec.js new file mode 100644 index 00000000000..7e52c580dfc --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/createCoreChainLockUpdateFactory.spec.js @@ -0,0 +1,65 @@ +const { + tendermint: { + types: { + CoreChainLock, + }, + }, +} = require('@dashevo/abci/types'); +const createCoreChainLockUpdateFactory = require('../../../../../lib/abci/handlers/proposal/createCoreChainLockUpdateFactory'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); + +describe('createCoreChainLockUpdateFactory', () => { + let createCoreChainLockUpdate; + let latestCoreChainLockMock; + let chainLockMock; + let coreChainLockedHeight; + let loggerMock; + let round; + + beforeEach(function beforeEach() { + round = 0; + loggerMock = new LoggerMock(this.sinon); + + chainLockMock = { + height: 1, + blockHash: Buffer.alloc(0), + signature: Buffer.alloc(0), + }; + + coreChainLockedHeight = 2; + + latestCoreChainLockMock = { + getChainLock: this.sinon.stub().returns(chainLockMock), + }; + + createCoreChainLockUpdate = createCoreChainLockUpdateFactory( + latestCoreChainLockMock, + ); + }); + + it('should return nextCoreChainLockUpdate if latestCoreChainLock above header height', async () => { + chainLockMock.height = 3; + + const response = await createCoreChainLockUpdate(coreChainLockedHeight, round, loggerMock); + + expect(latestCoreChainLockMock.getChainLock).to.have.been.calledOnceWithExactly(); + + const expectedCoreChainLock = new CoreChainLock({ + coreBlockHeight: chainLockMock.height, + coreBlockHash: chainLockMock.blockHash, + signature: chainLockMock.signature, + }); + + expect(response).to.deep.equal(expectedCoreChainLock); + }); + + it('should return undefined', async () => { + chainLockMock.height = 1; + + const response = await createCoreChainLockUpdate(coreChainLockedHeight, round, loggerMock); + + expect(latestCoreChainLockMock.getChainLock).to.have.been.calledOnceWithExactly(); + + expect(response).to.be.undefined(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/deliverTxFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/deliverTxFactory.spec.js new file mode 100644 index 00000000000..2f882d057fc --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/deliverTxFactory.spec.js @@ -0,0 +1,319 @@ +const Long = require('long'); + +const crypto = require('crypto'); + +const DashPlatformProtocol = require('@dashevo/dpp'); + +const { FeeResult } = require('@dashevo/rs-drive'); + +const ValidationResult = require('@dashevo/dpp/lib/validation/ValidationResult'); + +const createDPPMock = require('@dashevo/dpp/lib/test/mocks/createDPPMock'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); + +const StateTransitionExecutionContext = require('@dashevo/dpp/lib/stateTransition/StateTransitionExecutionContext'); + +const SomeConsensusError = require('@dashevo/dpp/lib/test/mocks/SomeConsensusError'); + +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); + +const deliverTxFactory = require('../../../../../lib/abci/handlers/proposal/deliverTxFactory'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const DPPValidationAbciError = require('../../../../../lib/abci/errors/DPPValidationAbciError'); +const InvalidArgumentAbciError = require('../../../../../lib/abci/errors/InvalidArgumentAbciError'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('deliverTxFactory', () => { + let deliverTx; + let documentTx; + let dataContractTx; + let dppMock; + let documentsBatchTransitionFixture; + let dataContractCreateTransitionFixture; + let dpp; + let unserializeStateTransitionMock; + let validationResult; + let executionTimerMock; + let loggerMock; + let round; + let proposalBlockExecutionContextMock; + let stateTransitionExecutionContextMock; + let identityBalanceRepositoryMock; + let processingFee; + let storageFee; + let refundsPerEpoch; + let feeRefunds; + let createContextLoggerMock; + let calculateStateTransitionFeeMock; + let calculateStateTransitionFeeFromOperationsMock; + + beforeEach(async function beforeEach() { + round = 42; + const dataContractFixture = getDataContractFixture(); + const documentFixture = getDocumentsFixture(); + + dpp = new DashPlatformProtocol(); + await dpp.initialize(); + + documentsBatchTransitionFixture = dpp.document.createStateTransition({ + create: documentFixture, + }); + + dataContractCreateTransitionFixture = dpp + .dataContract.createDataContractCreateTransition(dataContractFixture); + + loggerMock = new LoggerMock(this.sinon); + + stateTransitionExecutionContextMock = new StateTransitionExecutionContext(); + + processingFee = 10; + storageFee = 100; + const totalRefunds = 15; + refundsPerEpoch = { + 1: totalRefunds, + }; + feeRefunds = [ + { + identifier: Buffer.alloc(32), + creditsPerEpoch: { 1: totalRefunds }, + }, + ]; + + const actualSTFees = FeeResult.create(storageFee, processingFee, feeRefunds); + + identityBalanceRepositoryMock = { + applyFees: this.sinon.stub().resolves( + new StorageResult(actualSTFees), + ), + }; + + documentsBatchTransitionFixture.setExecutionContext(stateTransitionExecutionContextMock); + dataContractCreateTransitionFixture.setExecutionContext(stateTransitionExecutionContextMock); + + documentTx = documentsBatchTransitionFixture.toBuffer(); + + dataContractTx = dataContractCreateTransitionFixture.toBuffer(); + + dppMock = createDPPMock(this.sinon); + + validationResult = new ValidationResult(); + + dppMock + .stateTransition + .validateState + .resolves(validationResult); + + unserializeStateTransitionMock = this.sinon.stub(); + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + proposalBlockExecutionContextMock.getHeight.returns(Long.fromNumber(42)); + proposalBlockExecutionContextMock.getEpochInfo.returns({ + currentEpochIndex: 0, + }); + + executionTimerMock = { + clearTimer: this.sinon.stub(), + getTimer: this.sinon.stub(), + startTimer: this.sinon.stub(), + stopTimer: this.sinon.stub(), + isStarted: this.sinon.stub(), + }; + + createContextLoggerMock = this.sinon.stub().returns(loggerMock); + + calculateStateTransitionFeeMock = this.sinon.stub().returns({ + storageFee, + processingFee, + feeRefunds, + totalRefunds, + requiredAmount: processingFee - totalRefunds, + desiredAmount: storageFee + processingFee - totalRefunds, + }); + + calculateStateTransitionFeeFromOperationsMock = this.sinon.stub().returns({ + storageFee, + processingFee, + feeRefunds, + totalRefunds, + requiredAmount: processingFee - totalRefunds, + desiredAmount: storageFee + processingFee - totalRefunds, + }); + + deliverTx = deliverTxFactory( + unserializeStateTransitionMock, + dppMock, + proposalBlockExecutionContextMock, + executionTimerMock, + identityBalanceRepositoryMock, + calculateStateTransitionFeeMock, + calculateStateTransitionFeeFromOperationsMock, + createContextLoggerMock, + ); + }); + + it('should execute a state transition and return result', async () => { + unserializeStateTransitionMock.resolves(documentsBatchTransitionFixture); + + const response = await deliverTx(documentTx, round, loggerMock); + + expect(response).to.deep.equal({ + code: 0, + fees: { + processingFee, + storageFee, + refundsPerEpoch, + }, + }); + + expect(unserializeStateTransitionMock).to.be.calledOnceWithExactly( + documentsBatchTransitionFixture.toBuffer(), + { + logger: loggerMock, + executionTimer: executionTimerMock, + }, + ); + + expect(dppMock.stateTransition.validateState).to.be.calledOnceWithExactly( + documentsBatchTransitionFixture, + ); + + expect(dppMock.stateTransition.apply).to.be.calledOnceWithExactly( + documentsBatchTransitionFixture, + ); + + expect(identityBalanceRepositoryMock.applyFees).to.be.calledOnce(); + + const applyFeesToBalanceArgs = identityBalanceRepositoryMock.applyFees.getCall(0).args; + + expect(applyFeesToBalanceArgs).to.have.lengthOf(3); + + const identifier = applyFeesToBalanceArgs[0]; + + expect(identifier).to.equals(documentsBatchTransitionFixture.getOwnerId()); + + const feeResult = applyFeesToBalanceArgs[1]; + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.storageFee).to.equals(storageFee); + expect(feeResult.processingFee).to.equals(processingFee); + expect(feeResult.feeRefunds).to.deep.equals(feeRefunds); + + expect(applyFeesToBalanceArgs[2]).to.deep.equals({ useTransaction: true }); + + expect(proposalBlockExecutionContextMock.addDataContract).to.not.be.called(); + + expect( + dataContractCreateTransitionFixture.getExecutionContext().dryOperations, + ).to.have.length(0); + + const stHash = crypto + .createHash('sha256') + .update(documentTx) + .digest() + .toString('hex') + .toUpperCase(); + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + txId: stHash, + }); + }); + + it('should execute a DataContractCreateTransition', async () => { + unserializeStateTransitionMock.resolves(dataContractCreateTransitionFixture); + + const response = await deliverTx(dataContractTx, round, loggerMock); + + expect(response).to.deep.equal({ + code: 0, + fees: { + processingFee, + storageFee, + refundsPerEpoch, + }, + }); + + expect(unserializeStateTransitionMock).to.be.calledOnceWithExactly( + dataContractCreateTransitionFixture.toBuffer(), + { + logger: loggerMock, + executionTimer: executionTimerMock, + }, + ); + + expect(dppMock.stateTransition.validateState).to.be.calledOnceWithExactly( + dataContractCreateTransitionFixture, + ); + + expect(dppMock.stateTransition.apply).to.be.calledOnceWithExactly( + dataContractCreateTransitionFixture, + ); + + expect(identityBalanceRepositoryMock.applyFees).to.be.calledOnce(); + + const applyFeesToBalanceArgs = identityBalanceRepositoryMock.applyFees.getCall(0).args; + + expect(applyFeesToBalanceArgs).to.have.lengthOf(3); + + const identifier = applyFeesToBalanceArgs[0]; + + expect(identifier).to.equals(dataContractCreateTransitionFixture.getOwnerId()); + + const feeResult = applyFeesToBalanceArgs[1]; + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.storageFee).to.equals(storageFee); + expect(feeResult.processingFee).to.equals(processingFee); + expect(feeResult.feeRefunds).to.deep.equals(feeRefunds); + + expect(applyFeesToBalanceArgs[2]).to.deep.equals({ useTransaction: true }); + + expect( + dataContractCreateTransitionFixture.getExecutionContext().dryOperations, + ).to.have.length(0); + + expect(proposalBlockExecutionContextMock.addDataContract).to.be.calledOnceWith( + dataContractCreateTransitionFixture.getDataContract(), + ); + }); + + it('should throw DPPValidationAbciError if a state transition is invalid against state', async () => { + unserializeStateTransitionMock.resolves(dataContractCreateTransitionFixture); + + const error = new SomeConsensusError('Consensus error'); + + validationResult.addError(error); + + try { + await deliverTx(documentTx, round, loggerMock); + + expect.fail('should throw InvalidArgumentAbciError error'); + } catch (e) { + expect(e).to.be.instanceOf(DPPValidationAbciError); + expect(e.getCode()).to.equal(error.getCode()); + expect(e.getData()).to.deep.equal({ + arguments: ['Consensus error'], + }); + } + }); + + it('should throw DPPValidationAbciError if a state transition is not valid', async () => { + const errorMessage = 'Invalid structure'; + const error = new InvalidArgumentAbciError(errorMessage); + + unserializeStateTransitionMock.throws(error); + + try { + await deliverTx(documentTx, round, loggerMock); + + expect.fail('should throw InvalidArgumentAbciError error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidArgumentAbciError); + expect(e.getMessage()).to.equal(errorMessage); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + expect(dppMock.stateTransition.validate).to.not.be.called(); + } + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/endBlockFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/endBlockFactory.spec.js new file mode 100644 index 00000000000..cbb0ca7a2cc --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/endBlockFactory.spec.js @@ -0,0 +1,134 @@ +const Long = require('long'); + +const endBlockFactory = require('../../../../../lib/abci/handlers/proposal/endBlockFactory'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const GroveDBStoreMock = require('../../../../../lib/test/mock/GroveDBStoreMock'); + +describe('endBlockFactory', () => { + let endBlock; + let height; + let dpnsContractBlockHeight; + let loggerMock; + let createValidatorSetUpdateMock; + let validatorSetMock; + let getFeatureFlagForHeightMock; + let rsAbciMock; + let blockEndMock; + let time; + let createConsensusParamUpdateMock; + let rotateAndCreateValidatorSetUpdateMock; + let groveDBStoreMock; + let appHashFixture; + let validatorSetUpdateFixture; + let consensusParamUpdatesFixture; + let executionTimerMock; + let proposalBlockExecutionContextMock; + let round; + let coreChainLockedHeight; + let fees; + + beforeEach(function beforeEach() { + round = 42; + coreChainLockedHeight = 41; + time = Date.now(); + fees = { + processingFee: 10, + storageFee: 100, + feeRefunds: { + 1: 15, + }, + feeRefundsSum: 15, + }; + + executionTimerMock = { + clearTimer: this.sinon.stub(), + startTimer: this.sinon.stub(), + stopTimer: this.sinon.stub(), + }; + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + proposalBlockExecutionContextMock.hasDataContract.returns(true); + proposalBlockExecutionContextMock.getTimeMs.returns(time); + + proposalBlockExecutionContextMock.getEpochInfo.returns({ + currentEpochIndex: 42, + isEpochChange: true, + }); + + loggerMock = new LoggerMock(this.sinon); + + dpnsContractBlockHeight = 2; + + validatorSetMock = { + rotate: this.sinon.stub(), + getQuorum: this.sinon.stub(), + }; + + createValidatorSetUpdateMock = this.sinon.stub(); + + getFeatureFlagForHeightMock = this.sinon.stub().resolves(null); + + blockEndMock = this.sinon.stub(); + + rsAbciMock = { + blockEnd: blockEndMock, + }; + + blockEndMock.resolves({ + currentEpochIndex: 42, + isEpochChange: true, + }); + + consensusParamUpdatesFixture = Buffer.alloc(1); + validatorSetUpdateFixture = Buffer.alloc(2); + appHashFixture = Buffer.alloc(0); + + createConsensusParamUpdateMock = this.sinon.stub().resolves(consensusParamUpdatesFixture); + rotateAndCreateValidatorSetUpdateMock = this.sinon.stub().resolves(validatorSetUpdateFixture); + + groveDBStoreMock = new GroveDBStoreMock(this.sinon); + groveDBStoreMock.getRootHash.resolves(appHashFixture); + + endBlock = endBlockFactory( + proposalBlockExecutionContextMock, + validatorSetMock, + createValidatorSetUpdateMock, + getFeatureFlagForHeightMock, + createConsensusParamUpdateMock, + rotateAndCreateValidatorSetUpdateMock, + rsAbciMock, + groveDBStoreMock, + executionTimerMock, + ); + + height = Long.fromInt(dpnsContractBlockHeight); + }); + + it('should end block', async () => { + const response = await endBlock({ + height, round, fees, coreChainLockedHeight, + }, loggerMock); + + expect(response).to.deep.equal({ + consensusParamUpdates: consensusParamUpdatesFixture, + validatorSetUpdate: validatorSetUpdateFixture, + appHash: appHashFixture, + }); + + expect(proposalBlockExecutionContextMock.hasDataContract).to.not.have.been.called(); + expect(createConsensusParamUpdateMock).to.be.calledOnceWithExactly(height, round, loggerMock); + expect(rotateAndCreateValidatorSetUpdateMock).to.be.calledOnceWithExactly( + height, + coreChainLockedHeight, + round, + loggerMock, + ); + expect(groveDBStoreMock.getRootHash).to.be.calledOnceWithExactly({ useTransaction: true }); + + expect(rsAbciMock.blockEnd).to.be.calledOnceWithExactly({ fees }, true); + + expect(executionTimerMock.stopTimer).to.be.calledOnceWithExactly('roundExecution', true); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/processProposalFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/processProposalFactory.spec.js new file mode 100644 index 00000000000..c114a5e676a --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/processProposalFactory.spec.js @@ -0,0 +1,170 @@ +const { + tendermint: { + abci: { + ResponseProcessProposal, + ValidatorSetUpdate, + }, + types: { + ConsensusParams, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const processProposalHandlerFactory = require('../../../../../lib/abci/handlers/proposal/processProposalFactory'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); + +describe('processProposalFactory', () => { + let processProposalHandler; + let request; + let loggerMock; + let beginBlockMock; + let endBlockMock; + let deliverTxMock; + let appHash; + let validatorSetUpdate; + let consensusParamUpdates; + let coreChainLockUpdate; + let proposalBlockExecutionContextMock; + let round; + let executionTimerMock; + let quorumHash; + + beforeEach(function beforeEach() { + round = 0; + appHash = Buffer.alloc(1, 1); + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + loggerMock = new LoggerMock(this.sinon); + + consensusParamUpdates = new ConsensusParams({ + block: { + maxBytes: 1, + maxGas: 2, + }, + evidence: { + maxAgeDuration: null, + maxAgeNumBlocks: 1, + maxBytes: 2, + }, + version: { + appVersion: 1, + }, + }); + + validatorSetUpdate = new ValidatorSetUpdate(); + + beginBlockMock = this.sinon.stub(); + + endBlockMock = this.sinon.stub().resolves({ + consensusParamUpdates, + appHash, + validatorSetUpdate, + }); + + deliverTxMock = this.sinon.stub().resolves({ + code: 0, + fees: { + processingFee: 10, + storageFee: 100, + refundsPerEpoch: { + 1: 15, + }, + }, + }); + + beginBlockMock = this.sinon.stub(); + + executionTimerMock = { + getTimer: this.sinon.stub().returns(0.1), + }; + + processProposalHandler = processProposalHandlerFactory( + deliverTxMock, + proposalBlockExecutionContextMock, + beginBlockMock, + endBlockMock, + executionTimerMock, + ); + + const txs = new Array(3).fill(Buffer.alloc(5, 0)); + + const height = new Long(42); + + const time = { + seconds: Math.ceil(new Date().getTime() / 1000), + }; + const version = { + app: Long.fromInt(1), + }; + const proposerProTxHash = Uint8Array.from([1, 2, 3, 4]); + const coreChainLockedHeight = 10; + const proposedLastCommit = {}; + + coreChainLockUpdate = { + coreBlockHeight: 42, + coreBlockHash: '1528e523f4c20fa84ba70dd96372d34e00ce260f357d53ad1a8bc892ebf20e2d', + signature: '1897ce8f54d2070f44ca5c29983b68b391e8137c25e44f67416e579f3e3bdfef7b4fd22db7818399147e52907998857b0fbc8edfdc40a64f2c7df0e88544d31d12ca8c15e73d50dda25ca23f754ed3f789ed4bcb392161995f464017c10df404', + }; + + quorumHash = Buffer.alloc(32, 0); + + request = { + round, + height, + txs, + coreChainLockedHeight, + version, + proposedLastCommit, + time, + proposerProTxHash, + coreChainLockUpdate, + quorumHash, + }; + }); + + it('should return ResponseProcessProposal', async () => { + const result = await processProposalHandler(request, loggerMock); + + expect(result).to.be.an.instanceOf(ResponseProcessProposal); + expect(result.status).to.equal(1); + expect(result.appHash).to.equal(appHash); + expect(result.txResults).to.be.deep.equal(new Array(3).fill({ code: 0 })); + expect(result.consensusParamUpdates).to.be.equal(consensusParamUpdates); + expect(result.validatorSetUpdate).to.be.equal(validatorSetUpdate); + + expect(beginBlockMock).to.be.calledOnceWithExactly( + { + lastCommitInfo: request.proposedLastCommit, + height: request.height, + coreChainLockedHeight: request.coreChainLockedHeight, + version: request.version, + time: request.time, + proposerProTxHash: Buffer.from(request.proposerProTxHash), + proposedAppVersion: request.proposedAppVersion, + round, + quorumHash, + }, + loggerMock, + ); + + expect(deliverTxMock).to.be.calledThrice(); + + expect(endBlockMock).to.be.calledOnceWithExactly({ + height: request.height, + round, + fees: { + processingFee: 10 * 3, + storageFee: 100 * 3, + refundsPerEpoch: { + 1: 15 * 3, + }, + }, + coreChainLockedHeight: request.coreChainLockedHeight, + }, + loggerMock); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.spec.js new file mode 100644 index 00000000000..e404f70cdec --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory.spec.js @@ -0,0 +1,103 @@ +const { + tendermint: { + abci: { + ValidatorSetUpdate, + }, + }, +} = require('@dashevo/abci/types'); +const Long = require('long'); +const rotateAndCreateValidatorSetUpdateFactory = require('../../../../../lib/abci/handlers/proposal/rotateAndCreateValidatorSetUpdateFactory'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); + +describe('rotateAndCreateValidatorSetUpdateFactory', () => { + let rotateAndCreateValidatorSetUpdate; + let validatorSetMock; + let createValidatorSetUpdateMock; + let height; + let loggerMock; + let lastCommitInfoMock; + let round; + let proposalBlockExecutionContextMock; + let coreChainLockedHeight; + + beforeEach(function beforeEach() { + round = 0; + coreChainLockedHeight = 1; + + lastCommitInfoMock = { + blockSignature: Uint8Array.from('003657bb44d74c371d14485117de43313ca5c2848f3622d691c2b1bf3576a64bdc2538efab24854eb82ae7db38482dbd15a1cb3bc98e55173817c9d05c86e47a5d67614a501414aae6dd1565e59422d1d77c41ae9b38de34ecf1e9f778b2a97b'), + }; + + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + proposalBlockExecutionContextMock.getLastCommitInfo.returns(lastCommitInfoMock); + + validatorSetMock = { + rotate: this.sinon.stub(), + getQuorum: this.sinon.stub(), + }; + + createValidatorSetUpdateMock = this.sinon.stub(); + + loggerMock = new LoggerMock(this.sinon); + + rotateAndCreateValidatorSetUpdate = rotateAndCreateValidatorSetUpdateFactory( + proposalBlockExecutionContextMock, + validatorSetMock, + createValidatorSetUpdateMock, + ); + }); + + it('should rotate validator set and return ValidatorSetUpdate if height is divisible by ROTATION_BLOCK_INTERVAL', async () => { + height = Long.fromInt(15); + + const quorumHash = Buffer.alloc(64).fill(1).toString('hex'); + + validatorSetMock.rotate.resolves(true); + validatorSetMock.getQuorum.resolves({ quorumHash }); + + const validatorSetUpdate = new ValidatorSetUpdate(); + + createValidatorSetUpdateMock.returns(validatorSetUpdate); + + const response = await rotateAndCreateValidatorSetUpdate( + height, + coreChainLockedHeight, + round, + loggerMock, + ); + + expect(validatorSetMock.rotate).to.be.calledOnceWithExactly( + height, + coreChainLockedHeight, + Buffer.from(lastCommitInfoMock.blockSignature), + ); + + expect(createValidatorSetUpdateMock).to.be.calledOnceWithExactly(validatorSetMock); + + expect(response).to.be.equal(validatorSetUpdate); + }); + + it('should return undefined', async () => { + height = Long.fromInt(15); + + validatorSetMock.rotate.resolves(false); + + const response = await rotateAndCreateValidatorSetUpdate( + height, + coreChainLockedHeight, + round, + loggerMock, + ); + + expect(validatorSetMock.rotate).to.be.calledOnceWithExactly( + height, + coreChainLockedHeight, + Buffer.from(lastCommitInfoMock.blockSignature), + ); + + expect(createValidatorSetUpdateMock).to.not.be.called(); + + expect(response).to.be.undefined(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/proposal/verifyChainLockFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/proposal/verifyChainLockFactory.spec.js new file mode 100644 index 00000000000..d8debd3330b --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/proposal/verifyChainLockFactory.spec.js @@ -0,0 +1,117 @@ +const verifyChainLockFactory = require('../../../../../lib/abci/handlers/proposal/verifyChainLockFactory'); + +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); + +describe('verifyChainLockFactory', () => { + let verifyChainLock; + let chainLockMock; + let loggerMock; + let coreRpcClientMock; + let latestBlockExecutionContextMock; + + beforeEach(function beforeEach() { + latestBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + latestBlockExecutionContextMock.getCoreChainLockedHeight.returns(41); + + chainLockMock = { + coreBlockHash: Buffer.alloc(1, 1).toString(), + signature: Buffer.alloc(1, 2).toString(), + coreBlockHeight: 42, + }; + + loggerMock = new LoggerMock(this.sinon); + + coreRpcClientMock = { + verifyChainLock: this.sinon.stub(), + }; + coreRpcClientMock.verifyChainLock.resolves({ result: true }); + + verifyChainLock = verifyChainLockFactory( + coreRpcClientMock, + latestBlockExecutionContextMock, + loggerMock, + ); + }); + + it('should verify chain lock though Core', async () => { + const result = await verifyChainLock(chainLockMock); + + expect(result).to.be.true(); + expect(coreRpcClientMock.verifyChainLock).to.be.calledOnceWithExactly( + Buffer.from(chainLockMock.coreBlockHash).toString('hex'), + Buffer.from(chainLockMock.signature).toString('hex'), + chainLockMock.coreBlockHeight, + ); + + expect(latestBlockExecutionContextMock.getCoreChainLockedHeight).to.be.calledOnce(); + }); + + it('should return false if chainLock is not valid', async () => { + coreRpcClientMock.verifyChainLock.resolves({ result: false }); + + const result = await verifyChainLock(chainLockMock); + + expect(result).to.be.false(); + }); + + it('should return false if Core returns parse error', async () => { + const error = new Error('parse error'); + error.code = -32700; + + coreRpcClientMock.verifyChainLock.throws(error); + + const result = await verifyChainLock(chainLockMock); + + expect(result).to.be.false(); + expect(coreRpcClientMock.verifyChainLock).to.be.calledOnceWithExactly( + Buffer.from(chainLockMock.coreBlockHash).toString('hex'), + Buffer.from(chainLockMock.signature).toString('hex'), + chainLockMock.coreBlockHeight, + ); + }); + + it('should return false if Core returns invalid signature format error', async () => { + const error = new Error('invalid signature format'); + error.code = -8; + + coreRpcClientMock.verifyChainLock.throws(error); + + const result = await verifyChainLock(chainLockMock); + + expect(result).to.be.false(); + expect(coreRpcClientMock.verifyChainLock).to.be.calledOnceWithExactly( + Buffer.from(chainLockMock.coreBlockHash).toString('hex'), + Buffer.from(chainLockMock.signature).toString('hex'), + chainLockMock.coreBlockHeight, + ); + }); + + it('should throw an error if Core throws error', async () => { + const error = new Error(); + + coreRpcClientMock.verifyChainLock.throws(error); + + try { + await verifyChainLock(chainLockMock); + + expect.fail('error was not thrown'); + } catch (e) { + expect(e).to.deep.equal(error); + } + + expect(coreRpcClientMock.verifyChainLock).to.be.calledOnceWithExactly( + Buffer.from(chainLockMock.coreBlockHash).toString('hex'), + Buffer.from(chainLockMock.signature).toString('hex'), + chainLockMock.coreBlockHeight, + ); + }); + + it('should return false if coreBlockHeight >= lastCoreChainLockedHeight', async () => { + latestBlockExecutionContextMock.getCoreChainLockedHeight.returns(42); + const result = await verifyChainLock(chainLockMock); + + expect(result).to.be.false(); + expect(coreRpcClientMock.verifyChainLock).to.not.be.called(); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/dataContractQueryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/dataContractQueryHandlerFactory.spec.js new file mode 100644 index 00000000000..bafac7aa54f --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/dataContractQueryHandlerFactory.spec.js @@ -0,0 +1,124 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetDataContractResponse, + Proof, + }, +} = require('@dashevo/dapi-grpc'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); + +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const dataContractQueryHandlerFactory = require('../../../../../lib/abci/handlers/query/dataContractQueryHandlerFactory'); + +const NotFoundAbciError = require('../../../../../lib/abci/errors/NotFoundAbciError'); +const StoreRepositoryMock = require('../../../../../lib/test/mock/StoreRepositoryMock'); +const InvalidArgumentAbciError = require('../../../../../lib/abci/errors/InvalidArgumentAbciError'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('dataContractQueryHandlerFactory', () => { + let dataContractQueryHandler; + let dataContract; + let params; + let data; + let createQueryResponseMock; + let responseMock; + let dataContractRepositoryMock; + + beforeEach(function beforeEach() { + dataContract = getDataContractFixture(); + + createQueryResponseMock = this.sinon.stub(); + + responseMock = new GetDataContractResponse(); + responseMock.setProof(new Proof()); + + createQueryResponseMock.returns(responseMock); + + dataContractRepositoryMock = new StoreRepositoryMock(this.sinon); + + dataContractQueryHandler = dataContractQueryHandlerFactory( + dataContractRepositoryMock, + createQueryResponseMock, + ); + + params = { }; + data = { + id: dataContract.getId(), + }; + }); + + it('should throw NotFoundAbciError if Data Contract not found', async () => { + dataContractRepositoryMock.fetch.resolves( + new StorageResult(null), + ); + + try { + await dataContractQueryHandler(params, data, {}); + + expect.fail('should throw NotFoundAbciError'); + } catch (e) { + expect(e).to.be.an.instanceOf(NotFoundAbciError); + expect(dataContractRepositoryMock.fetch).to.be.calledOnce(); + } + }); + + it('should return data contract', async () => { + dataContractRepositoryMock.fetch.resolves( + new StorageResult(dataContract), + ); + + const result = await dataContractQueryHandler(params, data, {}); + + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); + + it('should InvalidArgumentAbciError on wrong Id', async () => { + data.id = Buffer.alloc(0); + + try { + await dataContractQueryHandler(params, data, {}); + + expect.fail('should throw InvalidArgumentAbciError'); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidArgumentAbciError); + } + }); + + it('should return proof if it was requested', async () => { + // const proof = { + // rootTreeProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', 'hex'), + // storeTreeProof: Buffer.from('03046b657931060076616c75653103046b657932060076616c75653210', + // 'hex'), + // }; + + const proof = Buffer.alloc(20, 255); + + dataContractRepositoryMock.fetch.resolves( + new StorageResult(dataContract), + ); + + dataContractRepositoryMock.prove.resolves( + new StorageResult(proof), + ); + + const result = await dataContractQueryHandler(params, data, { prove: true }); + + expect(dataContractRepositoryMock.prove).to.be.calledOnceWithExactly( + new Identifier(data.id), + ); + + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/documentQueryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/documentQueryHandlerFactory.spec.js new file mode 100644 index 00000000000..cb56c5aefbe --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/documentQueryHandlerFactory.spec.js @@ -0,0 +1,167 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetDocumentsResponse, + Proof, + }, +} = require('@dashevo/dapi-grpc'); + +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); + +const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); +const documentQueryHandlerFactory = require('../../../../../lib/abci/handlers/query/documentQueryHandlerFactory'); +const InvalidQueryError = require('../../../../../lib/document/errors/InvalidQueryError'); + +const UnavailableAbciError = require('../../../../../lib/abci/errors/UnavailableAbciError'); +const InvalidArgumentAbciError = require('../../../../../lib/abci/errors/InvalidArgumentAbciError'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('documentQueryHandlerFactory', () => { + let documentQueryHandler; + let fetchSignedDocumentsMock; + let proveSignedDocumentsMock; + let documents; + let params; + let data; + let options; + let createQueryResponseMock; + let responseMock; + + beforeEach(function beforeEach() { + documents = getDocumentsFixture(); + + fetchSignedDocumentsMock = this.sinon.stub(); + proveSignedDocumentsMock = this.sinon.stub(); + createQueryResponseMock = this.sinon.stub(); + + responseMock = new GetDocumentsResponse(); + responseMock.setProof(new Proof()); + + createQueryResponseMock.returns(responseMock); + + documentQueryHandler = documentQueryHandlerFactory( + fetchSignedDocumentsMock, + proveSignedDocumentsMock, + createQueryResponseMock, + ); + + params = {}; + data = { + contractId: generateRandomIdentifier(), + type: 'documentType', + orderBy: [{ sort: 'asc' }], + limit: 2, + startAt: undefined, + startAfter: undefined, + where: [['field', '==', 'value']], + }; + options = { + orderBy: data.orderBy, + limit: data.limit, + startAt: data.startAt, + startAfter: data.startAfter, + where: data.where, + }; + }); + + it('should return serialized documents', async () => { + fetchSignedDocumentsMock.resolves( + new StorageResult(documents), + ); + + const result = await documentQueryHandler(params, data, {}); + + expect(createQueryResponseMock).to.be.calledOnceWith(GetDocumentsResponse, undefined); + expect(fetchSignedDocumentsMock).to.be.calledOnceWith( + data.contractId, + data.type, + options, + ); + expect(proveSignedDocumentsMock).to.not.be.called(); + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); + + it('should return proof if it was requested', async () => { + // const proof = { + // rootTreeProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', + // 'hex'), + // storeTreeProof: Buffer.from('03046b657931060076616c75653103046b657932060076616c75653210', + // 'hex'), + // }; + + const proof = Buffer.alloc(20, 255); + + fetchSignedDocumentsMock.resolves(new StorageResult(documents)); + proveSignedDocumentsMock.resolves( + new StorageResult(proof), + ); + + const result = await documentQueryHandler(params, data, { prove: true }); + + expect(createQueryResponseMock).to.be.calledOnceWith(GetDocumentsResponse, true); + expect(fetchSignedDocumentsMock).to.not.be.called(); + expect(proveSignedDocumentsMock).to.be.calledOnceWith(data.contractId, data.type, options); + + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); + + it('should throw InvalidArgumentAbciError on invalid query', async () => { + fetchSignedDocumentsMock.throws(new InvalidQueryError('invalid')); + + try { + await documentQueryHandler(params, data, {}); + + expect.fail('should throw UnavailableAbciError'); + } catch (e) { + expect(e).to.be.an.instanceof(InvalidArgumentAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + expect(e.getMessage()).to.equal('Invalid query: invalid'); + expect(fetchSignedDocumentsMock).to.be.calledOnceWith(data.contractId, data.type); + } + }); + + it('should not proceed forward if createQueryResponse throws UnavailableAbciError', async () => { + createQueryResponseMock.throws(new UnavailableAbciError('message')); + + try { + await documentQueryHandler(params, data, {}); + + expect.fail('should throw UnavailableAbciError'); + } catch (e) { + expect(e).to.be.an.instanceof(UnavailableAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.UNAVAILABLE); + expect(e.getMessage()).to.equal('message'); + expect(fetchSignedDocumentsMock).to.not.be.called(); + } + }); + + it('should throw error if fetchSignedDocuments throws unknown error', async () => { + const error = new Error('Some error'); + + fetchSignedDocumentsMock.throws(error); + + try { + await documentQueryHandler(params, data, {}); + + expect.fail('should throw any error'); + } catch (e) { + expect(e).to.deep.equal(error); + expect(fetchSignedDocumentsMock).to.be.calledOnceWith(data.contractId, data.type); + } + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/getProofsQueryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/getProofsQueryHandlerFactory.spec.js new file mode 100644 index 00000000000..ca2970a27b5 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/getProofsQueryHandlerFactory.spec.js @@ -0,0 +1,122 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); +const cbor = require('cbor'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); + +const getProofsQueryHandlerFactory = require('../../../../../lib/abci/handlers/query/getProofsQueryHandlerFactory'); +const BlockExecutionContextMock = require('../../../../../lib/test/mock/BlockExecutionContextMock'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('getProofsQueryHandlerFactory', () => { + let getProofsQueryHandler; + let dataContract; + let identity; + let documents; + let dataContractData; + let documentsData; + let identityData; + let blockExecutionContextMock; + let identityRepositoryMock; + let dataContractRepositoryMock; + let documentRepository; + let timeMs; + + beforeEach(function beforeEach() { + dataContract = getDataContractFixture(); + identity = getIdentityFixture(); + documents = getDocumentsFixture(); + + const version = { + app: Long.fromInt(1), + }; + + timeMs = Date.now(); + + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + blockExecutionContextMock.getHeight.returns(new Long(42)); + blockExecutionContextMock.getCoreChainLockedHeight.returns(41); + blockExecutionContextMock.getTimeMs.returns(timeMs); + blockExecutionContextMock.getVersion.returns(version); + blockExecutionContextMock.getRound.returns(42); + blockExecutionContextMock.getLastCommitInfo.returns({ + quorumHash: Buffer.alloc(32, 1), + blockSignature: Buffer.alloc(32, 1), + }); + + identityRepositoryMock = { + proveMany: this.sinon.stub().resolves(new StorageResult(Buffer.from([1]))), + }; + dataContractRepositoryMock = { + proveMany: this.sinon.stub().resolves(new StorageResult(Buffer.from([1]))), + }; + + documentRepository = { + proveManyDocumentsFromDifferentContracts: this.sinon.stub().resolves( + new StorageResult(Buffer.from([1])), + ), + }; + + getProofsQueryHandler = getProofsQueryHandlerFactory( + blockExecutionContextMock, + identityRepositoryMock, + dataContractRepositoryMock, + documentRepository, + ); + + dataContractData = { + id: dataContract.getId(), + }; + identityData = { + id: identity.getId(), + }; + documentsData = documents.map((doc) => ({ + documentId: doc.getId(), + dataContractId: doc.getDataContractId(), + type: doc.getType(), + })); + }); + + it('should return proof for passed data contract ids', async () => { + const expectedProof = { + quorumHash: Buffer.alloc(32, 1), + signature: Buffer.alloc(32, 1), + merkleProof: Buffer.from([1]), + round: 42, + }; + + const result = await getProofsQueryHandler({}, { + dataContractIds: [dataContractData.id], + identityIds: [identityData.id], + documents: documentsData, + }); + + const expectedResult = new ResponseQuery({ + value: cbor.encode( + { + documentsProof: expectedProof, + identitiesProof: expectedProof, + dataContractsProof: expectedProof, + metadata: { + height: 42, + coreChainLockedHeight: 41, + timeMs, + protocolVersion: 1, + }, + }, + ), + }); + + expect(result).to.be.deep.equal(expectedResult); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.spec.js new file mode 100644 index 00000000000..9d4571ea4a4 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory.spec.js @@ -0,0 +1,135 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetIdentitiesByPublicKeyHashesResponse, + Proof, + }, +} = require('@dashevo/dapi-grpc'); + +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); + +const identitiesByPublicKeyHashesQueryHandlerFactory = require( + '../../../../../lib/abci/handlers/query/identitiesByPublicKeyHashesQueryHandlerFactory', +); +const InvalidArgumentAbciError = require('../../../../../lib/abci/errors/InvalidArgumentAbciError'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('identitiesByPublicKeyHashesQueryHandlerFactory', () => { + let identitiesByPublicKeyHashesQueryHandler; + let identityRepositoryMock; + let publicKeyHashes; + let identities; + let maxIdentitiesPerRequest; + let createQueryResponseMock; + let responseMock; + let params; + let data; + + beforeEach(function beforeEach() { + identityRepositoryMock = { + proveManyByPublicKeyHashes: this.sinon.stub(), + fetchManyByPublicKeyHashes: this.sinon.stub(), + }; + + maxIdentitiesPerRequest = 5; + + createQueryResponseMock = this.sinon.stub(); + + responseMock = new GetIdentitiesByPublicKeyHashesResponse(); + responseMock.setProof(new Proof()); + + createQueryResponseMock.returns(responseMock); + + identitiesByPublicKeyHashesQueryHandler = identitiesByPublicKeyHashesQueryHandlerFactory( + identityRepositoryMock, + maxIdentitiesPerRequest, + createQueryResponseMock, + ); + + publicKeyHashes = [ + Buffer.from('784ca12495d2e61f992db9e55d1f9599b0cf1328', 'hex'), + Buffer.from('784ca12495d2e61f992db9e55d1f9599b0cf1329', 'hex'), + Buffer.from('784ca12495d2e61f992db9e55d1f9599b0cf1330', 'hex'), + ]; + + identities = [ + getIdentityFixture(), + getIdentityFixture(), + ]; + + identityRepositoryMock + .fetchManyByPublicKeyHashes.resolves( + new StorageResult([identities[0], identities[1]]), + ); + + params = {}; + data = { publicKeyHashes }; + }); + + it('should throw an error if maximum requested items exceeded', async () => { + maxIdentitiesPerRequest = 1; + + identitiesByPublicKeyHashesQueryHandler = identitiesByPublicKeyHashesQueryHandlerFactory( + identityRepositoryMock, + maxIdentitiesPerRequest, + createQueryResponseMock, + ); + + try { + await identitiesByPublicKeyHashesQueryHandler(params, data, {}); + + expect.fail('Error was not thrown'); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidArgumentAbciError); + expect(e.getData()).to.deep.equal({ + maxIdentitiesPerRequest, + }); + } + }); + + it('should return identities', async () => { + params = publicKeyHashes; + + const result = await identitiesByPublicKeyHashesQueryHandler(params, data, {}); + + expect(identityRepositoryMock.fetchManyByPublicKeyHashes).to.be.calledOnceWithExactly( + publicKeyHashes, + ); + + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); + + it('should return proof if it was requested', async () => { + // const proof = { + // rootTreeProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', + // 'hex'), + // storeTreeProof: Buffer.from('03046b657931060076616c75653103046b657932060076616c75653210', + // 'hex'), + // }; + + const proof = Buffer.alloc(20, 1); + + identityRepositoryMock.proveManyByPublicKeyHashes.resolves( + new StorageResult(proof), + ); + + const result = await identitiesByPublicKeyHashesQueryHandler(params, data, { prove: true }); + + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + + expect(identityRepositoryMock.proveManyByPublicKeyHashes).to.be.calledOnceWithExactly( + data.publicKeyHashes, + ); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/identityQueryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/identityQueryHandlerFactory.spec.js new file mode 100644 index 00000000000..d0e9167fee5 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/identityQueryHandlerFactory.spec.js @@ -0,0 +1,112 @@ +const { + tendermint: { + abci: { + ResponseQuery, + }, + }, +} = require('@dashevo/abci/types'); + +const { + v0: { + GetIdentityResponse, + Proof, + }, +} = require('@dashevo/dapi-grpc'); + +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); + +const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); +const identityQueryHandlerFactory = require('../../../../../lib/abci/handlers/query/identityQueryHandlerFactory'); +const NotFoundAbciError = require('../../../../../lib/abci/errors/NotFoundAbciError'); +const StorageResult = require('../../../../../lib/storage/StorageResult'); + +describe('identityQueryHandlerFactory', () => { + let identityQueryHandler; + let identityRepositoryMock; + let identity; + let params; + let data; + let createQueryResponseMock; + let responseMock; + + beforeEach(function beforeEach() { + identityRepositoryMock = { + fetch: this.sinon.stub(), + prove: this.sinon.stub(), + }; + + createQueryResponseMock = this.sinon.stub(); + + responseMock = new GetIdentityResponse(); + responseMock.setProof(new Proof()); + + createQueryResponseMock.returns(responseMock); + + identityQueryHandler = identityQueryHandlerFactory( + identityRepositoryMock, + createQueryResponseMock, + ); + + identity = getIdentityFixture(); + + params = {}; + data = { + id: identity.getId(), + }; + }); + + it('should return serialized identity', async () => { + identityRepositoryMock.fetch.resolves( + new StorageResult(identity), + ); + + const result = await identityQueryHandler(params, data, {}); + + expect(identityRepositoryMock.fetch).to.be.calledOnceWith(data.id); + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); + + it('should throw NotFoundAbciError if identity not found', async () => { + identityRepositoryMock.fetch.resolves( + new StorageResult(null), + ); + + try { + await identityQueryHandler(params, data, {}); + + expect.fail('should throw NotFoundAbciError'); + } catch (e) { + expect(e).to.be.an.instanceof(NotFoundAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.NOT_FOUND); + expect(e.message).to.equal('Identity not found'); + expect(identityRepositoryMock.fetch).to.be.calledOnceWith(data.id); + } + }); + + it('should return proof if it was requested', async () => { + // const proof = { + // rootTreeProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', + // 'hex'), + // storeTreeProof: Buffer.from('03046b657931060076616c75653103046b657932060076616c75653210', + // 'hex'), + // }; + const proof = Buffer.alloc(20, 1); + + identityRepositoryMock.fetch.resolves( + new StorageResult(null), + ); + identityRepositoryMock.prove.resolves( + new StorageResult(proof), + ); + + const result = await identityQueryHandler(params, data, { prove: true }); + + expect(identityRepositoryMock.fetch).to.not.be.called(); + expect(identityRepositoryMock.prove).to.be.calledOnceWith(data.id); + expect(result).to.be.an.instanceof(ResponseQuery); + expect(result.code).to.equal(0); + expect(result.value).to.deep.equal(responseMock.serializeBinary()); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/query/response/createQueryResponseFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/query/response/createQueryResponseFactory.spec.js new file mode 100644 index 00000000000..e87bfa35555 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/query/response/createQueryResponseFactory.spec.js @@ -0,0 +1,77 @@ +const { + v0: { + GetDataContractResponse, + }, +} = require('@dashevo/dapi-grpc'); + +const BlockExecutionContextMock = require('../../../../../../lib/test/mock/BlockExecutionContextMock'); +const createQueryResponseFactory = require('../../../../../../lib/abci/handlers/query/response/createQueryResponseFactory'); + +describe('createQueryResponseFactory', () => { + let createQueryResponse; + let metadata; + let lastCommitInfo; + let blockExecutionContextMock; + let timeMs; + + beforeEach(function beforeEach() { + const version = { + app: 1, + }; + + timeMs = Date.now(); + + lastCommitInfo = { + quorumHash: Buffer.alloc(12).fill(1), + blockSignature: Buffer.alloc(12).fill(2), + }; + + metadata = { + height: 1, + coreChainLockedHeight: 1, + timeMs, + protocolVersion: version.app, + }; + + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + blockExecutionContextMock.getHeight.returns(metadata.height); + blockExecutionContextMock.getCoreChainLockedHeight.returns(metadata.coreChainLockedHeight); + blockExecutionContextMock.getTimeMs.returns(timeMs); + blockExecutionContextMock.getVersion.returns(version); + blockExecutionContextMock.getLastCommitInfo.returns(lastCommitInfo); + blockExecutionContextMock.isEmpty.returns(false); + blockExecutionContextMock.getRound.returns(42); + + createQueryResponse = createQueryResponseFactory( + blockExecutionContextMock, + ); + }); + + it('should create a response', () => { + const response = createQueryResponse(GetDataContractResponse); + + response.serializeBinary(); + + expect(response).to.be.instanceOf(GetDataContractResponse); + expect(response.getMetadata().toObject()).to.deep.equal(metadata); + expect(response.getProof()).to.undefined(); + }); + + it('should create a response with proof if requested', () => { + const response = createQueryResponse(GetDataContractResponse, true); + + response.serializeBinary(); + + expect(response).to.be.instanceOf(GetDataContractResponse); + + expect(response.getMetadata().toObject()).to.deep.equal(metadata); + + expect(response.getProof().toObject()).to.deep.equal({ + quorumHash: lastCommitInfo.quorumHash.toString('base64'), + signature: lastCommitInfo.blockSignature.toString('base64'), + merkleProof: '', + round: 42, + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/queryHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/queryHandlerFactory.spec.js new file mode 100644 index 00000000000..bac844658ee --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/queryHandlerFactory.spec.js @@ -0,0 +1,129 @@ +const cbor = require('cbor'); + +const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); +const queryHandlerFactory = require('../../../../lib/abci/handlers/queryHandlerFactory'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); +const InvalidArgumentAbciError = require('../../../../lib/abci/errors/InvalidArgumentAbciError'); + +describe('queryHandlerFactory', () => { + let queryHandler; + let queryHandlerRouterMock; + let sanitizeUrlMock; + let request; + let routeMock; + let loggerMock; + let createContextLoggerMock; + + beforeEach(function beforeEach() { + request = { + path: '/identity', + data: cbor.encode(Buffer.from('data')), + }; + + loggerMock = new LoggerMock(this.sinon); + + sanitizeUrlMock = this.sinon.stub(); + + routeMock = { + handler: this.sinon.stub(), + params: 'params', + }; + + queryHandlerRouterMock = { + find: this.sinon.stub().returns(routeMock), + }; + + createContextLoggerMock = this.sinon.stub(); + + queryHandler = queryHandlerFactory( + queryHandlerRouterMock, + sanitizeUrlMock, + loggerMock, + createContextLoggerMock, + ); + }); + + it('should throw InvalidArgumentAbciError if route was not found', async () => { + const sanitizedUrl = 'sanitizedUrl'; + + sanitizeUrlMock.returns(sanitizedUrl); + queryHandlerRouterMock.find.returns(false); + + try { + await queryHandler(request); + + expect.fail('should throw InvalidArgumentAbciError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidArgumentAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + + expect(sanitizeUrlMock).to.be.calledOnceWith(request.path); + expect(queryHandlerRouterMock.find).to.be.calledOnceWith('GET', sanitizedUrl); + expect(routeMock.handler).to.be.not.called(); + } + }); + + it('should throw InvalidArgumentAbciError if fail to decode request data', async () => { + const sanitizedUrl = 'sanitizedUrl'; + + sanitizeUrlMock.returns(sanitizedUrl); + + request.data = Buffer.from('bb'); + + try { + await queryHandler(request); + + expect.fail('should throw InvalidArgumentAbciError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidArgumentAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + + expect(sanitizeUrlMock).to.be.calledOnceWith(request.path); + expect(queryHandlerRouterMock.find).to.be.calledOnceWith('GET', sanitizedUrl); + expect(routeMock.handler).to.be.not.called(); + } + }); + + it('should throw InvalidArgumentAbciError on invalid request data', async () => { + const sanitizedUrl = 'sanitizedUrl'; + + sanitizeUrlMock.returns(sanitizedUrl); + + request.data = cbor.encode(null); + + try { + await queryHandler(request); + + expect.fail('should throw InvalidArgumentAbciError'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidArgumentAbciError); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + + expect(sanitizeUrlMock).to.be.calledOnceWith(request.path); + expect(queryHandlerRouterMock.find).to.be.calledOnceWith('GET', sanitizedUrl); + expect(routeMock.handler).to.be.not.called(); + } + }); + + it('should call route handler without data'); + + it('should call route handler and return response', async () => { + const data = 'some data'; + const encodedData = cbor.decode(Buffer.from(request.data)); + const sanitizedUrl = 'sanitizedUrl'; + + sanitizeUrlMock.returns(sanitizedUrl); + routeMock.handler.resolves(data); + queryHandlerRouterMock.find.returns(routeMock); + + const result = await queryHandler(request); + + expect(sanitizeUrlMock).to.be.calledOnceWith(request.path); + expect(queryHandlerRouterMock.find).to.be.calledOnceWith('GET', sanitizedUrl); + expect(routeMock.handler).to.be.calledOnceWith(routeMock.params, encodedData, request); + expect(result).to.equal(data); + expect(createContextLoggerMock).to.be.calledOnceWithExactly(loggerMock, { + abciMethod: 'query', + }); + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/stateTransition/unserializeStateTransitionFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/stateTransition/unserializeStateTransitionFactory.spec.js new file mode 100644 index 00000000000..d582ded796f --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/stateTransition/unserializeStateTransitionFactory.spec.js @@ -0,0 +1,204 @@ +const getIdentityCreateTransitionFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityCreateTransitionFixture'); + +const InvalidStateTransitionTypeError = require('@dashevo/dpp/lib/errors/consensus/basic/stateTransition/InvalidStateTransitionTypeError'); +const InvalidStateTransitionError = require('@dashevo/dpp/lib/stateTransition/errors/InvalidStateTransitionError'); +const BalanceNotEnoughError = require('@dashevo/dpp/lib/errors/consensus/fee/BalanceIsNotEnoughError'); +const ValidatorResult = require('@dashevo/dpp/lib/validation/ValidationResult'); + +const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes'); +const IdentityNotFoundError = require('@dashevo/dpp/lib/errors/consensus/signature/IdentityNotFoundError'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const unserializeStateTransitionFactory = require('../../../../../lib/abci/handlers/stateTransition/unserializeStateTransitionFactory'); +const LoggerMock = require('../../../../../lib/test/mock/LoggerMock'); +const DPPValidationAbciError = require('../../../../../lib/abci/errors/DPPValidationAbciError'); +const InvalidArgumentAbciError = require('../../../../../lib/abci/errors/InvalidArgumentAbciError'); + +describe('unserializeStateTransitionFactory', () => { + let unserializeStateTransition; + let stateTransitionFixture; + let dppMock; + let noopLoggerMock; + let stateTransition; + + beforeEach(function beforeEach() { + stateTransition = getIdentityCreateTransitionFixture(); + stateTransitionFixture = stateTransition.toBuffer(); + + dppMock = { + dispose: this.sinon.stub(), + stateTransition: { + createFromBuffer: this.sinon.stub(), + validateFee: this.sinon.stub(), + validateSignature: this.sinon.stub(), + validateState: this.sinon.stub(), + apply: this.sinon.stub(), + }, + }; + + dppMock.stateTransition.validateSignature.resolves(new ValidatorResult()); + + noopLoggerMock = new LoggerMock(this.sinon); + + unserializeStateTransition = unserializeStateTransitionFactory(dppMock, noopLoggerMock); + }); + + it('should throw InvalidArgumentAbciError if State Transition is not specified', async () => { + try { + await unserializeStateTransition(); + + expect.fail('should throw InvalidArgumentAbciError error'); + } catch (e) { + expect(e).to.be.instanceOf(InvalidArgumentAbciError); + expect(e.getMessage()).to.equal('State Transition is not specified'); + expect(e.getCode()).to.equal(GrpcErrorCodes.INVALID_ARGUMENT); + + expect(dppMock.stateTransition.validateFee).to.not.be.called(); + } + }); + + it('should throw InvalidArgumentAbciError if State Transition is invalid', async () => { + const dppError = new InvalidStateTransitionTypeError(-1); + const error = new InvalidStateTransitionError( + [dppError], + stateTransitionFixture, + ); + + dppMock.stateTransition.createFromBuffer.throws(error); + + try { + await unserializeStateTransition(stateTransitionFixture); + + expect.fail('should throw InvalidArgumentAbciError error'); + } catch (e) { + expect(e).to.be.instanceOf(DPPValidationAbciError); + expect(e.getCode()).to.equal(dppError.getCode()); + expect(e.getData()).to.deep.equal({ + arguments: [-1], + }); + + expect(dppMock.stateTransition.createFromBuffer).to.be.calledOnce(); + expect(dppMock.stateTransition.validateFee).to.not.be.called(); + } + }); + + it('should throw the error from createFromBuffer if throws not InvalidStateTransitionError', async () => { + const error = new Error('Custom error'); + dppMock.stateTransition.createFromBuffer.throws(error); + + try { + await unserializeStateTransition(stateTransitionFixture); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.equal(error); + + expect(dppMock.stateTransition.createFromBuffer).to.be.calledOnce(); + expect(dppMock.stateTransition.validateFee).to.not.be.called(); + } + }); + + it('should throw InsufficientFundsError in case if identity has not enough credits', async () => { + const balance = 1000; + const fee = 1; + const error = new BalanceNotEnoughError(balance, fee); + + dppMock.stateTransition.validateFee.resolves( + new ValidatorResult([error]), + ); + + dppMock.stateTransition.createFromBuffer.resolves(stateTransition); + + try { + await unserializeStateTransition(stateTransitionFixture); + + expect.fail('should throw an InsufficientFundsError'); + } catch (e) { + expect(e).to.be.instanceOf(DPPValidationAbciError); + expect(e.getCode()).to.equal(error.getCode()); + expect(e.getData()).to.deep.equal({ + arguments: [balance, fee], + }); + + expect(dppMock.stateTransition.createFromBuffer).to.be.calledOnce(); + expect(dppMock.stateTransition.validateFee).to.be.calledOnce(); + } + }); + + it('should return invalid result if validateSignature failed', async () => { + const identity = getIdentityFixture(); + const error = new IdentityNotFoundError(identity.getId()); + + dppMock.stateTransition.validateSignature.resolves( + new ValidatorResult([error]), + ); + + try { + await unserializeStateTransition(stateTransitionFixture); + + expect.fail('should throw an InsufficientFundsError'); + } catch (e) { + expect(e).to.be.instanceOf(DPPValidationAbciError); + expect(e.getCode()).to.equal(error.getCode()); + expect(e.getData()).to.deep.equal({ + arguments: [identity.getId()], + }); + + expect(dppMock.stateTransition.createFromBuffer).to.be.calledOnce(); + expect(dppMock.stateTransition.validateFee).to.have.not.been.called(); + } + }); + + it('should return stateTransition', async () => { + dppMock.stateTransition.createFromBuffer.resolves(stateTransition); + + dppMock.stateTransition.validateFee.resolves(new ValidatorResult()); + + const result = await unserializeStateTransition(stateTransitionFixture); + + expect(result).to.deep.equal(stateTransition); + + // TODO: Enable fee validation when RS Drive is ready + // expect(dppMock.stateTransition.validateFee).to.be.calledOnceWith(stateTransition); + // expect(dppMock.stateTransition.validateState).to.be.calledOnceWithExactly(stateTransition); + // expect(dppMock.stateTransition.apply).to.be.calledOnceWithExactly(stateTransition); + }); + + it('should use provided logger', async function it() { + const loggerMock = new LoggerMock(this.sinon); + + const balance = 1000; + const fee = 1000; + const error = new BalanceNotEnoughError(balance, fee); + + dppMock.stateTransition.createFromBuffer.resolves(stateTransition); + + dppMock.stateTransition.validateFee.resolves( + new ValidatorResult([error]), + ); + + try { + await unserializeStateTransition(stateTransitionFixture, { logger: loggerMock }); + + expect.fail('should throw an InsufficientFundsError'); + } catch (e) { + expect(e).to.be.instanceOf(DPPValidationAbciError); + expect(e.getCode()).to.equal(error.getCode()); + expect(e.getData()).to.deep.equal({ + arguments: [balance, fee], + }); + + expect(dppMock.stateTransition.createFromBuffer).to.be.calledOnce(); + expect(dppMock.stateTransition.validateFee).to.be.calledOnce(); + + expect(noopLoggerMock.info).to.not.have.been.called(); + expect(noopLoggerMock.debug).to.not.have.been.called(); + + expect(loggerMock.info).to.have.been.calledOnceWithExactly( + 'Insufficient funds to process state transition', + ); + expect(loggerMock.debug).to.have.been.calledOnceWithExactly({ + consensusError: error, + }); + } + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/validator/createValidatorSetUpdate.spec.js b/packages/js-drive/test/unit/abci/handlers/validator/createValidatorSetUpdate.spec.js new file mode 100644 index 00000000000..30c170d277f --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/validator/createValidatorSetUpdate.spec.js @@ -0,0 +1,57 @@ +const { + tendermint: { + abci: { + ValidatorSetUpdate, + }, + }, +} = require('@dashevo/abci/types'); +const { expect } = require('chai'); + +const createValidatorSetUpdate = require('../../../../../lib/abci/handlers/validator/createValidatorSetUpdate'); +const ValidatorNetworkInfo = require('../../../../../lib/validator/ValidatorNetworkInfo'); + +describe('createValidatorSetUpdate', () => { + let validatorSetMock; + let validatorMock; + let quorumHash; + let quorumPublicKey; + + beforeEach(function beforeEach() { + validatorMock = { + getPublicKeyShare: this.sinon.stub(), + getVotingPower: this.sinon.stub(), + getProTxHash: this.sinon.stub(), + getNetworkInfo: this.sinon.stub(), + }; + + validatorMock.getVotingPower.returns(Buffer.alloc(2, 32)); + validatorMock.getProTxHash.returns(Buffer.alloc(3, 32)); + validatorMock.getNetworkInfo.returns(new ValidatorNetworkInfo('192.168.65.2', 26656)); + + validatorSetMock = { + getValidators: this.sinon.stub(), + getQuorum: this.sinon.stub(), + }; + + quorumHash = Buffer.alloc(1, 32).toString('hex'); + quorumPublicKey = 'a7e75af9dd4d868a41ad2f5a5b021d653e31084261724fb40ae2f1b1c31c778d3b9464502d599cf6720723ec5c68b59d'; + + validatorMock.getPublicKeyShare.returns( + Buffer.from(quorumPublicKey, 'hex'), + ); + + validatorSetMock.getValidators.returns([validatorMock]); + validatorSetMock.getQuorum.returns({ + quorumHash, + quorumPublicKey, + }); + }); + + it('should create ValidatorSetUpdate object from specified ValidatorSet instance', () => { + const result = createValidatorSetUpdate(validatorSetMock); + + expect(result).to.be.an.instanceOf(ValidatorSetUpdate); + + // TODO: check something else? + }); +}); diff --git a/packages/js-drive/test/unit/abci/handlers/verifyVoteExtensionHandlerFactory.spec.js b/packages/js-drive/test/unit/abci/handlers/verifyVoteExtensionHandlerFactory.spec.js new file mode 100644 index 00000000000..ab558ad9639 --- /dev/null +++ b/packages/js-drive/test/unit/abci/handlers/verifyVoteExtensionHandlerFactory.spec.js @@ -0,0 +1,98 @@ +const { + tendermint: { + abci: { + ResponseVerifyVoteExtension, + }, + types: { + VoteExtensionType, + }, + }, +} = require('@dashevo/abci/types'); +const verifyVoteExtensionHandlerFactory = require('../../../../lib/abci/handlers/verifyVoteExtensionHandlerFactory'); +const BlockExecutionContextMock = require('../../../../lib/test/mock/BlockExecutionContextMock'); +const LoggerMock = require('../../../../lib/test/mock/LoggerMock'); + +describe('verifyVoteExtensionHandlerFactory', () => { + let verifyVoteExtensionHandler; + let proposalBlockExecutionContextMock; + let unsignedWithdrawalTransactionsMapMock; + + beforeEach(function beforeEach() { + proposalBlockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + const loggerMock = new LoggerMock(this.sinon); + proposalBlockExecutionContextMock.getContextLogger.returns(loggerMock); + + unsignedWithdrawalTransactionsMapMock = {}; + proposalBlockExecutionContextMock.getWithdrawalTransactionsMap.returns( + unsignedWithdrawalTransactionsMapMock, + ); + + verifyVoteExtensionHandler = verifyVoteExtensionHandlerFactory( + proposalBlockExecutionContextMock, + ); + }); + + it('should return ResponseVerifyVoteExtension with REJECT status if vote extensions length not match', async () => { + const voteExtensions = [ + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 1) }, + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 2) }, + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 3) }, + ]; + + const unsignedWithdrawalTransactionsMap = { + [Buffer.alloc(32, 1).toString('hex')]: undefined, + [Buffer.alloc(32, 2).toString('hex')]: undefined, + }; + + proposalBlockExecutionContextMock.getWithdrawalTransactionsMap.returns( + unsignedWithdrawalTransactionsMap, + ); + + const result = await verifyVoteExtensionHandler({ voteExtensions }); + + expect(result).to.be.an.instanceOf(ResponseVerifyVoteExtension); + expect(result.status).to.equal(2); + }); + + it('should return ResponseVerifyVoteExtension with REJECT status if vote extension is missing', async () => { + const voteExtensions = [ + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 1) }, + ]; + + const unsignedWithdrawalTransactionsMap = { + [Buffer.alloc(32, 1).toString('hex')]: undefined, + [Buffer.alloc(32, 2).toString('hex')]: undefined, + }; + + proposalBlockExecutionContextMock.getWithdrawalTransactionsMap.returns( + unsignedWithdrawalTransactionsMap, + ); + + const result = await verifyVoteExtensionHandler({ voteExtensions }); + + expect(result).to.be.an.instanceOf(ResponseVerifyVoteExtension); + expect(result.status).to.equal(2); + }); + + it('should return ACCEPT if everything is fine', async () => { + const voteExtensions = [ + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 1) }, + { type: VoteExtensionType.THRESHOLD_RECOVER, extension: Buffer.alloc(32, 2) }, + ]; + + const unsignedWithdrawalTransactionsMap = { + [Buffer.alloc(32, 1).toString('hex')]: undefined, + [Buffer.alloc(32, 2).toString('hex')]: undefined, + }; + + proposalBlockExecutionContextMock.getWithdrawalTransactionsMap.returns( + unsignedWithdrawalTransactionsMap, + ); + + const result = await verifyVoteExtensionHandler({ voteExtensions }); + + expect(result).to.be.an.instanceOf(ResponseVerifyVoteExtension); + expect(result.status).to.equal(1); + }); +}); diff --git a/packages/js-drive/test/unit/blockExecution/BlockExecutionContext.spec.js b/packages/js-drive/test/unit/blockExecution/BlockExecutionContext.spec.js new file mode 100644 index 00000000000..96bb1b8fd34 --- /dev/null +++ b/packages/js-drive/test/unit/blockExecution/BlockExecutionContext.spec.js @@ -0,0 +1,427 @@ +const { + tendermint: { + abci: { + CommitInfo, + }, + version: { + Consensus, + }, + }, +} = require('@dashevo/abci/types'); + +const Long = require('long'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const BlockExecutionContext = require('../../../lib/blockExecution/BlockExecutionContext'); +const getBlockExecutionContextObjectFixture = require('../../../lib/test/fixtures/getBlockExecutionContextObjectFixture'); + +describe('BlockExecutionContext', () => { + let blockExecutionContext; + let dataContract; + let lastCommitInfo; + let logger; + let plainObject; + let height; + let coreChainLockedHeight; + let version; + let epochInfo; + let timeMs; + let prepareProposalResult; + let proposedAppVersion; + + beforeEach(() => { + blockExecutionContext = new BlockExecutionContext(); + dataContract = getDataContractFixture(); + delete dataContract.entropy; + + plainObject = getBlockExecutionContextObjectFixture(dataContract); + + lastCommitInfo = CommitInfo.fromObject(plainObject.lastCommitInfo); + + logger = plainObject.contextLogger; + height = Long.fromNumber(plainObject.height); + proposedAppVersion = Long.fromNumber(plainObject.proposedAppVersion); + coreChainLockedHeight = plainObject.coreChainLockedHeight; + version = Consensus.fromObject(plainObject.version); + epochInfo = plainObject.epochInfo; + timeMs = plainObject.timeMs; + prepareProposalResult = plainObject.prepareProposalResult; + }); + + describe('#addDataContract', () => { + it('should add a Data Contract', async () => { + expect(blockExecutionContext.getDataContracts()).to.have.lengthOf(0); + + blockExecutionContext.addDataContract(dataContract); + const contracts = blockExecutionContext.getDataContracts(); + + expect(contracts).to.have.lengthOf(1); + expect(contracts[0]).to.deep.equal(dataContract); + }); + }); + + describe('#hasDataContract', () => { + it('should respond with false if data contract with specified ID is not present', async () => { + const result = blockExecutionContext.hasDataContract(dataContract.getId()); + + expect(result).to.be.false(); + }); + + it('should respond with true if data contract with specified ID is present', async () => { + blockExecutionContext.addDataContract(dataContract); + + const result = blockExecutionContext.hasDataContract(dataContract.getId()); + + expect(result).to.be.true(); + }); + }); + + describe('#getDataContracts', () => { + it('should get data contracts', async () => { + blockExecutionContext.addDataContract(dataContract); + blockExecutionContext.addDataContract(dataContract); + + const contracts = blockExecutionContext.getDataContracts(); + + expect(contracts).to.have.lengthOf(2); + expect(contracts[0]).to.deep.equal(dataContract); + expect(contracts[1]).to.deep.equal(dataContract); + }); + }); + + describe('#reset', () => { + it('should reset state', () => { + blockExecutionContext.addDataContract(dataContract); + + expect(blockExecutionContext.getDataContracts()).to.have.lengthOf(1); + + blockExecutionContext.reset(); + + expect(blockExecutionContext.getDataContracts()).to.have.lengthOf(0); + + expect(blockExecutionContext.getHeight()).to.be.null(); + expect(blockExecutionContext.getCoreChainLockedHeight()).to.be.null(); + expect(blockExecutionContext.getVersion()).to.be.null(); + expect(blockExecutionContext.getTimeMs()).to.be.null(); + expect(blockExecutionContext.getLastCommitInfo()).to.be.null(); + expect(blockExecutionContext.getProposedAppVersion()).to.be.null(); + expect(blockExecutionContext.getWithdrawalTransactionsMap()).to.deep.equal({}); + }); + }); + + describe('#setCoreChainLockedHeight', () => { + it('should set coreChainLockedHeight', async () => { + const result = blockExecutionContext.setCoreChainLockedHeight(coreChainLockedHeight); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.coreChainLockedHeight).to.deep.equal(coreChainLockedHeight); + }); + }); + + describe('#getCoreChainLockedHeight', () => { + it('should get coreChainLockedHeight', async () => { + blockExecutionContext.coreChainLockedHeight = coreChainLockedHeight; + + expect(blockExecutionContext.getCoreChainLockedHeight()).to.deep.equal(coreChainLockedHeight); + }); + }); + + describe('#setHeight', () => { + it('should set height', async () => { + const result = blockExecutionContext.setHeight(height); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.height).to.deep.equal(height); + }); + }); + + describe('#getHeight', () => { + it('should get height', async () => { + blockExecutionContext.height = height; + + expect(blockExecutionContext.getHeight()).to.deep.equal(height); + }); + }); + + describe('#setProposedAppVersion', () => { + it('should set proposed app version', async () => { + const result = blockExecutionContext.setProposedAppVersion(proposedAppVersion); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.proposedAppVersion).to.deep.equal(proposedAppVersion); + }); + }); + + describe('#getProposedAppVersion', () => { + it('should get proposed app version', async () => { + blockExecutionContext.proposedAppVersion = proposedAppVersion; + + expect(blockExecutionContext.getProposedAppVersion()).to.deep.equal(proposedAppVersion); + }); + }); + + describe('#setVersion', () => { + it('should set version', async () => { + const result = blockExecutionContext.setVersion(version); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.version).to.deep.equal(version); + }); + }); + + describe('#getVersion', () => { + it('should get version', async () => { + blockExecutionContext.version = version; + + expect(blockExecutionContext.getVersion()).to.deep.equal(version); + }); + }); + + describe('#setLastCommitInfo', () => { + it('should set lastCommitInfo', async () => { + const result = blockExecutionContext.setLastCommitInfo(lastCommitInfo); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.lastCommitInfo).to.deep.equal(lastCommitInfo); + }); + }); + + describe('#getLastCommitInfo', () => { + it('should get lastCommitInfo', async () => { + blockExecutionContext.lastCommitInfo = lastCommitInfo; + + expect(blockExecutionContext.getLastCommitInfo()).to.deep.equal(lastCommitInfo); + }); + }); + + describe('#setWithdrawalTransactionsMap', () => { + it('should set withdrawalTransactionsMap', async () => { + const result = blockExecutionContext.setWithdrawalTransactionsMap( + plainObject.withdrawalTransactionsMap, + ); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.withdrawalTransactionsMap).to.deep.equal( + plainObject.withdrawalTransactionsMap, + ); + }); + }); + + describe('#getWithdrawalTransactionsMap', () => { + it('should get withdrawalTransactionsMap', async () => { + blockExecutionContext.withdrawalTransactionsMap = plainObject.withdrawalTransactionsMap; + + expect(blockExecutionContext.getWithdrawalTransactionsMap()).to.deep.equal( + plainObject.withdrawalTransactionsMap, + ); + }); + }); + + describe('#setRound', () => { + it('should set round', async () => { + const result = blockExecutionContext.setRound( + plainObject.round, + ); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.round).to.deep.equal( + plainObject.round, + ); + }); + }); + + describe('#getRound', () => { + it('should get round', async () => { + blockExecutionContext.round = plainObject.round; + + expect(blockExecutionContext.getRound()).to.deep.equal( + plainObject.round, + ); + }); + }); + + describe('#setPrepareProposalResult', () => { + it('should set PrepareProposal result', async () => { + const result = blockExecutionContext.setPrepareProposalResult( + plainObject.prepareProposalResult, + ); + + expect(result).to.equal(blockExecutionContext); + + expect(blockExecutionContext.prepareProposalResult).to.deep.equal( + plainObject.prepareProposalResult, + ); + }); + }); + + describe('#getPrepareProposalResult', () => { + it('should get PrepareProposal result', async () => { + blockExecutionContext.prepareProposalResult = plainObject.prepareProposalResult; + + expect(blockExecutionContext.getPrepareProposalResult()).to.deep.equal( + plainObject.prepareProposalResult, + ); + }); + }); + + describe('#setTimeMs', () => { + it('should set time', async () => { + blockExecutionContext.setTimeMs(timeMs); + + expect(blockExecutionContext.timeMs).to.deep.equal(timeMs); + }); + }); + + describe('#getTimeMs', () => { + it('should get time', async () => { + blockExecutionContext.timeMs = timeMs; + + expect(blockExecutionContext.getTimeMs()).to.deep.equal(timeMs); + }); + }); + + describe('#setEpochInfo', () => { + it('should set epoch info'); + }); + + describe('#getEpochInfo', () => { + it('should return epoch info'); + }); + + describe('#populate', () => { + it('should populate instance from another instance', () => { + const anotherBlockExecutionContext = new BlockExecutionContext(); + + anotherBlockExecutionContext.dataContracts = [dataContract]; + anotherBlockExecutionContext.lastCommitInfo = lastCommitInfo; + anotherBlockExecutionContext.height = height; + anotherBlockExecutionContext.version = version; + anotherBlockExecutionContext.coreChainLockedHeight = coreChainLockedHeight; + anotherBlockExecutionContext.contextLogger = logger; + anotherBlockExecutionContext.withdrawalTransactionsMap = plainObject + .withdrawalTransactionsMap; + anotherBlockExecutionContext.epochInfo = epochInfo; + anotherBlockExecutionContext.timeMs = timeMs; + + blockExecutionContext.populate(anotherBlockExecutionContext); + + expect(blockExecutionContext.dataContracts).to.equal( + anotherBlockExecutionContext.dataContracts, + ); + expect(blockExecutionContext.lastCommitInfo).to.equal( + anotherBlockExecutionContext.lastCommitInfo, + ); + expect(blockExecutionContext.height).to.equal( + anotherBlockExecutionContext.height, + ); + expect(blockExecutionContext.version).to.equal( + anotherBlockExecutionContext.version, + ); + expect(blockExecutionContext.coreChainLockedHeight).to.equal( + anotherBlockExecutionContext.coreChainLockedHeight, + ); + expect(blockExecutionContext.contextLogger).to.equal( + anotherBlockExecutionContext.contextLogger, + ); + expect(blockExecutionContext.withdrawalTransactionsMap).to.equal( + anotherBlockExecutionContext.withdrawalTransactionsMap, + ); + expect(blockExecutionContext.epochInfo).to.equal( + anotherBlockExecutionContext.epochInfo, + ); + expect(blockExecutionContext.timeMs).to.equal( + anotherBlockExecutionContext.timeMs, + ); + }); + }); + + describe('#toObject', () => { + it('should return a plain object', () => { + blockExecutionContext.dataContracts = [dataContract]; + blockExecutionContext.lastCommitInfo = lastCommitInfo; + blockExecutionContext.height = height; + blockExecutionContext.coreChainLockedHeight = coreChainLockedHeight; + blockExecutionContext.version = version; + blockExecutionContext.contextLogger = logger; + blockExecutionContext.epochInfo = epochInfo; + blockExecutionContext.timeMs = timeMs; + blockExecutionContext.withdrawalTransactionsMap = plainObject.withdrawalTransactionsMap; + blockExecutionContext.round = plainObject.round; + blockExecutionContext.prepareProposalResult = plainObject.prepareProposalResult; + blockExecutionContext.proposedAppVersion = proposedAppVersion; + + expect(blockExecutionContext.toObject()).to.deep.equal(plainObject); + }); + + it('should skipContextLogger if the option passed', () => { + blockExecutionContext.dataContracts = [dataContract]; + blockExecutionContext.lastCommitInfo = lastCommitInfo; + blockExecutionContext.height = height; + blockExecutionContext.coreChainLockedHeight = coreChainLockedHeight; + blockExecutionContext.version = version; + blockExecutionContext.contextLogger = logger; + blockExecutionContext.withdrawalTransactionsMap = plainObject.withdrawalTransactionsMap; + blockExecutionContext.round = plainObject.round; + blockExecutionContext.epochInfo = epochInfo; + blockExecutionContext.timeMs = timeMs; + blockExecutionContext.prepareProposalResult = prepareProposalResult; + blockExecutionContext.proposedAppVersion = proposedAppVersion; + + const result = blockExecutionContext.toObject({ skipContextLogger: true }); + + delete plainObject.contextLogger; + + expect(result).to.deep.equal(plainObject); + }); + + it('should skipPrepareProposalResult if the option passed', () => { + blockExecutionContext.dataContracts = [dataContract]; + blockExecutionContext.lastCommitInfo = lastCommitInfo; + blockExecutionContext.height = height; + blockExecutionContext.coreChainLockedHeight = coreChainLockedHeight; + blockExecutionContext.version = version; + blockExecutionContext.contextLogger = logger; + blockExecutionContext.withdrawalTransactionsMap = plainObject.withdrawalTransactionsMap; + blockExecutionContext.round = plainObject.round; + blockExecutionContext.epochInfo = epochInfo; + blockExecutionContext.timeMs = timeMs; + blockExecutionContext.proposedAppVersion = proposedAppVersion; + + const result = blockExecutionContext.toObject({ skipPrepareProposalResult: true }); + + delete plainObject.prepareProposalResult; + + expect(result).to.deep.equal(plainObject); + }); + }); + + describe('#fromObject', () => { + it('should populate instance from a plain object', () => { + blockExecutionContext.fromObject(plainObject); + + if (blockExecutionContext.dataContracts[0].$defs === undefined) { + blockExecutionContext.dataContracts[0].$defs = {}; + } + + expect(blockExecutionContext.dataContracts).to.have.deep.members( + [dataContract], + ); + expect(blockExecutionContext.lastCommitInfo).to.deep.equal(lastCommitInfo); + expect(blockExecutionContext.height).to.deep.equal(height); + expect(blockExecutionContext.version).to.deep.equal(version); + expect(blockExecutionContext.coreChainLockedHeight).to.deep.equal(coreChainLockedHeight); + expect(blockExecutionContext.contextLogger).to.equal(logger); + expect(blockExecutionContext.withdrawalTransactionsMap).to.deep.equal( + plainObject.withdrawalTransactionsMap, + ); + expect(blockExecutionContext.timeMs).to.equal(timeMs); + }); + }); +}); diff --git a/packages/js-drive/test/unit/core/LatestCoreChainLock.spec.js b/packages/js-drive/test/unit/core/LatestCoreChainLock.spec.js new file mode 100644 index 00000000000..2ab24a6db1e --- /dev/null +++ b/packages/js-drive/test/unit/core/LatestCoreChainLock.spec.js @@ -0,0 +1,44 @@ +const LatestCoreChainLock = require('../../../lib/core/LatestCoreChainLock'); + +describe('LatestCoreChainLock', () => { + describe('#constructor', () => { + it('should instantiate', () => { + const latestCoreChainLock = new LatestCoreChainLock(); + expect(latestCoreChainLock.chainLock).to.equal(undefined); + const latestCoreChainLockWithValue = new LatestCoreChainLock('someValue'); + expect(latestCoreChainLockWithValue.chainLock).to.equal('someValue'); + }); + }); + + describe('#update', () => { + it('should update', () => { + const latestCoreChainLock = new LatestCoreChainLock(); + latestCoreChainLock.update('someValue'); + expect(latestCoreChainLock.chainLock).to.equal('someValue'); + }); + + it('should emit updated chainLock', (done) => { + const chainLock = 'someValue'; + const latestCoreChainLock = new LatestCoreChainLock(); + + latestCoreChainLock.on(LatestCoreChainLock.EVENTS.update, (data) => { + expect(data).to.equal(chainLock); + + done(); + }); + + latestCoreChainLock.update(chainLock); + }); + }); + + describe('#getChainLock', () => { + it('should return chainLock', async () => { + const chainLock = 'someValue'; + + const latestCoreChainLock = new LatestCoreChainLock(); + latestCoreChainLock.update(chainLock); + + expect(latestCoreChainLock.getChainLock()).to.equal(chainLock); + }); + }); +}); diff --git a/packages/js-drive/test/unit/core/ensureBlock.spec.js b/packages/js-drive/test/unit/core/ensureBlock.spec.js new file mode 100644 index 00000000000..a9b86156f94 --- /dev/null +++ b/packages/js-drive/test/unit/core/ensureBlock.spec.js @@ -0,0 +1,66 @@ +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +chai.should(); + +const EventEmitter = require('events'); +const ZMQClient = require('../../../lib/core/ZmqClient'); +const ensureBlock = require('../../../lib/core/ensureBlock'); + +describe('ensureBlock', () => { + const hash = '00000'; + const otherHash = '00001'; + const socketClient = new EventEmitter(); + let rpcClient; + + beforeEach(function beforeEach() { + socketClient.subscribe = this.sinon.stub(); + + rpcClient = { + getBlock: this.sinon.stub().resolves(true), + }; + }); + + it('should ensure a block exist before returning promise', async () => { + await ensureBlock(socketClient, rpcClient, hash); + + expect(rpcClient.getBlock).to.be.calledOnceWithExactly(hash); + }); + + it('should wait for block if not found before returning promise', (done) => { + const err = new Error(); + err.code = -5; + err.message = 'Block not found'; + + rpcClient.getBlock.throws(err); + + ensureBlock(socketClient, rpcClient, hash).then(done); + + setImmediate(() => { + socketClient.emit(ZMQClient.TOPICS.hashblock, otherHash); + }); + + setImmediate(() => { + socketClient.emit(ZMQClient.TOPICS.hashblock, hash); + }); + + expect(rpcClient.getBlock).to.be.calledOnceWithExactly(hash); + }); + + it('should throw on unexpected error', async () => { + const err = new Error(); + err.code = -6; + err.message = 'Another error'; + + rpcClient.getBlock.throws(err); + + try { + await ensureBlock(socketClient, rpcClient, hash); + expect.fail('Internal error must be thrown'); + } catch (e) { + expect(e).to.equal(err); + expect(rpcClient.getBlock).to.be.calledOnceWithExactly(hash); + } + }); +}); diff --git a/packages/js-drive/test/unit/core/getRandomQuorumFactory.spec.js b/packages/js-drive/test/unit/core/getRandomQuorumFactory.spec.js new file mode 100644 index 00000000000..d75a5c2aea4 --- /dev/null +++ b/packages/js-drive/test/unit/core/getRandomQuorumFactory.spec.js @@ -0,0 +1,196 @@ +const { expect } = require('chai'); +const getRandomQuorumFactory = require('../../../lib/core/getRandomQuorumFactory'); + +describe('getRandomQuorumFactory', () => { + let smlMock; + let quorumType; + let getRandomQuorum; + let coreRpcClientMock; + let randomQuorum; + let coreHeight; + + beforeEach(function beforeEach() { + smlMock = { + getQuorumsOfType: this.sinon.stub(), + getQuorum: this.sinon.stub(), + quorumList: [], + blockHash: '0'.repeat(32), + }; + + coreHeight = 690; + + quorumType = 102; + + smlMock.getQuorum.resolves(randomQuorum); + + smlMock.getQuorumsOfType.returns([ + { + quorumHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + validMembersCount: 90, + }, + { + quorumHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + getAllQuorumMembers: 90, + }, + ]); + + const quorumListExtended = { + llmq_test: [ + { + '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c': { + creationHeight: 672, + minedBlockHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + }, + }, + { + '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce': { + creationHeight: 672, + minedBlockHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + }, + }, + ], + llmq_test_instantsend: [ + { + '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c': { + creationHeight: 672, + minedBlockHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + }, + }, + { + '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce': { + creationHeight: 672, + minedBlockHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + }, + }, + ], + llmq_test_v17: [ + { + '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c': { + creationHeight: 672, + minedBlockHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + }, + }, + { + '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce': { + creationHeight: 672, + minedBlockHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + }, + }, + ], + }; + + coreRpcClientMock = { + quorum: this.sinon.stub().resolves({ result: quorumListExtended }), + }; + + getRandomQuorum = getRandomQuorumFactory(coreRpcClientMock); + }); + + it('should return random quorum based on entropy', async () => { + const result = await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect(smlMock.getQuorumsOfType).to.have.been.calledOnceWithExactly(quorumType); + expect(smlMock.getQuorum).to.have.been.calledOnceWithExactly( + quorumType, '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + ); + expect(result).to.equals(randomQuorum); + }); + + it('should throw an error if SML does not contain any quorums', async () => { + smlMock.getQuorumsOfType.returns([]); + + try { + await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect.fail('should throw an error'); + } catch (e) { + expect(e.message).to.be.equal(`SML at block ${'0'.repeat(32)} contains no quorums of any type`); + } + }); + + it('should throw an error if SML contains quorums that differ from the specified quorum type', async () => { + smlMock.getQuorumsOfType.returns([]); + smlMock.quorumList = [{ llmqType: 999 }]; + + try { + await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect.fail('should throw an error'); + } catch (e) { + expect(e.message).to.be.equal(`SML at block ${'0'.repeat(32)} contains no quorums of type ${quorumType}, but contains entries for types 999. Please check the Drive configuration`); + } + }); + + it('should filter quorums by minQuorumMembers', async () => { + smlMock.getQuorumsOfType.returns([ + { + quorumHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + validMembersCount: 90, + }, + { + quorumHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + validMembersCount: 89, + }, + ]); + + const result = await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect(smlMock.getQuorumsOfType).to.have.been.calledOnceWithExactly(quorumType); + expect(smlMock.getQuorum).to.have.been.calledOnceWithExactly( + quorumType, '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + ); + expect(result).to.equals(randomQuorum); + }); + + it('should filter quorums by ttl', async () => { + coreRpcClientMock.quorum.resolves({ + result: { + llmq_test_v17: [ + { + '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c': { + creationHeight: 100, + minedBlockHash: '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + }, + }, + { + '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce': { + creationHeight: 672, + minedBlockHash: '251fae1f7fe89d38c4ce71781685c96291cc3852e9586c8eb6d5a71b73d6285c', + }, + }, + ], + }, + }); + + const result = await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect(smlMock.getQuorumsOfType).to.have.been.calledOnceWithExactly(quorumType); + expect(smlMock.getQuorum).to.have.been.calledOnceWithExactly( + quorumType, '1af5ffbdb862a18b454106b6b99e30bb683fa5bab8278b19a6e91bf742405cce', + ); + + expect(coreRpcClientMock.quorum).to.be.calledOnceWithExactly('listextended', coreHeight); + expect(result).to.equals(randomQuorum); + }); + + it('should choose from all quorums if filtered list is empty', async () => { + smlMock.getQuorumsOfType.returns([ + { + quorumHash: Buffer.alloc(1, 64).toString('hex'), + validMembersCount: 10, + }, + { + quorumHash: Buffer.alloc(1, 32).toString('hex'), + validMembersCount: 10, + }, + ]); + + const result = await getRandomQuorum(smlMock, quorumType, Buffer.alloc(1), coreHeight); + + expect(smlMock.getQuorumsOfType).to.have.been.calledOnceWithExactly(quorumType); + expect(smlMock.getQuorum).to.have.been.calledOnceWithExactly( + quorumType, '20', + ); + expect(result).to.equals(randomQuorum); + }); +}); diff --git a/packages/js-drive/test/unit/core/updateSimplifiedMasternodeListFactory.spec.js b/packages/js-drive/test/unit/core/updateSimplifiedMasternodeListFactory.spec.js new file mode 100644 index 00000000000..b058e0743c2 --- /dev/null +++ b/packages/js-drive/test/unit/core/updateSimplifiedMasternodeListFactory.spec.js @@ -0,0 +1,195 @@ +const SimplifiedMNListDiff = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListDiff'); +const { expect } = require('chai'); +const updateSimplifiedMasternodeListFactory = require('../../../lib/core/updateSimplifiedMasternodeListFactory'); +const NotEnoughBlocksForValidSMLError = require('../../../lib/core/errors/NotEnoughBlocksForValidSMLError'); +const LoggerMock = require('../../../lib/test/mock/LoggerMock'); + +describe('updateSimplifiedMasternodeListFactory', () => { + let updateSimplifiedMasternodeList; + let coreRpcClientMock; + let network; + let smlMaxListsLimit; + let simplifiedMasternodeListMock; + let rawDiff; + let coreHeight; + + beforeEach(function beforeEach() { + network = 'regtest'; + + rawDiff = { + baseBlockHash: '644bd9dcbc0537026af6d31181570f934d868f121c55513009bb36f509ec816e', + blockHash: '23beac1b700c4a49855a9653e036219384ac2fab7eeba2ec45b3e2d0063d1285', + cbTxMerkleTree: '03000000032f7f142e19bee0c595dac9f900695d1e428a4db70a805fda6c834cfec0de506a0d39baea39dbbaf9827a1f3b8f381a65ebcf4c2ef415025bc4d20afd372e680d12c226f084a6e28e421fbedff22b13aa1191d6a80744d104fa75ede12332467d0107', + cbTx: '03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502e9030101ffffffff01a2567a76070000001976a914f713c2fa5ef0e7c48f0d1b3ad2a79150037c72d788ac00000000460200e90300003fdbe53b9a4cd0b62284195cbd4f4c1655ebdd70e9117ed3c0e49c37bfce46060000000000000000000000000000000000000000000000000000000000000000', + deletedMNs: [], + mnList: [ + { + proRegTxHash: 'e57402007ca10454d77437d9c1156b1c4ff8af86d699c08e9a31dbd1dfe3c991', + confirmedHash: '0000000000000000000000000000000000000000000000000000000000000000', + service: '127.0.0.1:20001', + pubKeyOperator: '906d84cb88f532145d8838414f777b971c976ffcf8ccfc57413a13cf2f8a7750a92f9b997a5a741f1afa34d989f4312b', + votingAddress: 'ydC3Qkhq6qc1qgHD8PVSHyAB6t3NYa7aw4', + nType: 0, + isValid: true, + }, + ], + deletedQuorums: [], + newQuorums: [], + merkleRootMNList: '0646cebf379ce4c0d37e11e970ddeb55164c4fbd5c198422b6d04c9a3be5db3f', + merkleRootQuorums: '0000000000000000000000000000000000000000000000000000000000000000', + }; + + coreHeight = 84202; + + coreRpcClientMock = { + protx: this.sinon.stub(), + }; + + coreRpcClientMock.protx.resolves({ + result: rawDiff, + }); + + simplifiedMasternodeListMock = { + applyDiffs: this.sinon.stub(), + reset: this.sinon.stub(), + }; + + smlMaxListsLimit = 2; + + const loggerMock = new LoggerMock(this.sinon); + + updateSimplifiedMasternodeList = updateSimplifiedMasternodeListFactory( + coreRpcClientMock, + simplifiedMasternodeListMock, + smlMaxListsLimit, + network, + loggerMock, + ); + }); + + it('should throw error if not enough blocks for valid SML', async () => { + try { + await updateSimplifiedMasternodeList(smlMaxListsLimit); + + expect.fail('should throw NotEnoughBlocksForValidSMLError'); + } catch (e) { + expect(e).to.be.instanceOf(NotEnoughBlocksForValidSMLError); + expect(e.getBlockHeight()).to.be.equal(smlMaxListsLimit); + } + }); + + it('should obtain 16 latest diffs according to core height on first call', async () => { + const isUpdated = await updateSimplifiedMasternodeList(coreHeight); + + expect(isUpdated).to.be.true(); + + const proTxCallCount = coreHeight - (coreHeight - smlMaxListsLimit) + 1; + + expect(coreRpcClientMock.protx.callCount).to.equal(proTxCallCount); + + expect(coreRpcClientMock.protx.getCall(0).args).to.have.deep.members( + [ + 'diff', + 1, + (coreHeight - smlMaxListsLimit), + true, + ], + ); + + for (let i = 1; i < proTxCallCount; i++) { + expect(coreRpcClientMock.protx.getCall(i).args).to.have.deep.members( + [ + 'diff', + (coreHeight - smlMaxListsLimit) + (i - 1), + (coreHeight - smlMaxListsLimit) + (i - 1) + 1, + true, + ], + ); + } + + const smlDiffs = []; + for (let i = 0; i < proTxCallCount; i++) { + smlDiffs.push(new SimplifiedMNListDiff(rawDiff, network)); + } + + const argsDiffBuffers = simplifiedMasternodeListMock.applyDiffs.getCall(0).args[0].map( + (item) => item.toBuffer(), + ); + + const smlDiffBuffers = smlDiffs.map((item) => item.toBuffer()); + + expect(argsDiffBuffers).to.deep.equal(smlDiffBuffers); + }); + + it('should update diffs since last call and up to passed core height', async () => { + let isUpdated = await updateSimplifiedMasternodeList(coreHeight); + + expect(isUpdated).to.be.true(); + + isUpdated = await updateSimplifiedMasternodeList(coreHeight + 1); + + expect(isUpdated).to.be.true(); + + const proTxCallCount = smlMaxListsLimit + 2; + + expect(coreRpcClientMock.protx.callCount).to.equal(proTxCallCount); + + expect(coreRpcClientMock.protx.getCall(0).args).to.have.deep.members( + [ + 'diff', + 1, + (coreHeight - smlMaxListsLimit), + true, + ], + ); + + for (let i = 1; i < proTxCallCount; i++) { + expect(coreRpcClientMock.protx.getCall(i).args).to.have.deep.members( + [ + 'diff', + (coreHeight - smlMaxListsLimit) + (i - 1), + (coreHeight - smlMaxListsLimit) + (i - 1) + 1, + true, + ], + ); + } + + const simplifiedMNListDiffArray = []; + + for (let i = 0; i < proTxCallCount - 1; i++) { + simplifiedMNListDiffArray.push(new SimplifiedMNListDiff(rawDiff, network)); + } + + const argsDiffsBuffers = simplifiedMasternodeListMock.applyDiffs.getCall(0).args[0].map( + (item) => item.toBuffer(), + ); + + const smlDiffBuffers = simplifiedMNListDiffArray.map((item) => item.toBuffer()); + + expect(argsDiffsBuffers).to.deep.equal(smlDiffBuffers); + }); + + it('should not update more than 16 diffs', async () => { + let isUpdated = await updateSimplifiedMasternodeList(coreHeight); // 3 + + expect(isUpdated).to.be.true(); + + isUpdated = await updateSimplifiedMasternodeList(coreHeight + 10); // 3 + + expect(isUpdated).to.be.true(); + + const proTxCallCount = 3 + 3; + + expect(coreRpcClientMock.protx.callCount).to.equal(proTxCallCount); + }); + + it('should return false if SML was not updated', async () => { + let isUpdated = await updateSimplifiedMasternodeList(coreHeight); + + expect(isUpdated).to.be.true(); + + isUpdated = await updateSimplifiedMasternodeList(coreHeight); + + expect(isUpdated).to.be.false(); + }); +}); diff --git a/packages/js-drive/test/unit/core/waitForChainLockedHeightFactory.spec.js b/packages/js-drive/test/unit/core/waitForChainLockedHeightFactory.spec.js new file mode 100644 index 00000000000..6226680bf53 --- /dev/null +++ b/packages/js-drive/test/unit/core/waitForChainLockedHeightFactory.spec.js @@ -0,0 +1,63 @@ +const EventEmitter = require('events'); +const waitForChainLockedHeightFactory = require('../../../lib/core/waitForChainLockedHeightFactory'); +const MissingChainlockError = require('../../../lib/core/errors/MissingChainLockError'); +const LatestCoreChainLock = require('../../../lib/core/LatestCoreChainLock'); + +describe('waitForChainLockedHeightFactory', () => { + let waitForChainLockedHeight; + let latestCoreChainLockMock; + let chainLock; + let coreHeight; + + beforeEach(function beforeEach() { + coreHeight = 84202; + + chainLock = { + height: coreHeight, + signature: '0a43f1c3e5b3e8dbd670bca8d437dc25572f72d8e1e9be673e9ebbb606570307c3e5f5d073f7beb209dd7e0b8f96c751060ab3a7fb69a71d5ccab697b8cfa5a91038a6fecf76b7a827d75d17f01496302942aa5e2c7f4a48246efc8d3941bf6c', + }; + + latestCoreChainLockMock = new EventEmitter(); + latestCoreChainLockMock.getChainLock = this.sinon.stub().returns(chainLock); + + waitForChainLockedHeight = waitForChainLockedHeightFactory( + latestCoreChainLockMock, + ); + }); + + it('should throw MissingChainlockError if chainlock is empty', async () => { + latestCoreChainLockMock.getChainLock.returns(null); + + try { + await waitForChainLockedHeight(coreHeight); + + expect.fail(); + } catch (e) { + expect(e).to.be.an.instanceOf(MissingChainlockError); + } + }); + + it('should resolve promise if existing chainlock on the same height or higher', async () => { + latestCoreChainLockMock.getChainLock.returns(chainLock); + + await waitForChainLockedHeight(coreHeight); + }); + + it('should resolve when chainLock height to be equal or higher', (done) => { + coreHeight = chainLock.height + 1; + + waitForChainLockedHeight(coreHeight) + .then(() => { + expect(latestCoreChainLockMock.getChainLock).to.have.been.calledOnce(); + + done(); + }); + + setImmediate(() => { + latestCoreChainLockMock.emit(LatestCoreChainLock.EVENTS.update, { + ...chainLock, + height: chainLock.height + 1, + }); + }); + }); +}); diff --git a/packages/js-drive/test/unit/core/waitForCoreChainLockSyncFactory.spec.js b/packages/js-drive/test/unit/core/waitForCoreChainLockSyncFactory.spec.js new file mode 100644 index 00000000000..d88d7801873 --- /dev/null +++ b/packages/js-drive/test/unit/core/waitForCoreChainLockSyncFactory.spec.js @@ -0,0 +1,86 @@ +const EventEmitter = require('events'); +const LatestCoreChainLock = require('../../../lib/core/LatestCoreChainLock'); +const ZMQClient = require('../../../lib/core/ZmqClient'); +const waitForCoreChainLockSyncFactory = require('../../../lib/core/waitForCoreChainLockSyncFactory'); +const LoggerMock = require('../../../lib/test/mock/LoggerMock'); + +describe('waitForCoreChainLockSyncFactory', () => { + let waitForCoreChainLockHandler; + let coreRpcClientMock; + let coreZMQClientMock; + let latestCoreChainLock; + let chainLock; + let rawChainLockSigMessage; + + beforeEach(function beforeEach() { + chainLock = { + blockHash: '0000003df90e1cec3fea6bd17508f653cea093c536199e9d50a05bd69ee23b5d', + height: 3887, + signature: '1770e35c281ebfcf14b8a62071f76146eb0a5ede6fb43543a9c0ccddf3cf87fcdd0a96eea867595bb980dcea13e6283f16744631df895404434c7840f9b3d9c1069790a0459a0d35b7ae353519f5d437ded547f8d65f6c4916e988c842488e7a', + }; + + rawChainLockSigMessage = Buffer.from('00000020fd0ab0fc0fb0cbecb62cf7555aee6a8ce18564a9bbed8b22585d9f8563000000ee131c25019aaee0f1bdde2a5d6eb99ec0b4497e68776f18916951e8ddb6b922dd3be45f62f6011ed18800000103000500010000000000000000000000000000000000000000000000000000000000000000ffffffff05024c0f010bffffffff0200c817a8040000001976a91416b93a3b9168a20605cc3cda62f6135a3baa531a88ac00ac23fc060000001976a91416b93a3b9168a20605cc3cda62f6135a3baa531a88ac000000004602004c0f00003d8e273bf286d48ccba5a87b5adf332ed070a15e4e2d81eeb9ff685373be5656961e0b73ea855fdac9cc530782a7f0a22d25d1eaab4b2068efa647e9da0915d02f0f00005d3be29ed65ba0509d9e1936c593a0ce53f60875d16bea3fec1c0ef93d0000001770e35c281ebfcf14b8a62071f76146eb0a5ede6fb43543a9c0ccddf3cf87fcdd0a96eea867595bb980dcea13e6283f16744631df895404434c7840f9b3d9c1069790a0459a0d35b7ae353519f5d437ded547f8d65f6c4916e988c842488e7a', 'hex'); + + latestCoreChainLock = new LatestCoreChainLock(); + coreRpcClientMock = { + getBestChainLock: this.sinon.stub().resolves({ + result: chainLock, + error: null, + id: 5, + }), + getBlock: this.sinon.stub(), + }; + coreZMQClientMock = new EventEmitter(); + coreZMQClientMock.subscribe = this.sinon.stub(); + + const loggerMock = new LoggerMock(this.sinon); + + waitForCoreChainLockHandler = waitForCoreChainLockSyncFactory( + coreZMQClientMock, + coreRpcClientMock, + latestCoreChainLock, + loggerMock, + ); + }); + + it('should wait for chainlock to be synced', async () => { + expect(latestCoreChainLock.chainLock).to.equal(undefined); + + await waitForCoreChainLockHandler(); + + expect(latestCoreChainLock.chainLock.toJSON()).to.deep.equal(chainLock); + + expect(coreZMQClientMock.subscribe).to.be.calledTwice(); + expect(coreZMQClientMock.subscribe).to.be.calledWith(ZMQClient.TOPICS.rawchainlocksig); + expect(coreZMQClientMock.subscribe).to.be.calledWith(ZMQClient.TOPICS.hashblock); + expect(coreRpcClientMock.getBestChainLock).to.be.calledOnce(); + }); + + it('should handle when no chainlock is found via RPC', (done) => { + expect(latestCoreChainLock.chainLock).to.equal(undefined); + + const err = new Error(); + err.code = -32603; + err.message = 'Chainlock not found'; + + coreRpcClientMock.getBestChainLock.throws(err); + + waitForCoreChainLockHandler() + .then(() => { + expect(latestCoreChainLock.chainLock.toJSON()).to.deep.equal(chainLock); + + expect(coreZMQClientMock.subscribe).to.be.calledTwice(); + expect(coreZMQClientMock.subscribe).to.be.calledWith(ZMQClient.TOPICS.rawchainlocksig); + expect(coreZMQClientMock.subscribe).to.be.calledWith(ZMQClient.TOPICS.hashblock); + expect(coreRpcClientMock.getBestChainLock).to.be.calledOnce(); + done(); + }); + + setImmediate(() => { + coreZMQClientMock.emit( + ZMQClient.TOPICS.rawchainlocksig, + rawChainLockSigMessage, + ); + }); + }); +}); diff --git a/packages/js-drive/test/unit/dpp/CachedStateRepositoryDecorator.spec.js b/packages/js-drive/test/unit/dpp/CachedStateRepositoryDecorator.spec.js new file mode 100644 index 00000000000..00f40a8ba79 --- /dev/null +++ b/packages/js-drive/test/unit/dpp/CachedStateRepositoryDecorator.spec.js @@ -0,0 +1,247 @@ +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const createStateRepositoryMock = require('@dashevo/dpp/lib/test/mocks/createStateRepositoryMock'); + +const CachedStateRepositoryDecorator = require('../../../lib/dpp/CachedStateRepositoryDecorator'); + +describe('CachedStateRepositoryDecorator', () => { + let stateRepositoryMock; + let cachedStateRepository; + let id; + let identity; + let documents; + let dataContract; + + beforeEach(function beforeEach() { + id = 'id'; + identity = getIdentityFixture(); + documents = getDocumentsFixture(); + dataContract = getDataContractFixture(); + + stateRepositoryMock = createStateRepositoryMock(this.sinon); + + cachedStateRepository = new CachedStateRepositoryDecorator( + stateRepositoryMock, + ); + }); + + describe('#fetchIdentity', () => { + it('should fetch identity from state repository', async () => { + stateRepositoryMock.fetchIdentity.resolves(identity); + + const result = await cachedStateRepository.fetchIdentity(id); + + expect(result).to.deep.equal(identity); + expect(stateRepositoryMock.fetchIdentity).to.be.calledOnceWith(id); + }); + }); + + describe('#createIdentity', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.createIdentity(identity); + + expect(stateRepositoryMock.createIdentity).to.be.calledOnceWith(identity); + }); + }); + + describe('#addKeysToIdentity', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.addKeysToIdentity(identity.getId(), identity.getPublicKeys()); + + expect(stateRepositoryMock.addKeysToIdentity).to.be.calledOnceWith( + identity.getId(), + identity.getPublicKeys(), + ); + }); + }); + + describe('#fetchIdentityBalance', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.fetchIdentityBalance(identity.getId()); + + expect(stateRepositoryMock.fetchIdentityBalance).to.be.calledOnceWith( + identity.getId(), + ); + }); + }); + + describe('#fetchIdentityBalanceWithDebt', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.fetchIdentityBalanceWithDebt(identity.getId()); + + expect(stateRepositoryMock.fetchIdentityBalanceWithDebt).to.be.calledOnceWith( + identity.getId(), + ); + }); + }); + + describe('#addToIdentityBalance', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.addToIdentityBalance(identity.getId(), 100); + + expect(stateRepositoryMock.addToIdentityBalance).to.be.calledOnceWith( + identity.getId(), + 100, + ); + }); + }); + + describe('#disableIdentityKeys', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.disableIdentityKeys(identity.getId(), [100], 100); + + expect(stateRepositoryMock.disableIdentityKeys).to.be.calledOnceWith( + identity.getId(), + [100], + 100, + ); + }); + }); + + describe('#updateIdentityRevision', () => { + it('should store identity to repository', async () => { + await cachedStateRepository.updateIdentityRevision(identity.getId(), 1); + + expect(stateRepositoryMock.updateIdentityRevision).to.be.calledOnceWith( + identity.getId(), + 1, + ); + }); + }); + + describe('#fetchDocuments', () => { + it('should fetch documents from state repository', async () => { + const contractId = 'contractId'; + const type = 'documentType'; + const options = {}; + + stateRepositoryMock.fetchDocuments.resolves(documents); + + const result = await cachedStateRepository.fetchDocuments(contractId, type, options); + + expect(result).to.equal(documents); + expect(stateRepositoryMock.fetchDocuments).to.be.calledOnceWith(contractId, type, options); + }); + }); + + describe('#createDocument', () => { + it('should create document in repository', async () => { + const [document] = documents; + + await cachedStateRepository.createDocument(document); + + expect(stateRepositoryMock.createDocument).to.be.calledOnceWith(document); + }); + }); + + describe('#updateDocument', () => { + it('should update document in repository', async () => { + const [document] = documents; + + await cachedStateRepository.updateDocument(document); + + expect(stateRepositoryMock.updateDocument).to.be.calledOnceWith(document); + }); + }); + + describe('#removeDocument', () => { + it('should delete document from repository', async () => { + const type = 'documentType'; + + await cachedStateRepository.removeDocument(dataContract, type, id); + + expect(stateRepositoryMock.removeDocument).to.be.calledOnceWith(dataContract, type, id); + }); + }); + + describe('fetchTransaction', () => { + it('should fetch transaction from state repository', async () => { + stateRepositoryMock.fetchTransaction.resolves(dataContract); + + const result = await cachedStateRepository.fetchTransaction(id); + + expect(result).to.equal(dataContract); + expect(stateRepositoryMock.fetchTransaction).to.be.calledOnceWith(id); + }); + }); + + describe('#fetchDataContract', () => { + it('should fetch data contract from state repository if it is not present in cache', async () => { + stateRepositoryMock.fetchDataContract.resolves(dataContract); + + const result = await cachedStateRepository.fetchDataContract(id); + + expect(result).to.equal(dataContract); + expect(stateRepositoryMock.fetchDataContract).to.be.calledOnceWith(id); + }); + }); + + describe('#fetchLatestPlatformBlockHeight', () => { + it('should fetch latest platform height from state repository', async () => { + stateRepositoryMock.fetchLatestPlatformBlockHeight.resolves(10); + + const result = await cachedStateRepository.fetchLatestPlatformBlockHeight(id); + + expect(result).to.equal(10); + expect(stateRepositoryMock.fetchLatestPlatformBlockHeight).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestPlatformBlockTime', () => { + it('should fetch latest platform block time from state repository', async () => { + const timeMs = Date.now(); + + stateRepositoryMock.fetchLatestPlatformBlockTime.returns(timeMs); + + const result = await cachedStateRepository.fetchLatestPlatformBlockTime(); + + expect(result).to.deep.equal(timeMs); + expect(stateRepositoryMock.fetchLatestPlatformBlockTime).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestPlatformCoreChainLockedHeight', () => { + it('should fetch latest platform core chain locked height from state repository', async () => { + const height = 42; + + stateRepositoryMock.fetchLatestPlatformCoreChainLockedHeight.resolves(height); + + const result = await cachedStateRepository.fetchLatestPlatformCoreChainLockedHeight(id); + + expect(result).to.deep.equal(height); + expect(stateRepositoryMock.fetchLatestPlatformCoreChainLockedHeight).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestWithdrawalTransactionIndex', () => { + it('should call fetchLatestWithdrawalTransactionIndex', async () => { + stateRepositoryMock.fetchLatestWithdrawalTransactionIndex.resolves(42); + + const result = await cachedStateRepository.fetchLatestWithdrawalTransactionIndex(); + + expect(result).to.equal(42); + expect( + stateRepositoryMock.fetchLatestWithdrawalTransactionIndex, + ).to.have.been.calledOnce(); + }); + }); + + describe('#enqueueWithdrawalTransaction', () => { + it('should call enqueueWithdrawalTransaction', async () => { + const index = 42; + const transactionBytes = Buffer.alloc(32, 1); + + await cachedStateRepository.enqueueWithdrawalTransaction( + index, transactionBytes, + ); + + expect( + stateRepositoryMock.enqueueWithdrawalTransaction, + ).to.have.been.calledOnceWithExactly( + index, + transactionBytes, + ); + }); + }); +}); diff --git a/packages/js-drive/test/unit/dpp/DriveStateRepository.spec.js b/packages/js-drive/test/unit/dpp/DriveStateRepository.spec.js new file mode 100644 index 00000000000..a974f8522a7 --- /dev/null +++ b/packages/js-drive/test/unit/dpp/DriveStateRepository.spec.js @@ -0,0 +1,732 @@ +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const ReadOperation = require('@dashevo/dpp/lib/stateTransition/fee/operations/ReadOperation'); +const StateTransitionExecutionContext = require('@dashevo/dpp/lib/stateTransition/StateTransitionExecutionContext'); + +const Long = require('long'); + +const DriveStateRepository = require('../../../lib/dpp/DriveStateRepository'); +const StorageResult = require('../../../lib/storage/StorageResult'); +const BlockExecutionContextMock = require('../../../lib/test/mock/BlockExecutionContextMock'); +const BlockInfo = require('../../../lib/blockExecution/BlockInfo'); + +describe('DriveStateRepository', () => { + let stateRepository; + let identityRepositoryMock; + let identityBalanceRepositoryMock; + let identityPublicKeyRepositoryMock; + let dataContractRepositoryMock; + let fetchDocumentsMock; + let documentsRepositoryMock; + let spentAssetLockTransactionsRepositoryMock; + let coreRpcClientMock; + let id; + let identity; + let documents; + let dataContract; + let blockExecutionContextMock; + let simplifiedMasternodeListMock; + let instantLockMock; + let repositoryOptions; + let executionContext; + let operations; + let blockInfo; + let rsDriveMock; + let blockHeight; + let timeMs; + + beforeEach(function beforeEach() { + identity = getIdentityFixture(); + documents = getDocumentsFixture(); + dataContract = getDataContractFixture(); + id = generateRandomIdentifier(); + + coreRpcClientMock = { + getRawTransaction: this.sinon.stub(), + verifyIsLock: this.sinon.stub(), + }; + + dataContractRepositoryMock = { + fetch: this.sinon.stub(), + create: this.sinon.stub(), + update: this.sinon.stub(), + }; + + identityRepositoryMock = { + fetch: this.sinon.stub(), + create: this.sinon.stub(), + updateRevision: this.sinon.stub(), + }; + + identityBalanceRepositoryMock = { + add: this.sinon.stub(), + fetch: this.sinon.stub(), + fetchWithDebt: this.sinon.stub(), + }; + + identityPublicKeyRepositoryMock = { + fetch: this.sinon.stub(), + add: this.sinon.stub(), + disable: this.sinon.stub(), + }; + + fetchDocumentsMock = this.sinon.stub(); + + documentsRepositoryMock = { + create: this.sinon.stub(), + update: this.sinon.stub(), + find: this.sinon.stub(), + delete: this.sinon.stub(), + }; + + spentAssetLockTransactionsRepositoryMock = { + store: this.sinon.stub(), + find: this.sinon.stub(), + delete: this.sinon.stub(), + }; + + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + + timeMs = Date.now(); + + blockHeight = Long.fromNumber(1); + + blockInfo = new BlockInfo(blockHeight.toNumber(), 0, timeMs); + + blockExecutionContextMock.getEpochInfo.returns({ + currentEpochIndex: blockInfo.epoch, + }); + blockExecutionContextMock.getHeight.returns(blockHeight); + blockExecutionContextMock.getTimeMs.returns(timeMs); + + simplifiedMasternodeListMock = { + getStore: this.sinon.stub(), + }; + + repositoryOptions = { useTransaction: true }; + + rsDriveMock = { + fetchLatestWithdrawalTransactionIndex: this.sinon.stub(), + enqueueWithdrawalTransaction: this.sinon.stub(), + }; + + rsDriveMock.fetchLatestWithdrawalTransactionIndex.resolves(42); + + stateRepository = new DriveStateRepository( + identityRepositoryMock, + identityBalanceRepositoryMock, + identityPublicKeyRepositoryMock, + dataContractRepositoryMock, + fetchDocumentsMock, + documentsRepositoryMock, + spentAssetLockTransactionsRepositoryMock, + coreRpcClientMock, + blockExecutionContextMock, + simplifiedMasternodeListMock, + rsDriveMock, + repositoryOptions, + ); + + instantLockMock = { + getRequestId: () => 'someRequestId', + txid: 'someTxId', + signature: 'signature', + verify: this.sinon.stub(), + }; + + executionContext = new StateTransitionExecutionContext(); + operations = [new ReadOperation(1)]; + }); + + describe('#fetchIdentity', () => { + it('should fetch identity from repository', async () => { + identityRepositoryMock.fetch.resolves( + new StorageResult(identity, operations), + ); + + const result = await stateRepository.fetchIdentity(id, executionContext); + + expect(result).to.equal(identity); + expect(identityRepositoryMock.fetch).to.be.calledOnceWith( + id, + { + blockInfo, + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#createIdentity', () => { + it('should create identity', async () => { + identityRepositoryMock.create.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.createIdentity(identity, executionContext); + + expect(identityRepositoryMock.create).to.be.calledOnceWith( + identity, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#fetchIdentityBalance', () => { + it('should fetch identity balance', async () => { + identityBalanceRepositoryMock.fetch.resolves( + new StorageResult(1, operations), + ); + + const result = await stateRepository.fetchIdentityBalance(identity.getId(), executionContext); + + expect(result).to.equals(1); + + expect(identityBalanceRepositoryMock.fetch).to.be.calledOnceWith( + identity.getId(), + { + blockInfo, + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#fetchIdentityBalanceWithDebt', () => { + it('should fetch identity balance', async () => { + identityBalanceRepositoryMock.fetchWithDebt.resolves( + new StorageResult(1, operations), + ); + + const result = await stateRepository.fetchIdentityBalanceWithDebt( + identity.getId(), + executionContext, + ); + + expect(result).to.equals(1); + + expect(identityBalanceRepositoryMock.fetchWithDebt).to.be.calledOnceWith( + identity.getId(), + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#addToIdentityBalance', () => { + it('should update identity balance', async () => { + identityBalanceRepositoryMock.add.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.addToIdentityBalance(identity.getId(), 1, executionContext); + + expect(identityBalanceRepositoryMock.add).to.be.calledOnceWith( + identity.getId(), + 1, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#updateIdentityRevision', () => { + it('should update identity revision', async () => { + identityRepositoryMock.updateRevision.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.updateIdentityRevision(identity.getId(), 1, executionContext); + + expect(identityRepositoryMock.updateRevision).to.be.calledOnceWith( + identity.getId(), + 1, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#addKeysToIdentity', () => { + it('should add keys to identity', async () => { + identityPublicKeyRepositoryMock.add.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.addKeysToIdentity( + identity.getId(), + identity.getPublicKeys(), + executionContext, + ); + + expect(identityPublicKeyRepositoryMock.add).to.be.calledOnceWith( + identity.getId(), + identity.getPublicKeys(), + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#disableIdentityKeys', () => { + it('should disable identity keys', async () => { + identityPublicKeyRepositoryMock.disable.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.disableIdentityKeys( + identity.getId(), + [1, 2], + 123, + executionContext, + ); + + expect(identityPublicKeyRepositoryMock.disable).to.be.calledOnceWith( + identity.getId(), + [1, 2], + 123, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#fetchDataContract', () => { + it('should fetch data contract from repository', async () => { + dataContractRepositoryMock.fetch.resolves( + new StorageResult(dataContract, operations), + ); + + const result = await stateRepository.fetchDataContract(id, executionContext); + + expect(result).to.equal(dataContract); + expect(dataContractRepositoryMock.fetch).to.be.calledOnceWithExactly( + id, + { + blockInfo, + dryRun: false, + useTransaction: repositoryOptions.useTransaction, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#createDataContract', () => { + it('should create data contract to repository', async () => { + dataContractRepositoryMock.create.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.createDataContract(dataContract, executionContext); + + expect(dataContractRepositoryMock.create).to.be.calledOnceWith( + dataContract, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#updateDataContract', () => { + it('should store data contract to repository', async () => { + dataContractRepositoryMock.update.resolves( + new StorageResult(undefined, operations), + ); + + await stateRepository.updateDataContract(dataContract, executionContext); + + expect(dataContractRepositoryMock.update).to.be.calledOnceWith( + dataContract, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#fetchDocuments', () => { + it('should fetch documents from repository', async () => { + const type = 'documentType'; + const options = {}; + + fetchDocumentsMock.resolves( + new StorageResult(documents, operations), + ); + + const result = await stateRepository.fetchDocuments( + id, + type, + options, + executionContext, + ); + + expect(result).to.equal(documents); + expect(fetchDocumentsMock).to.be.calledOnceWith( + id, + type, + { + blockInfo, + ...options, + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#createDocument', () => { + it('should create document in repository', async () => { + documentsRepositoryMock.create.resolves( + new StorageResult(undefined, operations), + ); + + const [document] = documents; + + await stateRepository.createDocument(document, executionContext); + + expect(documentsRepositoryMock.create).to.be.calledOnceWith( + document, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#updateDocument', () => { + it('should store document in repository', async () => { + documentsRepositoryMock.update.resolves( + new StorageResult(undefined, operations), + ); + + const [document] = documents; + + await stateRepository.updateDocument(document, executionContext); + + expect(documentsRepositoryMock.update).to.be.calledOnceWith( + document, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#removeDocument', () => { + it('should delete document from repository', async () => { + documentsRepositoryMock.delete.resolves( + new StorageResult(undefined, operations), + ); + + const type = 'documentType'; + + await stateRepository.removeDocument(dataContract, type, id, executionContext); + + expect(documentsRepositoryMock.delete).to.be.calledOnceWith( + dataContract, + type, + id, + blockInfo, + { + useTransaction: repositoryOptions.useTransaction, + dryRun: false, + }, + ); + + expect(executionContext.getOperations()).to.deep.equals(operations); + }); + }); + + describe('#fetchTransaction', () => { + it('should fetch transaction from core', async () => { + const rawTransaction = { + hex: 'some result', + height: 1, + }; + + coreRpcClientMock.getRawTransaction.resolves({ result: rawTransaction }); + + const result = await stateRepository.fetchTransaction(id, executionContext); + + expect(result).to.deep.equal({ + data: Buffer.from(rawTransaction.hex, 'hex'), + height: rawTransaction.height, + }); + + expect(coreRpcClientMock.getRawTransaction).to.be.calledOnceWithExactly(id, 1); + + const operation = new ReadOperation(Buffer.from(rawTransaction.hex, 'hex').length); + + expect(executionContext.getOperations()).to.deep.equals([operation]); + }); + + it('should return null if core throws Invalid address or key error', async () => { + const error = new Error('Some error'); + error.code = -5; + + coreRpcClientMock.getRawTransaction.throws(error); + + const result = await stateRepository.fetchTransaction(id); + + expect(result).to.equal(null); + expect(coreRpcClientMock.getRawTransaction).to.be.calledOnceWith(id); + }); + + it('should throw an error if core throws an unknown error', async () => { + const error = new Error('Some error'); + + coreRpcClientMock.getRawTransaction.throws(error); + + try { + await stateRepository.fetchTransaction(id); + + expect.fail('should throw error'); + } catch (e) { + expect(e).to.equal(error); + expect(coreRpcClientMock.getRawTransaction).to.be.calledOnceWith(id); + } + }); + + it('should return mocked transaction on dry run', async () => { + executionContext.enableDryRun(); + + const result = await stateRepository.fetchTransaction(id, executionContext); + + executionContext.disableDryRun(); + + expect(result).to.deep.equal({ + data: Buffer.alloc(0), + height: 1, + }); + + expect(coreRpcClientMock.getRawTransaction).to.not.be.called(id); + }); + }); + + describe('#fetchLatestPlatformBlockHeight', () => { + it('should fetch latest platform block height', async () => { + blockExecutionContextMock.getHeight.resolves(10); + + const result = await stateRepository.fetchLatestPlatformBlockHeight(); + + expect(result).to.equal(10); + expect(blockExecutionContextMock.getHeight).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestPlatformBlockTime', () => { + it('should fetch latest platform block time', async () => { + const result = await stateRepository.fetchLatestPlatformBlockTime(); + + expect(result).to.deep.equal(timeMs); + expect(blockExecutionContextMock.getTimeMs).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestPlatformCoreChainLockedHeight', () => { + it('should fetch latest platform core chainlocked height', async () => { + blockExecutionContextMock.getCoreChainLockedHeight.returns(10); + + const result = await stateRepository.fetchLatestPlatformCoreChainLockedHeight(); + + expect(result).to.equal(10); + expect(blockExecutionContextMock.getCoreChainLockedHeight).to.be.calledOnce(); + }); + }); + + describe('#verifyInstantLock', () => { + let smlStore; + + beforeEach(() => { + blockExecutionContextMock.getHeight.returns(41); + blockExecutionContextMock.getCoreChainLockedHeight.returns(42); + + smlStore = {}; + + simplifiedMasternodeListMock.getStore.returns(smlStore); + }); + + it('it should verify instant lock using Core', async () => { + coreRpcClientMock.verifyIsLock.resolves({ result: true }); + + const result = await stateRepository.verifyInstantLock(instantLockMock); + + expect(result).to.equal(true); + expect(coreRpcClientMock.verifyIsLock).to.have.been.calledOnceWithExactly( + 'someRequestId', + 'someTxId', + 'signature', + 42, + ); + expect(instantLockMock.verify).to.have.not.been.called(); + }); + + it('should return false if core throws Invalid address or key error', async () => { + const error = new Error('Some error'); + error.code = -5; + + coreRpcClientMock.verifyIsLock.throws(error); + + const result = await stateRepository.verifyInstantLock(instantLockMock); + + expect(result).to.equal(false); + expect(coreRpcClientMock.verifyIsLock).to.have.been.calledOnceWithExactly( + 'someRequestId', + 'someTxId', + 'signature', + 42, + ); + expect(instantLockMock.verify).to.have.not.been.called(); + }); + + it('should return false if core throws Invalid parameter', async () => { + const error = new Error('Some error'); + error.code = -8; + + coreRpcClientMock.verifyIsLock.throws(error); + + const result = await stateRepository.verifyInstantLock(instantLockMock); + + expect(result).to.equal(false); + expect(coreRpcClientMock.verifyIsLock).to.have.been.calledOnceWithExactly( + 'someRequestId', + 'someTxId', + 'signature', + 42, + ); + expect(instantLockMock.verify).to.have.not.been.called(); + }); + + it('should return false if coreChainLockedHeight is null', async () => { + blockExecutionContextMock.getCoreChainLockedHeight.returns(null); + + const result = await stateRepository.verifyInstantLock(instantLockMock); + + expect(result).to.be.false(); + }); + + it('should return true on dry run', async () => { + const error = new Error('Some error'); + error.code = -5; + + coreRpcClientMock.verifyIsLock.throws(error); + + executionContext.enableDryRun(); + + const result = await stateRepository.verifyInstantLock(instantLockMock, executionContext); + + executionContext.disableDryRun(); + + expect(result).to.be.true(); + expect(instantLockMock.verify).to.have.not.been.called(); + expect(coreRpcClientMock.verifyIsLock).to.have.not.been.called(); + }); + }); + + describe('#fetchSMLStore', () => { + it('should fetch SML store', async () => { + simplifiedMasternodeListMock.getStore.resolves('store'); + + const result = await stateRepository.fetchSMLStore(); + + expect(result).to.equal('store'); + expect(simplifiedMasternodeListMock.getStore).to.be.calledOnce(); + }); + }); + + describe('#fetchLatestWithdrawalTransactionIndex', () => { + it('should call fetchLatestWithdrawalTransactionIndex', async () => { + const result = await stateRepository.fetchLatestWithdrawalTransactionIndex(); + + expect(result).to.equal(42); + expect( + rsDriveMock.fetchLatestWithdrawalTransactionIndex, + ).to.have.been.calledOnceWithExactly( + blockInfo, + repositoryOptions.useTransaction, + repositoryOptions.dryRun, + ); + }); + }); + + describe('#enqueueWithdrawalTransaction', () => { + it('should call enqueueWithdrawalTransaction', async () => { + const index = 42; + const transactionBytes = Buffer.alloc(32, 1); + + await stateRepository.enqueueWithdrawalTransaction( + index, transactionBytes, + ); + + expect( + rsDriveMock.enqueueWithdrawalTransaction, + ).to.have.been.calledOnceWithExactly( + index, + transactionBytes, + blockInfo, + repositoryOptions.useTransaction, + ); + }); + }); +}); diff --git a/packages/js-drive/test/unit/dpp/LoggedStateRepositoryDecorator.spec.js b/packages/js-drive/test/unit/dpp/LoggedStateRepositoryDecorator.spec.js new file mode 100644 index 00000000000..0e1344b80e5 --- /dev/null +++ b/packages/js-drive/test/unit/dpp/LoggedStateRepositoryDecorator.spec.js @@ -0,0 +1,983 @@ +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const createStateRepositoryMock = require('@dashevo/dpp/lib/test/mocks/createStateRepositoryMock'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const LoggedStateRepositoryDecorator = require('../../../lib/dpp/LoggedStateRepositoryDecorator'); +const LoggerMock = require('../../../lib/test/mock/LoggerMock'); +const BlockExecutionContextMock = require('../../../lib/test/mock/BlockExecutionContextMock'); + +describe('LoggedStateRepositoryDecorator', () => { + let loggedStateRepositoryDecorator; + let stateRepositoryMock; + let loggerMock; + let blockExecutionContextMock; + + beforeEach(function beforeEach() { + stateRepositoryMock = createStateRepositoryMock(this.sinon); + loggerMock = new LoggerMock(this.sinon); + + blockExecutionContextMock = new BlockExecutionContextMock(this.sinon); + blockExecutionContextMock.getContextLogger.returns(loggerMock); + + loggedStateRepositoryDecorator = new LoggedStateRepositoryDecorator( + stateRepositoryMock, + blockExecutionContextMock, + ); + }); + + describe('#fetchIdentity', () => { + let id; + + beforeEach(() => { + id = generateRandomIdentifier(); + }); + + it('should call logger with proper params', async () => { + const response = getIdentityFixture(); + + stateRepositoryMock.fetchIdentity.resolves(response); + + await loggedStateRepositoryDecorator.fetchIdentity(id); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentity', + parameters: { id }, + response, + }, + }, 'StateRepository#fetchIdentity'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchIdentity.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchIdentity(id); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentity', + parameters: { id }, + response: undefined, + }, + }, 'StateRepository#fetchIdentity'); + }); + }); + + describe('#storeIdentity', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.createIdentity.resolves(response); + + await loggedStateRepositoryDecorator.createIdentity(identity); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createIdentity', + parameters: { identity }, + response, + }, + }, 'StateRepository#createIdentity'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.createIdentity.throws(error); + + try { + await loggedStateRepositoryDecorator.createIdentity(identity); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createIdentity', + parameters: { identity }, + response: undefined, + }, + }, 'StateRepository#createIdentity'); + }); + }); + + describe('#addKeysToIdentity', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.addKeysToIdentity.resolves(response); + + await loggedStateRepositoryDecorator.addKeysToIdentity( + identity.getId(), + identity.getPublicKeys(), + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'addKeysToIdentity', + parameters: { identityId: identity.getId(), keys: identity.getPublicKeys() }, + response, + }, + }, 'StateRepository#addKeysToIdentity'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.addKeysToIdentity.throws(error); + + try { + await loggedStateRepositoryDecorator.addKeysToIdentity( + identity.getId(), + identity.getPublicKeys(), + ); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'addKeysToIdentity', + parameters: { identityId: identity.getId(), keys: identity.getPublicKeys() }, + response: undefined, + }, + }, 'StateRepository#addKeysToIdentity'); + }); + }); + + describe('#fetchIdentityBalance', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.fetchIdentityBalance.resolves(response); + + await loggedStateRepositoryDecorator.fetchIdentityBalance( + identity.getId(), + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentityBalance', + parameters: { identityId: identity.getId() }, + response, + }, + }, 'StateRepository#fetchIdentityBalance'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchIdentityBalance.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchIdentityBalance(identity.getId()); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentityBalance', + parameters: { identityId: identity.getId() }, + response: undefined, + }, + }, 'StateRepository#fetchIdentityBalance'); + }); + }); + + describe('#fetchIdentityBalanceWithDebt', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.fetchIdentityBalanceWithDebt.resolves(response); + + await loggedStateRepositoryDecorator.fetchIdentityBalanceWithDebt( + identity.getId(), + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentityBalanceWithDebt', + parameters: { identityId: identity.getId() }, + response, + }, + }, 'StateRepository#fetchIdentityBalanceWithDebt'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchIdentityBalanceWithDebt.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchIdentityBalanceWithDebt(identity.getId()); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchIdentityBalanceWithDebt', + parameters: { identityId: identity.getId() }, + response: undefined, + }, + }, 'StateRepository#fetchIdentityBalanceWithDebt'); + }); + }); + + describe('#addToIdentityBalance', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.addToIdentityBalance.resolves(response); + + const amount = 200; + + await loggedStateRepositoryDecorator.addToIdentityBalance( + identity.getId(), + amount, + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'addToIdentityBalance', + parameters: { identityId: identity.getId(), amount }, + response, + }, + }, 'StateRepository#addToIdentityBalance'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.addToIdentityBalance.throws(error); + + const amount = 100; + + try { + await loggedStateRepositoryDecorator.addToIdentityBalance(identity.getId(), amount); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'addToIdentityBalance', + parameters: { identityId: identity.getId(), amount }, + response: undefined, + }, + }, 'StateRepository#addToIdentityBalance'); + }); + }); + + describe('#disableIdentityKeys', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.disableIdentityKeys.resolves(response); + + const keyIds = [1, 2]; + const disableAt = 123; + + await loggedStateRepositoryDecorator.disableIdentityKeys( + identity.getId(), + keyIds, + disableAt, + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'disableIdentityKeys', + parameters: { identityId: identity.getId(), keyIds, disableAt }, + response, + }, + }, 'StateRepository#disableIdentityKeys'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.disableIdentityKeys.throws(error); + + const keyIds = [1, 2]; + const disableAt = 123; + + try { + await loggedStateRepositoryDecorator.disableIdentityKeys( + identity.getId(), + keyIds, + disableAt, + ); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'disableIdentityKeys', + parameters: { identityId: identity.getId(), keyIds, disableAt }, + response: undefined, + }, + }, 'StateRepository#disableIdentityKeys'); + }); + }); + + describe('#updateIdentityRevision', () => { + let identity; + + beforeEach(() => { + identity = getIdentityFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.updateIdentityRevision.resolves(response); + + const revision = 1; + + await loggedStateRepositoryDecorator.updateIdentityRevision( + identity.getId(), + revision, + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateIdentityRevision', + parameters: { identityId: identity.getId(), revision }, + response, + }, + }, 'StateRepository#updateIdentityRevision'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.updateIdentityRevision.throws(error); + + const revision = 1; + + try { + await loggedStateRepositoryDecorator.updateIdentityRevision( + identity.getId(), + revision, + ); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateIdentityRevision', + parameters: { identityId: identity.getId(), revision }, + response: undefined, + }, + }, 'StateRepository#updateIdentityRevision'); + }); + }); + + describe('#fetchDataContract', () => { + let id; + + beforeEach(() => { + id = generateRandomIdentifier(); + }); + + it('should call logger with proper params', async () => { + const response = getDataContractFixture(); + + stateRepositoryMock.fetchDataContract.resolves(response); + + await loggedStateRepositoryDecorator.fetchDataContract(id); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchDataContract', + parameters: { id }, + response, + }, + }, 'StateRepository#fetchDataContract'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchDataContract.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchDataContract(id); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchDataContract', + parameters: { id }, + response: undefined, + }, + }, 'StateRepository#fetchDataContract'); + }); + }); + + describe('#createDataContract', () => { + let dataContract; + + beforeEach(() => { + dataContract = getDataContractFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.createDataContract.resolves(response); + + await loggedStateRepositoryDecorator.createDataContract(dataContract); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createDataContract', + parameters: { dataContract }, + response, + }, + }, 'StateRepository#createDataContract'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.createDataContract.throws(error); + + try { + await loggedStateRepositoryDecorator.createDataContract(dataContract); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createDataContract', + parameters: { dataContract }, + response: undefined, + }, + }, 'StateRepository#createDataContract'); + }); + }); + + describe('#updateDataContract', () => { + let dataContract; + + beforeEach(() => { + dataContract = getDataContractFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.updateDataContract.resolves(response); + + await loggedStateRepositoryDecorator.updateDataContract(dataContract); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateDataContract', + parameters: { dataContract }, + response, + }, + }, 'StateRepository#updateDataContract'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.updateDataContract.throws(error); + + try { + await loggedStateRepositoryDecorator.updateDataContract(dataContract); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateDataContract', + parameters: { dataContract }, + response: undefined, + }, + }, 'StateRepository#updateDataContract'); + }); + }); + + describe('#fetchDocuments', () => { + let contractId; + let type; + let options; + + beforeEach(() => { + contractId = generateRandomIdentifier(); + type = 'type'; + options = { + where: [['field', '==', 'value']], + }; + }); + + it('should call logger with proper params', async () => { + const response = getDocumentsFixture(); + + stateRepositoryMock.fetchDocuments.resolves(response); + + await loggedStateRepositoryDecorator.fetchDocuments(contractId, type, options); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchDocuments', + parameters: { contractId, type, options }, + response, + }, + }, 'StateRepository#fetchDocuments'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchDocuments.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchDocuments(contractId, type, options); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchDocuments', + parameters: { contractId, type, options }, + response: undefined, + }, + }, 'StateRepository#fetchDocuments'); + }); + }); + + describe('#createDocument', () => { + let document; + + beforeEach(() => { + [document] = getDocumentsFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.createDocument.resolves(response); + + await loggedStateRepositoryDecorator.createDocument(document); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createDocument', + parameters: { document }, + response, + }, + }, 'StateRepository#createDocument'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.createDocument.throws(error); + + try { + await loggedStateRepositoryDecorator.createDocument(document); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'createDocument', + parameters: { document }, + response: undefined, + }, + }, 'StateRepository#createDocument'); + }); + }); + + describe('#updateDocument', () => { + let document; + + beforeEach(() => { + [document] = getDocumentsFixture(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.updateDocument.resolves(response); + + await loggedStateRepositoryDecorator.updateDocument(document); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateDocument', + parameters: { document }, + response, + }, + }, 'StateRepository#updateDocument'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.updateDocument.throws(error); + + try { + await loggedStateRepositoryDecorator.updateDocument(document); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'updateDocument', + parameters: { document }, + response: undefined, + }, + }, 'StateRepository#updateDocument'); + }); + }); + + describe('#removeDocument', () => { + let dataContract; + let type; + let id; + + beforeEach(() => { + dataContract = getDataContractFixture(); + type = 'type'; + id = generateRandomIdentifier(); + }); + + it('should call logger with proper params', async () => { + const response = undefined; + + stateRepositoryMock.removeDocument.resolves(response); + + await loggedStateRepositoryDecorator.removeDocument(dataContract, type, id); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'removeDocument', + parameters: { dataContract, type, id }, + response, + }, + }, 'StateRepository#removeDocument'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.removeDocument.throws(error); + + try { + await loggedStateRepositoryDecorator.removeDocument(dataContract, type, id); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'removeDocument', + parameters: { dataContract, type, id }, + response: undefined, + }, + }, 'StateRepository#removeDocument'); + }); + }); + + describe('#fetchTransaction', () => { + let id; + + beforeEach(() => { + id = 'id'; + }); + + it('should call logger with proper params', async () => { + const response = { hex: '123' }; + + stateRepositoryMock.fetchTransaction.resolves(response); + + await loggedStateRepositoryDecorator.fetchTransaction(id); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchTransaction', + parameters: { id }, + response, + }, + }, 'StateRepository#fetchTransaction'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchTransaction.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchTransaction(id); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchTransaction', + parameters: { id }, + response: undefined, + }, + }, 'StateRepository#fetchTransaction'); + }); + }); + + describe('#fetchLatestPlatformBlockHeight', () => { + it('should call logger with proper params', async () => { + const response = {}; + + stateRepositoryMock.fetchLatestPlatformBlockHeight.resolves(response); + + await loggedStateRepositoryDecorator.fetchLatestPlatformBlockHeight(); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformBlockHeight', + parameters: {}, + response, + }, + }, 'StateRepository#fetchLatestPlatformBlockHeight'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchLatestPlatformBlockHeight.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchLatestPlatformBlockHeight(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformBlockHeight', + parameters: {}, + response: undefined, + }, + }, 'StateRepository#fetchLatestPlatformBlockHeight'); + }); + }); + + describe('#fetchLatestPlatformBlockTime', () => { + it('should call logger with proper params', async () => { + const response = {}; + + stateRepositoryMock.fetchLatestPlatformBlockTime.resolves(response); + + await loggedStateRepositoryDecorator.fetchLatestPlatformBlockTime(); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformBlockTime', + parameters: {}, + response, + }, + }, 'StateRepository#fetchLatestPlatformBlockTime'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchLatestPlatformBlockTime.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchLatestPlatformBlockTime(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformBlockTime', + parameters: {}, + response: undefined, + }, + }, 'StateRepository#fetchLatestPlatformBlockTime'); + }); + }); + + describe('#fetchLatestPlatformCoreChainLockedHeight', () => { + it('should call logger with proper params', async () => { + const response = {}; + + stateRepositoryMock.fetchLatestPlatformCoreChainLockedHeight.resolves(response); + + await loggedStateRepositoryDecorator.fetchLatestPlatformCoreChainLockedHeight(); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformCoreChainLockedHeight', + parameters: {}, + response, + }, + }, 'StateRepository#fetchLatestPlatformCoreChainLockedHeight'); + }); + + it('should call logger in case of error', async () => { + const error = new Error('unknown error'); + + stateRepositoryMock.fetchLatestPlatformCoreChainLockedHeight.throws(error); + + try { + await loggedStateRepositoryDecorator.fetchLatestPlatformCoreChainLockedHeight(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).equals(error); + } + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestPlatformCoreChainLockedHeight', + parameters: {}, + response: undefined, + }, + }, 'StateRepository#fetchLatestPlatformCoreChainLockedHeight'); + }); + }); + + describe('#fetchLatestWithdrawalTransactionIndex', () => { + it('should call fetchLatestWithdrawalTransactionIndex', async () => { + stateRepositoryMock.fetchLatestWithdrawalTransactionIndex.resolves(42); + + const result = await loggedStateRepositoryDecorator.fetchLatestWithdrawalTransactionIndex(); + + expect(result).to.equal(42); + expect( + stateRepositoryMock.fetchLatestWithdrawalTransactionIndex, + ).to.have.been.calledOnce(); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'fetchLatestWithdrawalTransactionIndex', + parameters: {}, + response: 42, + }, + }, 'StateRepository#fetchLatestWithdrawalTransactionIndex'); + }); + }); + + describe('#enqueueWithdrawalTransaction', () => { + it('should call enqueueWithdrawalTransaction', async () => { + const index = 42; + const transactionBytes = Buffer.alloc(32, 1); + + await loggedStateRepositoryDecorator.enqueueWithdrawalTransaction( + index, transactionBytes, + ); + + expect( + stateRepositoryMock.enqueueWithdrawalTransaction, + ).to.have.been.calledOnceWithExactly( + index, + transactionBytes, + ); + + expect(loggerMock.trace).to.be.calledOnceWithExactly({ + stateRepository: { + method: 'enqueueWithdrawalTransaction', + parameters: { index, transactionBytes }, + response: undefined, + }, + }, 'StateRepository#enqueueWithdrawalTransaction'); + }); + }); +}); diff --git a/packages/js-drive/test/unit/featureFlag/getFeatureFlagForHeightFactory.spec.js b/packages/js-drive/test/unit/featureFlag/getFeatureFlagForHeightFactory.spec.js new file mode 100644 index 00000000000..508fa99878d --- /dev/null +++ b/packages/js-drive/test/unit/featureFlag/getFeatureFlagForHeightFactory.spec.js @@ -0,0 +1,65 @@ +const Long = require('long'); + +const Identifier = require('@dashevo/dpp/lib/Identifier'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); + +const getFeatureFlagForHeightFactory = require('../../../lib/featureFlag/getFeatureFlagForHeightFactory'); +const StorageResult = require('../../../lib/storage/StorageResult'); + +describe('getFeatureFlagForHeightFactory', () => { + let featureFlagDataContractId; + let fetchDocumentsMock; + let getFeatureFlagForHeight; + let document; + let featureFlagDataContractBlockHeight; + + beforeEach(function beforeEach() { + featureFlagDataContractId = Identifier.from(Buffer.alloc(32, 1)); + + ([document] = getDocumentsFixture()); + + fetchDocumentsMock = this.sinon.stub().resolves( + new StorageResult([document]), + ); + + featureFlagDataContractBlockHeight = 42; + + getFeatureFlagForHeight = getFeatureFlagForHeightFactory( + featureFlagDataContractId, + fetchDocumentsMock, + ); + }); + + it('should call `fetchDocuments` and return first item from the result', async () => { + const result = await getFeatureFlagForHeight('someType', new Long(43)); + + const query = { + where: [ + ['enableAtHeight', '==', 43], + ], + }; + + expect(fetchDocumentsMock).to.have.been.calledOnceWithExactly( + featureFlagDataContractId, + 'someType', + { + ...query, + useTransaction: false, + }, + ); + expect(result).to.deep.equal(document); + }); + + it('should return null if featureFlagDataContractId is undefined', async () => { + getFeatureFlagForHeight = getFeatureFlagForHeightFactory( + undefined, + featureFlagDataContractBlockHeight, + fetchDocumentsMock, + ); + + const result = await getFeatureFlagForHeight('someType', new Long(42)); + + expect(result).to.equal(null); + expect(fetchDocumentsMock).to.not.be.called(); + }); +}); diff --git a/packages/js-drive/test/unit/featureFlag/getLatestFeatureFlagFactory.spec.js b/packages/js-drive/test/unit/featureFlag/getLatestFeatureFlagFactory.spec.js new file mode 100644 index 00000000000..d0db783d48f --- /dev/null +++ b/packages/js-drive/test/unit/featureFlag/getLatestFeatureFlagFactory.spec.js @@ -0,0 +1,53 @@ +const Identifier = require('@dashevo/dpp/lib/Identifier'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const { expect } = require('chai'); + +const Long = require('long'); + +const getLatestFeatureFlagFactory = require('../../../lib/featureFlag/getLatestFeatureFlagFactory'); +const StorageResult = require('../../../lib/storage/StorageResult'); + +describe('getLatestFeatureFlagFactory', () => { + let featureFlagDataContractId; + let fetchDocumentsMock; + let getLatestFeatureFlag; + let document; + + beforeEach(function beforeEach() { + featureFlagDataContractId = Identifier.from(Buffer.alloc(32, 1)); + + ([document] = getDocumentsFixture()); + + fetchDocumentsMock = this.sinon.stub(); + fetchDocumentsMock.resolves( + new StorageResult([document]), + ); + + getLatestFeatureFlag = getLatestFeatureFlagFactory( + featureFlagDataContractId, + fetchDocumentsMock, + ); + }); + + it('should call `fetchDocuments` and return first item from the result', async () => { + const result = await getLatestFeatureFlag('someType', new Long(42)); + + const query = { + where: [ + ['enableAtHeight', '<=', 42], + ], + orderBy: [ + ['enableAtHeight', 'desc'], + ], + limit: 1, + useTransaction: false, + }; + + expect(fetchDocumentsMock).to.have.been.calledOnceWithExactly( + featureFlagDataContractId, + 'someType', + query, + ); + expect(result).to.deep.equal(document); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/createMasternodeIdentityFactory.spec.js b/packages/js-drive/test/unit/identity/masternode/createMasternodeIdentityFactory.spec.js new file mode 100644 index 00000000000..31fed3011d5 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/createMasternodeIdentityFactory.spec.js @@ -0,0 +1,191 @@ +const createDPPMock = require('@dashevo/dpp/lib/test/mocks/createDPPMock'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); +const ValidationResult = require('@dashevo/dpp/lib/validation/ValidationResult'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const createMasternodeIdentityFactory = require('../../../../lib/identity/masternode/createMasternodeIdentityFactory'); +const InvalidMasternodeIdentityError = require('../../../../lib/identity/masternode/errors/InvalidMasternodeIdentityError'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); + +describe('createMasternodeIdentityFactory', () => { + let createMasternodeIdentity; + let dppMock; + let validationResult; + let getWithdrawPubKeyTypeFromPayoutScriptMock; + let getPublicKeyFromPayoutScriptMock; + let identityRepositoryMock; + let blockInfo; + + beforeEach(function beforeEach() { + dppMock = createDPPMock(this.sinon); + + getWithdrawPubKeyTypeFromPayoutScriptMock = this.sinon.stub().returns( + IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH, + ); + + getPublicKeyFromPayoutScriptMock = this.sinon.stub().returns( + Buffer.alloc(20, 1), + ); + + validationResult = new ValidationResult(); + + dppMock.identity.validate.resolves(validationResult); + + identityRepositoryMock = { + create: this.sinon.stub(), + }; + + createMasternodeIdentity = createMasternodeIdentityFactory( + dppMock, + identityRepositoryMock, + getWithdrawPubKeyTypeFromPayoutScriptMock, + getPublicKeyFromPayoutScriptMock, + ); + + blockInfo = new BlockInfo(1, 0, Date.now()); + }); + + it('should create masternode identity', async () => { + const identityId = generateRandomIdentifier(); + const pubKeyData = Buffer.from([0]); + const pubKeyType = IdentityPublicKey.TYPES.ECDSA_HASH160; + + const result = await createMasternodeIdentity(blockInfo, identityId, pubKeyData, pubKeyType); + + const identity = new Identity({ + protocolVersion: 1, + id: identityId, + publicKeys: [{ + id: 0, + type: pubKeyType, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: true, + // Copy data buffer + data: Buffer.from([0]), + }], + balance: 0, + revision: 0, + }); + + expect(result).to.deep.equal(identity); + + expect(identityRepositoryMock.create).to.have.been.calledOnceWithExactly( + identity, + blockInfo, + { useTransaction: true }, + ); + + expect(getWithdrawPubKeyTypeFromPayoutScriptMock).to.not.be.called(); + + expect(getPublicKeyFromPayoutScriptMock).to.not.be.called(); + + expect(dppMock.identity.validate).to.be.calledOnceWithExactly(identity); + }); + + it('should store identity and public key hashed to the previous store', async () => { + const identityId = generateRandomIdentifier(); + const pubKeyData = Buffer.from([0]); + const pubKeyType = IdentityPublicKey.TYPES.ECDSA_HASH160; + + const result = await createMasternodeIdentity(blockInfo, identityId, pubKeyData, pubKeyType); + + const identity = new Identity({ + protocolVersion: 1, + id: identityId, + publicKeys: [{ + id: 0, + type: pubKeyType, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: true, + // Copy data buffer + data: Buffer.from([0]), + }], + balance: 0, + revision: 0, + }); + + expect(result).to.deep.equal(identity); + + expect(identityRepositoryMock.create).to.have.been.calledOnceWithExactly( + identity, + blockInfo, + { useTransaction: true }, + ); + }); + + it('should throw DPPValidationAbciError if identity is not valid', async () => { + const validationError = new Error('Validation error'); + + validationResult.addError(validationError); + + const identityId = generateRandomIdentifier(); + const pubKeyData = Buffer.from([0]); + const pubKeyType = IdentityPublicKey.TYPES.ECDSA_HASH160; + + try { + await createMasternodeIdentity(blockInfo, identityId, pubKeyData, pubKeyType); + + expect.fail('should fail with an error'); + } catch (e) { + expect(e).to.be.an.instanceof(InvalidMasternodeIdentityError); + expect(e.message).to.be.equal('Invalid masternode identity'); + expect(e.getValidationError()).to.be.deep.equal(validationError); + } + }); + + // TODO: Enable keys when we have support of non unique keys in DPP + it.skip('should create masternode identity with payoutScript public key', async () => { + const identityId = generateRandomIdentifier(); + const pubKeyData = Buffer.from([0]); + const pubKeyType = IdentityPublicKey.TYPES.ECDSA_HASH160; + const payoutScript = new Script(Address.fromString('7UkJidhNjEPJCQnCTXeaJKbJmL4JuyV66w')); + + const result = await createMasternodeIdentity( + blockInfo, + identityId, + pubKeyData, + pubKeyType, + payoutScript, + ); + + const identity = new Identity({ + protocolVersion: 1, + id: identityId, + publicKeys: [{ + id: 0, + type: pubKeyType, + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + readOnly: true, + data: Buffer.from([0]), + }, { + id: 1, + type: IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH, + purpose: IdentityPublicKey.PURPOSES.WITHDRAW, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.CRITICAL, + readOnly: false, + data: Buffer.alloc(20, 1), + }], + balance: 0, + revision: 0, + }); + + expect(result).to.deep.equal(identity); + + expect(identityRepositoryMock.create).to.have.been.calledOnceWithExactly( + identity, + blockInfo, + { useTransaction: true }, + ); + + expect(getWithdrawPubKeyTypeFromPayoutScriptMock).to.be.calledOnce(); + + expect(getPublicKeyFromPayoutScriptMock).to.be.calledOnce(); + + expect(dppMock.identity.validate).to.be.calledOnceWithExactly(identity); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/createOperatorIdentifier.spec.js b/packages/js-drive/test/unit/identity/masternode/createOperatorIdentifier.spec.js new file mode 100644 index 00000000000..59815f056d5 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/createOperatorIdentifier.spec.js @@ -0,0 +1,23 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const createOperatorIdentifier = require('../../../../lib/identity/masternode/createOperatorIdentifier'); + +describe('createOperatorIdentifier', () => { + let smlEntry; + + beforeEach(() => { + smlEntry = { + proRegTxHash: '5557273f5922d9925e2327908ddb128bcf8e055a04d86e23431809bedd077060', + confirmedHash: '0000003da09fd100c60ad5743c44257bb9220ad8162a9b6cae9d005c8e465dba', + service: '95.222.25.60:19997', + pubKeyOperator: '08b66151b81bd6a08bad2e68810ea07014012d6d804859219958a7fbc293689aa902bd0cd6db7a4699c9e88a4ae8c2c0', + votingAddress: 'yZRteAQ51BoeD3sJL1iGdt6HJLgkWGurw5', + isValid: false, + }; + }); + + it('should return operator identifier from smlEntry', () => { + const identifier = createOperatorIdentifier(smlEntry); + + expect(identifier).to.deep.equal(Identifier.from('EwLi1FgGwvmLQ9nkfnttpXzv4SfC7XGBvs61QBCtnHEL')); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/createVotingIdentifier.spec.js b/packages/js-drive/test/unit/identity/masternode/createVotingIdentifier.spec.js new file mode 100644 index 00000000000..7d65de4353d --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/createVotingIdentifier.spec.js @@ -0,0 +1,23 @@ +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const createVotingIdentifier = require('../../../../lib/identity/masternode/createVotingIdentifier'); + +describe('createVotingIdentifier', () => { + let smlEntry; + + beforeEach(() => { + smlEntry = { + proRegTxHash: '5557273f5922d9925e2327908ddb128bcf8e055a04d86e23431809bedd077060', + confirmedHash: '0000003da09fd100c60ad5743c44257bb9220ad8162a9b6cae9d005c8e465dba', + service: '95.222.25.60:19997', + pubKeyOperator: '08b66151b81bd6a08bad2e68810ea07014012d6d804859219958a7fbc293689aa902bd0cd6db7a4699c9e88a4ae8c2c0', + votingAddress: 'yZRteAQ51BoeD3sJL1iGdt6HJLgkWGurw5', + isValid: false, + }; + }); + + it('should return voting identifier from smlEntry', () => { + const identifier = createVotingIdentifier(smlEntry); + + expect(identifier).to.deep.equal(Identifier.from('G1p14MYdpNRLNWuKgQ9SjJUPxfuaJMTwYjdRWu9sLzvL')); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/getPublicKeyFromPayoutScript.spec.js b/packages/js-drive/test/unit/identity/masternode/getPublicKeyFromPayoutScript.spec.js new file mode 100644 index 00000000000..9239624e377 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/getPublicKeyFromPayoutScript.spec.js @@ -0,0 +1,43 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const InvalidIdentityPublicKeyTypeError = require('@dashevo/dpp/lib/stateTransition/errors/InvalidIdentityPublicKeyTypeError'); +const getPublicKeyFromPayoutScript = require('../../../../lib/identity/masternode/getPublicKeyFromPayoutScript'); + +describe('getPublicKeyFromPayoutScript', () => { + it('should return public key for ECDSA_HASH160 script', () => { + const payoutAddress = Address.fromString('yLceJztHVZFbeqE9v86sLD9bDKFBmNqHQD'); + const scriptBuffer = new Script(payoutAddress); + + const type = IdentityPublicKey.TYPES.ECDSA_HASH160; + + const result = getPublicKeyFromPayoutScript(scriptBuffer, type); + + expect(result).to.deep.equal(Buffer.from('0340a3abf7e6eccf42b4dd71ef8c20ed53a78d1f', 'hex')); + }); + + it('should return public key for BIP13_SCRIPT_HASH script', () => { + const payoutAddress = Address.fromString('7UkJidhNjEPJCQnCTXeaJKbJmL4JuyV66w'); + const scriptBuffer = new Script(payoutAddress); + + const type = IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH; + + const result = getPublicKeyFromPayoutScript(scriptBuffer, type); + + expect(result).to.deep.equal(Buffer.from('19a7d869032368fd1f1e26e5e73a4ad0e474960e', 'hex')); + }); + + it('should throw InvalidIdentityPublicKeyTypeError if type is unknown', () => { + const payoutAddress = Address.fromString('7UkJidhNjEPJCQnCTXeaJKbJmL4JuyV66w'); + const scriptBuffer = new Script(payoutAddress); + + try { + getPublicKeyFromPayoutScript(scriptBuffer, -1); + + expect.fail('should throw InvalidIdentityPublicKeyTypeError'); + } catch (e) { + expect(e).to.be.an.instanceof(InvalidIdentityPublicKeyTypeError); + expect(e.getPublicKeyType()).to.equal(-1); + } + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.spec.js b/packages/js-drive/test/unit/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.spec.js new file mode 100644 index 00000000000..5b2950fec34 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory.spec.js @@ -0,0 +1,44 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const getWithdrawPubKeyTypeFromPayoutScriptFactory = require('../../../../lib/identity/masternode/getWithdrawPubKeyTypeFromPayoutScriptFactory'); +const InvalidPayoutScriptError = require('../../../../lib/identity/masternode/errors/InvalidPayoutScriptError'); + +describe('getWithdrawPubKeyTypeFromPayoutScriptFactory', () => { + let getWithdrawPubKeyTypeFromPayoutScript; + let network; + + beforeEach(() => { + network = 'testnet'; + getWithdrawPubKeyTypeFromPayoutScript = getWithdrawPubKeyTypeFromPayoutScriptFactory( + network, + ); + }); + + it('should return ECDSA_HASH160 if address has p2pkh type', () => { + const payoutScript = Script(Address.fromString('yTsGq4wV8WF5GKLaYV2C43zrkr2sfTtysT')); + const type = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + expect(type).to.be.equal(IdentityPublicKey.TYPES.ECDSA_HASH160); + }); + + it('should return BIP13_SCRIPT_HASH if address has p2sh type', () => { + const payoutScript = Script(Address.fromString('7UkJidhNjEPJCQnCTXeaJKbJmL4JuyV66w')); + const type = getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + expect(type).to.be.equal(IdentityPublicKey.TYPES.BIP13_SCRIPT_HASH); + }); + + it('should throw InvalidPayoutScriptError if address is not p2sh or p2pkh', () => { + const payoutScript = new Script(); + + try { + getWithdrawPubKeyTypeFromPayoutScript(payoutScript); + + expect.fail('should throw InvalidPayoutScriptError'); + } catch (e) { + expect(e).to.be.an.instanceOf(InvalidPayoutScriptError); + expect(e.getPayoutScript()).to.deep.equal(payoutScript); + } + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/handleNewMasternodeFactory.spec.js b/packages/js-drive/test/unit/identity/masternode/handleNewMasternodeFactory.spec.js new file mode 100644 index 00000000000..84e5b382fbf --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/handleNewMasternodeFactory.spec.js @@ -0,0 +1,178 @@ +const createDPPMock = require('@dashevo/dpp/lib/test/mocks/createDPPMock'); +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Address = require('@dashevo/dashcore-lib/lib/address'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const handleNewMasternodeFactory = require('../../../../lib/identity/masternode/handleNewMasternodeFactory'); +const getSmlFixture = require('../../../../lib/test/fixtures/getSmlFixture'); +const createOperatorIdentifier = require('../../../../lib/identity/masternode/createOperatorIdentifier'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); +const createVotingIdentifier = require('../../../../lib/identity/masternode/createVotingIdentifier'); + +describe('handleNewMasternodeFactory', () => { + let handleNewMasternode; + let dppMock; + let createMasternodeIdentityMock; + let createRewardShareDocumentMock; + let fetchTransactionMock; + let transactionFixture; + let masternodeEntry; + let dataContract; + let blockInfo; + let identityFixture; + + beforeEach(function beforeEach() { + const smlFixture = getSmlFixture(); + [masternodeEntry] = smlFixture[0].mnList; + masternodeEntry.operatorPayoutAddress = 'yTCALGQTFNsA4pMPLTKAWdaLRmxfGpbujY'; + + dataContract = getDataContractFixture(); + + dppMock = createDPPMock(this.sinon); + + identityFixture = getIdentityFixture(); + + createMasternodeIdentityMock = this.sinon.stub().resolves(identityFixture); + createRewardShareDocumentMock = this.sinon.stub(); + + blockInfo = new BlockInfo(1, 0, Date.now()); + + transactionFixture = { + extraPayload: { + operatorReward: 0, + keyIDOwner: Buffer.alloc(20).fill('a').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('b').toString('hex'), + }, + }; + + fetchTransactionMock = this.sinon.stub().resolves(transactionFixture); + + handleNewMasternode = handleNewMasternodeFactory( + dppMock, + createMasternodeIdentityMock, + createRewardShareDocumentMock, + fetchTransactionMock, + ); + }); + + it('should create masternode identity', async () => { + masternodeEntry.payoutAddress = 'yRRwW957BJwL6SVVh3s8ASQYa2qXnduyfx'; + + const payoutAddress = Address.fromString(masternodeEntry.payoutAddress); + const payoutScript = new Script(payoutAddress); + + const result = await handleNewMasternode(masternodeEntry, dataContract, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(2); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.createdEntities[0].toObject()).to.deep.equal(identityFixture.toObject()); + expect(result.createdEntities[1].toObject()).to.deep.equal(identityFixture.toObject()); + + expect(fetchTransactionMock).to.be.calledOnceWithExactly(masternodeEntry.proRegTxHash); + expect(createMasternodeIdentityMock.getCall(0)).to.be.calledWithExactly( + blockInfo, + Identifier.from('HYyu6DdUQyiHZwzeWpmahu7AUrsEF9MKkRcrdQnKeNSj'), + Buffer.from('6161616161616161616161616161616161616161', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + payoutScript, + ); + + expect(createMasternodeIdentityMock.getCall(1)).to.be.calledWithExactly( + blockInfo, + Identifier.from('GVYoKVDd29gbmHzbVGepFjCbdymCS5Jq26CCiLnWNL6C'), + Buffer.from('6262626262626262626262626262626262626262', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + ); + + expect(createRewardShareDocumentMock).to.not.be.called(); + }); + + it('should not create voting identity if keyIDVoting equals keyIDOwner', async () => { + masternodeEntry.payoutAddress = 'yRRwW957BJwL6SVVh3s8ASQYa2qXnduyfx'; + + const payoutAddress = Address.fromString(masternodeEntry.payoutAddress); + const payoutScript = new Script(payoutAddress); + + transactionFixture.extraPayload.keyIDVoting = transactionFixture.extraPayload.keyIDOwner; + + fetchTransactionMock.resolves(transactionFixture); + + const result = await handleNewMasternode(masternodeEntry, dataContract, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(1); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.createdEntities[0].toJSON()).to.deep.equal(identityFixture.toJSON()); + + expect(fetchTransactionMock).to.be.calledOnceWithExactly(masternodeEntry.proRegTxHash); + expect(createMasternodeIdentityMock).to.be.calledOnceWithExactly( + blockInfo, + Identifier.from('HYyu6DdUQyiHZwzeWpmahu7AUrsEF9MKkRcrdQnKeNSj'), + Buffer.from('6161616161616161616161616161616161616161', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + payoutScript, + ); + + expect(createRewardShareDocumentMock).to.not.be.called(); + }); + + it('should create masternode identity and a document in rewards data contract with percentage', async () => { + transactionFixture.extraPayload.operatorReward = 10; + + const result = await handleNewMasternode(masternodeEntry, dataContract, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(3); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.createdEntities[0].toJSON()).to.deep.equal(identityFixture.toJSON()); + expect(result.createdEntities[1].toJSON()).to.deep.equal(identityFixture.toJSON()); + expect(result.createdEntities[2].toJSON()).to.deep.equal(identityFixture.toJSON()); + + const operatorIdentifier = createOperatorIdentifier(masternodeEntry); + const operatorPayoutAddress = Address.fromString(masternodeEntry.operatorPayoutAddress); + const operatorPayoutScript = new Script(operatorPayoutAddress); + + const votingIdentifier = createVotingIdentifier(masternodeEntry); + const payoutAddress = Address.fromString(masternodeEntry.payoutAddress); + const payoutScript = new Script(payoutAddress); + + expect(fetchTransactionMock).to.be.calledOnceWithExactly(masternodeEntry.proRegTxHash); + expect(createMasternodeIdentityMock).to.be.calledThrice(); + expect(createMasternodeIdentityMock.getCall(0)).to.be.calledWithExactly( + blockInfo, + Identifier.from('HYyu6DdUQyiHZwzeWpmahu7AUrsEF9MKkRcrdQnKeNSj'), + Buffer.from('6161616161616161616161616161616161616161', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + payoutScript, + ); + + expect(createMasternodeIdentityMock.getCall(1)).to.be.calledWithExactly( + blockInfo, + operatorIdentifier, + Buffer.from('951a3208ba531ea75aedd2dc0a9efc75f2c4d9492f1ee0a989b593bcd9722b1a101774d80a426552a9f91d24eb55af6e', 'hex'), + IdentityPublicKey.TYPES.BLS12_381, + operatorPayoutScript, + ); + + expect(createMasternodeIdentityMock.getCall(2)).to.be.calledWithExactly( + blockInfo, + votingIdentifier, + Buffer.from('6262626262626262626262626262626262626262', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + ); + + expect(createRewardShareDocumentMock).to.be.calledOnceWithExactly( + dataContract, + Identifier.from('HYyu6DdUQyiHZwzeWpmahu7AUrsEF9MKkRcrdQnKeNSj'), + Identifier.from('4Ftw1Euv5BJrUk73gKeELFsVqrfVXjbUTSt4tNZjBaVq'), + 10, + blockInfo, + ); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/handleUpdatedScriptPayoutFactory.spec.js b/packages/js-drive/test/unit/identity/masternode/handleUpdatedScriptPayoutFactory.spec.js new file mode 100644 index 00000000000..dec890be168 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/handleUpdatedScriptPayoutFactory.spec.js @@ -0,0 +1,157 @@ +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const Script = require('@dashevo/dashcore-lib/lib/script'); +const identitySchema = require('@dashevo/dpp/schema/identity/identity.json'); +const handleUpdatedScriptPayoutFactory = require('../../../../lib/identity/masternode/handleUpdatedScriptPayoutFactory'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); +const StorageResult = require('../../../../lib/storage/StorageResult'); + +describe('handleUpdatedScriptPayoutFactory', () => { + let handleUpdatedScriptPayout; + let getWithdrawPubKeyTypeFromPayoutScriptMock; + let getPublicKeyFromPayoutScriptMock; + let identity; + let blockInfo; + let identityRepositoryMock; + let identityPublicKeyRepositoryMock; + + beforeEach(function beforeEach() { + identity = getIdentityFixture(); + + blockInfo = new BlockInfo(1, 0, Date.now()); + + getWithdrawPubKeyTypeFromPayoutScriptMock = this.sinon.stub().returns( + IdentityPublicKey.TYPES.ECDSA_HASH160, + ); + + getPublicKeyFromPayoutScriptMock = this.sinon.stub().returns(Buffer.alloc(20, '0')); + + identityRepositoryMock = { + updateRevision: this.sinon.stub(), + fetch: this.sinon.stub().resolves(new StorageResult(identity, [])), + }; + + identityPublicKeyRepositoryMock = { + add: this.sinon.stub(), + disable: this.sinon.stub(), + }; + + handleUpdatedScriptPayout = handleUpdatedScriptPayoutFactory( + identityRepositoryMock, + identityPublicKeyRepositoryMock, + getWithdrawPubKeyTypeFromPayoutScriptMock, + getPublicKeyFromPayoutScriptMock, + ); + }); + + it('should not update identity if identityPublicKeys max length was reached', async () => { + const { maxItems } = identitySchema.properties.publicKeys; + for (let i = identity.getPublicKeys().length; i < maxItems; ++i) { + identity.publicKeys.push({ + data: 'fakePublicKey', + }); + } + + const newPubKeyData = Buffer.alloc(20, '0'); + + const result = await handleUpdatedScriptPayout( + identity.getId(), + newPubKeyData, + identity.publicKeys[0].getData(), + ); + + expect(result.createdEntities).to.have.lengthOf(0); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(identityRepositoryMock.updateRevision).to.not.be.called(); + expect(identityPublicKeyRepositoryMock.add).to.not.be.called(); + expect(identityPublicKeyRepositoryMock.disable).to.not.be.called(); + }); + + it('should add a public key and disable an old one', async () => { + const newPubKeyData = Buffer.alloc(20, '0'); + + const result = await handleUpdatedScriptPayout( + identity.getId(), + newPubKeyData, + blockInfo, + identity.publicKeys[0].getData(), + ); + + const newWithdrawalIdentityPublicKey = new IdentityPublicKey() + .setId(2) + .setType(IdentityPublicKey.TYPES.ECDSA_HASH160) + .setData(Buffer.from(newPubKeyData)) + .setReadOnly(true) + .setPurpose(IdentityPublicKey.PURPOSES.WITHDRAW) + .setSecurityLevel(IdentityPublicKey.SECURITY_LEVELS.MASTER); + + expect(identityRepositoryMock.updateRevision).to.be.calledOnceWithExactly( + identity.getId(), + 1, + blockInfo, + { useTransaction: true }, + ); + + expect(identityPublicKeyRepositoryMock.add).to.be.calledOnceWithExactly( + identity.getId(), + [newWithdrawalIdentityPublicKey], + blockInfo, + { useTransaction: true }, + ); + + expect(result.createdEntities).to.have.lengthOf(1); + expect(result.updatedEntities).to.have.lengthOf(1); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.createdEntities[0]).to.be.instanceOf(IdentityPublicKey); + expect(result.createdEntities[0].toObject()).to.deep.equal( + newWithdrawalIdentityPublicKey.toObject(), + ); + + expect(result.updatedEntities[0]).to.be.instanceOf(Identity); + expect(result.updatedEntities[0].toObject()).to.deep.equal(identity.toObject()); + }); + + it('should add public keys', async () => { + const newPubKeyData = Buffer.alloc(20, '0'); + + const result = await handleUpdatedScriptPayout( + identity.getId(), + newPubKeyData, + blockInfo, + new Script(), + ); + + const newWithdrawalIdentityPublicKey = new IdentityPublicKey() + .setId(2) + .setType(IdentityPublicKey.TYPES.ECDSA_HASH160) + .setData(Buffer.from(newPubKeyData)) + .setReadOnly(true) + .setPurpose(IdentityPublicKey.PURPOSES.WITHDRAW) + .setSecurityLevel(IdentityPublicKey.SECURITY_LEVELS.MASTER); + + expect(identityRepositoryMock.updateRevision).to.be.calledOnceWithExactly( + identity.getId(), + 1, + blockInfo, + { useTransaction: true }, + ); + + expect(identityPublicKeyRepositoryMock.add).to.be.calledOnceWithExactly( + identity.getId(), + [newWithdrawalIdentityPublicKey], + blockInfo, + { useTransaction: true }, + ); + + expect(result.createdEntities).to.have.lengthOf(1); + expect(result.updatedEntities).to.have.lengthOf(1); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.updatedEntities[0]).to.be.instanceOf(Identity); + expect(result.updatedEntities[0].toObject()).to.deep.equal(identity.toObject()); + }); +}); diff --git a/packages/js-drive/test/unit/identity/masternode/handleUpdatedVotingAddressFactory.spec.js b/packages/js-drive/test/unit/identity/masternode/handleUpdatedVotingAddressFactory.spec.js new file mode 100644 index 00000000000..d1c3ba3f140 --- /dev/null +++ b/packages/js-drive/test/unit/identity/masternode/handleUpdatedVotingAddressFactory.spec.js @@ -0,0 +1,102 @@ +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const IdentityPublicKey = require('@dashevo/dpp/lib/identity/IdentityPublicKey'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); +const handleUpdatedVotingAddressFactory = require('../../../../lib/identity/masternode/handleUpdatedVotingAddressFactory'); +const StorageResult = require('../../../../lib/storage/StorageResult'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); + +describe('handleUpdatedVotingAddressFactory', () => { + let handleUpdatedVotingAddress; + let createMasternodeIdentityMock; + let smlEntry; + let identity; + let fetchTransactionMock; + let transactionFixture; + let identityRepositoryMock; + let blockInfo; + + beforeEach(function beforeEach() { + smlEntry = { + proRegTxHash: '5557273f5922d9925e2327908ddb128bcf8e055a04d86e23431809bedd077060', + confirmedHash: '0000003da09fd100c60ad5743c44257bb9220ad8162a9b6cae9d005c8e465dba', + service: '95.222.25.60:19997', + pubKeyOperator: '08b66151b81bd6a08bad2e68810ea07014012d6d804859219958a7fbc293689aa902bd0cd6db7a4699c9e88a4ae8c2c0', + votingAddress: 'yZRteAQ51BoeD3sJL1iGdt6HJLgkWGurw5', + isValid: false, + }; + + transactionFixture = { + extraPayload: { + operatorReward: 0, + keyIDOwner: Buffer.alloc(20).fill('a').toString('hex'), + keyIDVoting: Buffer.alloc(20).fill('b').toString('hex'), + }, + }; + + fetchTransactionMock = this.sinon.stub().resolves(transactionFixture); + + identity = getIdentityFixture(); + + identityRepositoryMock = { + update: this.sinon.stub(), + fetch: this.sinon.stub().resolves(new StorageResult(null, [])), + }; + + createMasternodeIdentityMock = this.sinon.stub(); + + blockInfo = new BlockInfo(1, 0, Date.now()); + + handleUpdatedVotingAddress = handleUpdatedVotingAddressFactory( + identityRepositoryMock, + createMasternodeIdentityMock, + fetchTransactionMock, + ); + }); + + it('should store updated identity', async () => { + createMasternodeIdentityMock.resolves(identity); + + const result = await handleUpdatedVotingAddress(smlEntry, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(1); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(result.createdEntities[0]).to.deep.equal(identity); + expect(createMasternodeIdentityMock).to.be.calledOnceWithExactly( + blockInfo, + Identifier.from('G1p14MYdpNRLNWuKgQ9SjJUPxfuaJMTwYjdRWu9sLzvL'), + Buffer.from('8fd1a9502c58ab103792693e951bf39f10ee46a9', 'hex'), + IdentityPublicKey.TYPES.ECDSA_HASH160, + ); + expect(fetchTransactionMock).to.be.calledWithExactly(smlEntry.proRegTxHash); + }); + + it('should not update identity if identity already exists', async () => { + identityRepositoryMock.fetch.resolves(new StorageResult(identity, [])); + + const result = await handleUpdatedVotingAddress(smlEntry, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(0); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(createMasternodeIdentityMock).to.not.be.called(); + expect(fetchTransactionMock).to.be.calledWithExactly(smlEntry.proRegTxHash); + }); + + it('should not update identity if owner and voting keys are the same', async () => { + transactionFixture.extraPayload.keyIDVoting = transactionFixture.extraPayload.keyIDOwner; + + fetchTransactionMock.resolves(transactionFixture); + + const result = await handleUpdatedVotingAddress(smlEntry, blockInfo); + + expect(result.createdEntities).to.have.lengthOf(0); + expect(result.updatedEntities).to.have.lengthOf(0); + expect(result.removedEntities).to.have.lengthOf(0); + + expect(createMasternodeIdentityMock).to.not.be.called(); + expect(fetchTransactionMock).to.be.calledWithExactly(smlEntry.proRegTxHash); + }); +}); diff --git a/packages/js-drive/test/unit/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.spec.js b/packages/js-drive/test/unit/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.spec.js new file mode 100644 index 00000000000..1985b1b1623 --- /dev/null +++ b/packages/js-drive/test/unit/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory.spec.js @@ -0,0 +1,89 @@ +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const Identifier = require('@dashevo/dpp/lib/identifier/Identifier'); + +const updateWithdrawalTransactionIdAndStatusFactory = require('../../../../lib/identity/withdrawals/updateWithdrawalTransactionIdAndStatusFactory'); +const BlockInfo = require('../../../../lib/blockExecution/BlockInfo'); + +describe('updateWithdrawalTransactionIdAndStatusFactory', () => { + let updateWithdrawalTransactionIdAndStatus; + let withdrawalsContractId; + let documentRepositoryMock; + let fetchDocumentsMock; + let document1Fixture; + let document2Fixture; + + beforeEach(function beforeEach() { + ([document1Fixture, document2Fixture] = getDocumentsFixture()); + + document1Fixture.set('transactionId', Buffer.alloc(32, 1)); + document2Fixture.set('transactionId', Buffer.alloc(32, 3)); + + withdrawalsContractId = Identifier.from(Buffer.alloc(32)); + + documentRepositoryMock = { + update: this.sinon.stub(), + }; + + fetchDocumentsMock = this.sinon.stub(); + fetchDocumentsMock.resolves([document1Fixture, document2Fixture]); + + updateWithdrawalTransactionIdAndStatus = updateWithdrawalTransactionIdAndStatusFactory( + documentRepositoryMock, + fetchDocumentsMock, + withdrawalsContractId, + ); + }); + + it('should update documents transactionId, status and revision', async () => { + const blockInfo = new BlockInfo(1, 1, 1); + + const coreChainLockedHeight = 42; + + const transactionIdMap = { + [Buffer.alloc(32, 1).toString('hex')]: Buffer.alloc(32, 2), + [Buffer.alloc(32, 3).toString('hex')]: Buffer.alloc(32, 4), + }; + + await updateWithdrawalTransactionIdAndStatus( + blockInfo, + coreChainLockedHeight, + transactionIdMap, + { + useTransaction: true, + }, + ); + + expect(fetchDocumentsMock).to.have.been.calledOnceWithExactly( + withdrawalsContractId, + 'withdrawal', + { + where: [ + ['status', '==', 1], + ['transactionId', 'in', [Buffer.alloc(32, 1), Buffer.alloc(32, 3)]], + ], + orderBy: [ + ['transactionId', 'asc'], + ], + useTransaction: true, + }, + ); + + expect(documentRepositoryMock.update).to.have.been.calledTwice(); + expect(documentRepositoryMock.update.getCall(0).args).to.deep.equal( + [document1Fixture, blockInfo, { useTransaction: true }], + ); + expect(documentRepositoryMock.update.getCall(1).args).to.deep.equal( + [document2Fixture, blockInfo, { useTransaction: true }], + ); + + expect(document1Fixture.get('transactionSignHeight')).to.deep.equal(coreChainLockedHeight); + expect(document1Fixture.get('transactionId')).to.deep.equal(Buffer.alloc(32, 2)); + expect(document1Fixture.get('status')).to.deep.equal(2); + expect(document1Fixture.getRevision()).to.deep.equal(2); + + expect(document2Fixture.get('transactionSignHeight')).to.deep.equal(coreChainLockedHeight); + expect(document2Fixture.get('transactionId')).to.deep.equal(Buffer.alloc(32, 4)); + expect(document2Fixture.get('status')).to.deep.equal(2); + expect(document2Fixture.getRevision()).to.deep.equal(2); + }); +}); diff --git a/packages/js-drive/test/unit/util/ExecutionTimer.spec.js b/packages/js-drive/test/unit/util/ExecutionTimer.spec.js new file mode 100644 index 00000000000..b1f3aff7a98 --- /dev/null +++ b/packages/js-drive/test/unit/util/ExecutionTimer.spec.js @@ -0,0 +1,43 @@ +const ExecutionTimer = require('../../../lib/util/ExecutionTimer'); +const wait = require('../../../lib/util/wait'); + +describe('ExecutionTimer', () => { + let timer; + + beforeEach(() => { + timer = new ExecutionTimer(); + }); + + describe('#startTimer', () => { + it('should throw an error if timer already started', () => { + timer.startTimer('some'); + + try { + timer.startTimer('some'); + expect.fail('An error was not thrown'); + } catch (e) { + expect(e.message).to.equal('some timer is already started'); + } + }); + }); + + describe('#stopTimer', () => { + it('should throw an error if timer has not been started', () => { + try { + timer.stopTimer('some'); + expect.fail('An error was not thrown'); + } catch (e) { + expect(e.message).to.equal('some timer is not started'); + } + }); + }); + + it('should measure function execution time', async () => { + // TODO: maybe there should be a better way to do it + timer.startTimer('some'); + await wait(1500); + const timings = timer.stopTimer('some'); + + expect(parseInt(timings, 10)).to.equal(1); + }); +}); diff --git a/packages/js-drive/test/unit/util/errorHandlerFactory.spec.js b/packages/js-drive/test/unit/util/errorHandlerFactory.spec.js new file mode 100644 index 00000000000..466f87948a4 --- /dev/null +++ b/packages/js-drive/test/unit/util/errorHandlerFactory.spec.js @@ -0,0 +1,142 @@ +const errorHandlerFactory = require('../../../lib/errorHandlerFactory'); +const LoggerMock = require('../../../lib/test/mock/LoggerMock'); + +describe('errorHandlerFactory', () => { + let errorHandler; + let containerMock; + let loggerMock; + let closeAbciServerMock; + + beforeEach(function beforeEach() { + this.sinon.stub(console, 'log'); + this.sinon.stub(console, 'error'); + this.sinon.stub(process, 'exit'); + + containerMock = { + dispose: this.sinon.stub(), + }; + + closeAbciServerMock = this.sinon.stub(); + + loggerMock = new LoggerMock(this.sinon); + + errorHandler = errorHandlerFactory( + loggerMock, + containerMock, + closeAbciServerMock, + ); + }); + + it('should close server, log error, dispose container and exit process on first call', async () => { + const error = new Error('message'); + + await errorHandler(error); + + expect(closeAbciServerMock).to.be.calledOnceWithExactly(); + + // Error face is printed + // eslint-disable-next-line no-console + expect(console.log).to.be.calledOnce(); + + expect(loggerMock.fatal).to.be.calledOnceWithExactly({ err: error }, error.message); + + expect(containerMock.dispose).to.be.calledOnceWithExactly(); + + expect(process.exit).to.be.calledOnceWithExactly(1); + }); + + it('should use consensus logger if it\'s present', async function it() { + const error = new Error('message'); + + const contextLogger = new LoggerMock(this.sinon); + + error.contextLogger = contextLogger; + + await errorHandler(error); + + expect(loggerMock.fatal).to.not.be.called(); + expect(contextLogger.fatal).to.be.calledOnceWithExactly({ err: error }, error.message); + + expect(containerMock.dispose).to.be.calledOnceWithExactly(); + + expect(process.exit).to.be.calledOnceWithExactly(1); + + // eslint-disable-next-line no-console + expect(console.log).to.be.calledOnce(); + }); + + it('should collect an error on second call', async () => { + const error1 = new Error('error1'); + const error2 = new Error('error2'); + + await Promise.all([ + errorHandler(error1), + errorHandler(error2), + ]); + + expect(closeAbciServerMock).to.be.calledOnceWithExactly(); + + // Error face is printed + // eslint-disable-next-line no-console + expect(console.log).to.be.calledOnce(); + + expect(loggerMock.fatal).to.be.calledTwice(); + + expect(loggerMock.fatal.getCall(0)).to.be.calledWithExactly({ err: error1 }, error1.message); + expect(loggerMock.fatal.getCall(1)).to.be.calledWithExactly({ err: error2 }, error2.message); + + expect(containerMock.dispose).to.be.calledOnceWithExactly(); + + expect(process.exit).to.be.calledOnceWithExactly(1); + }); + + it('should dispose container and output error in console if it was thrown during error handling', async () => { + const closeError = new Error('close server error'); + + closeAbciServerMock.throws(closeError); + + const error = new Error('message'); + + await errorHandler(error); + + expect(closeAbciServerMock).to.be.calledOnceWithExactly(); + + // Error face is printed + // eslint-disable-next-line no-console + expect(console.log).to.not.be.called(); + + expect(loggerMock.fatal).to.not.be.called(); + + expect(containerMock.dispose).to.be.calledOnceWithExactly(); + + // eslint-disable-next-line no-console + expect(console.error).to.be.calledOnceWithExactly(closeError); + + expect(process.exit).to.be.calledOnceWithExactly(1); + }); + + it('should output error in console if it was thrown during dispose', async () => { + const disposeError = new Error('dispose error'); + + containerMock.dispose.throws(disposeError); + + const error = new Error('message'); + + await errorHandler(error); + + expect(closeAbciServerMock).to.be.calledOnceWithExactly(); + + // Error face is printed + // eslint-disable-next-line no-console + expect(console.log).to.be.calledOnce(); + + expect(loggerMock.fatal).to.be.calledOnceWithExactly({ err: error }, error.message); + + expect(containerMock.dispose).to.be.calledOnceWithExactly(); + + // eslint-disable-next-line no-console + expect(console.error).to.be.calledOnceWithExactly(disposeError); + + expect(process.exit).to.be.calledOnceWithExactly(1); + }); +}); diff --git a/packages/js-drive/test/unit/util/rejectAfter.spec.js b/packages/js-drive/test/unit/util/rejectAfter.spec.js new file mode 100644 index 00000000000..31a281a6fa6 --- /dev/null +++ b/packages/js-drive/test/unit/util/rejectAfter.spec.js @@ -0,0 +1,34 @@ +const rejectAfter = require('../../../lib/util/rejectAfter'); + +describe('rejectAfter', () => { + it('should return resolved promise', async () => { + const resolvedValue = 1; + const promise = Promise.resolve(resolvedValue); + + const actualValue = await rejectAfter(promise, new Error(), 1000); + + expect(actualValue).to.equal(resolvedValue); + }); + + it('should return rejected promise', (done) => { + const error = new Error(); + const promise = Promise.reject(error); + + const actualPromise = rejectAfter(promise, new Error(), 1000); + + expect(actualPromise).to.be.rejectedWith(error).and.notify(done); + }); + + it('should reject unresolved promise after specified time', function it(done) { + const promise = new Promise(() => {}); + const error = new Error(); + + const clock = this.sinon.useFakeTimers(); + + const rejectedPromise = rejectAfter(promise, error, 1000); + + clock.next(); + + expect(rejectedPromise).to.be.rejectedWith(error).and.notify(done); + }); +}); diff --git a/packages/js-drive/test/unit/util/sanitizeUrl.spec.js b/packages/js-drive/test/unit/util/sanitizeUrl.spec.js new file mode 100644 index 00000000000..73c40d47682 --- /dev/null +++ b/packages/js-drive/test/unit/util/sanitizeUrl.spec.js @@ -0,0 +1,13 @@ +const { expect } = require('chai'); +const sanitizeUrl = require('../../../lib/util/sanitizeUrl'); + +describe('sanitizeUrl', () => { + it('should sanitize an url', () => { + const sanitized = sanitizeUrl('https://www.dash.org?something=true'); + expect(sanitized).to.equal('https://www.dash.org'); + }); + it('should handle non RFC path', () => { + const sanitized = sanitizeUrl('/foo;jsessionid=123456'); + expect(sanitized).to.equal('/foo'); + }); +}); diff --git a/packages/js-drive/test/unit/util/wait.spec.js b/packages/js-drive/test/unit/util/wait.spec.js new file mode 100644 index 00000000000..353cf3606b0 --- /dev/null +++ b/packages/js-drive/test/unit/util/wait.spec.js @@ -0,0 +1,30 @@ +const wait = require('../../../lib/util/wait'); + +describe('wait', () => { + let clock; + let executeWithWait; + + beforeEach(function beforeEach() { + clock = this.sinon.useFakeTimers(); + + executeWithWait = async (f) => { + await wait(1200); + f(); + }; + }); + + it('should delay execution of a flow for a specified amount of milliseconds', function it(done) { + const callback = this.sinon.stub(); + + executeWithWait(callback).then(() => { + expect(callback).to.have.been.calledOnce(); + done(); + }).catch(done); + + clock.tick(1199); + + expect(callback).to.have.not.been.called(); + + clock.tick(1); + }); +}); diff --git a/packages/js-drive/test/unit/validator/Validator.spec.js b/packages/js-drive/test/unit/validator/Validator.spec.js new file mode 100644 index 00000000000..c3544e8140b --- /dev/null +++ b/packages/js-drive/test/unit/validator/Validator.spec.js @@ -0,0 +1,37 @@ +const { expect } = require('chai'); +const Validator = require('../../../lib/validator/Validator'); +const ValidatorNetworkInfo = require('../../../lib/validator/ValidatorNetworkInfo'); + +describe('Validator', () => { + let networkInfo; + let host; + let port; + + beforeEach(() => { + host = '192.168.65.2'; + port = 26656; + networkInfo = new ValidatorNetworkInfo(host, port); + }); + + describe('#createFromQuorumMember', () => { + it('should create an instance from quorum member info', () => { + const memberInfo = { + proTxHash: Buffer.alloc(0, 32).toString('hex'), + pubKeyShare: Buffer.alloc(1, 32).toString('hex'), + }; + + const instance = Validator.createFromQuorumMember(memberInfo, networkInfo); + + expect(instance).to.be.an.instanceOf(Validator); + expect(instance.getProTxHash()).to.deep.equal( + Buffer.from(memberInfo.proTxHash, 'hex'), + ); + expect(instance.getPublicKeyShare()).to.deep.equal( + Buffer.from(memberInfo.pubKeyShare, 'hex'), + ); + expect(instance.getNetworkInfo()).to.be.an.instanceOf(ValidatorNetworkInfo); + expect(instance.getNetworkInfo().getHost()).to.equal(host); + expect(instance.getNetworkInfo().getPort()).to.equal(port); + }); + }); +}); diff --git a/packages/js-drive/test/unit/validator/ValidatorNetworkInfo.spec.js b/packages/js-drive/test/unit/validator/ValidatorNetworkInfo.spec.js new file mode 100644 index 00000000000..92ba6b25946 --- /dev/null +++ b/packages/js-drive/test/unit/validator/ValidatorNetworkInfo.spec.js @@ -0,0 +1,24 @@ +const ValidatorNetworkInfo = require('../../../lib/validator/ValidatorNetworkInfo'); + +describe('ValidatorNetworkInfo', () => { + let validatorNetworkInfo; + let host; + let port; + + beforeEach(() => { + host = '192.168.65.2'; + port = 26656; + }); + + it('should return host', () => { + validatorNetworkInfo = new ValidatorNetworkInfo(host, port); + + expect(validatorNetworkInfo.getHost()).to.equal(host); + }); + + it('should return port', () => { + validatorNetworkInfo = new ValidatorNetworkInfo(host, port); + + expect(validatorNetworkInfo.getPort()).to.equal(port); + }); +}); diff --git a/packages/js-drive/test/unit/validator/ValidatorSet.spec.js b/packages/js-drive/test/unit/validator/ValidatorSet.spec.js new file mode 100644 index 00000000000..7e9e0e3db5f --- /dev/null +++ b/packages/js-drive/test/unit/validator/ValidatorSet.spec.js @@ -0,0 +1,253 @@ +const Long = require('long'); + +const QuorumEntry = require('@dashevo/dashcore-lib/lib/deterministicmnlist/QuorumEntry'); + +const SimplifiedMNListEntry = require('@dashevo/dashcore-lib/lib/deterministicmnlist/SimplifiedMNListEntry'); +const ValidatorSet = require('../../../lib/validator/ValidatorSet'); +const getSmlFixture = require('../../../lib/test/fixtures/getSmlFixture'); +const ValidatorSetIsNotInitializedError = require('../../../lib/validator/errors/ValidatorSetIsNotInitializedError'); +const Validator = require('../../../lib/validator/Validator'); +const PublicKeyShareIsNotPresentError = require('../../../lib/validator/errors/PublicKeyShareIsNotPresentError'); + +describe('ValidatorSet', () => { + let smlStoreMock; + let simplifiedMasternodeListMock; + let smlDiffMock; + let smlMock; + let quorumMembers; + let rotationEntropy; + let quorumEntry; + let coreHeight; + let coreRpcClientMock; + let validatorNetworkPort; + + let validatorSetLLMQType; + + let validatorSet; + let getRandomQuorumMock; + let fetchQuorumMembersMock; + + beforeEach(function beforeEach() { + coreHeight = 42; + + validatorSetLLMQType = 4; + + quorumEntry = new QuorumEntry(getSmlFixture()[0].newQuorums[0]); + + smlDiffMock = { + blockHash: 'some block hash', + }; + + smlMock = { + getQuorum: this.sinon.stub().returns(quorumEntry), + toSimplifiedMNListDiff: this.sinon.stub().returns(smlDiffMock), + getQuorumsOfType: this.sinon.stub().returns( + getSmlFixture()[0].newQuorums.filter((quorum) => quorum.llmqType === 1), + ), + getValidMasternodesList: this.sinon.stub().returns([ + new SimplifiedMNListEntry({ + proRegTxHash: 'c286807d463b06c7aba3b9a60acf64c1fc03da8c1422005cd9b4293f08cf0562', + confirmedHash: '4eb56228c535db3b234907113fd41d57bcc7cdcb8e0e00e57590af27ee88c119', + service: '192.168.65.2:20101', + pubKeyOperator: '809519c5f6f3be1c08782ac42ae9a83b6c7205eba43f9a96a4f032ec7a73f1a7c25fa78cce0d6d9c135f7e2c28527179', + votingAddress: 'yXmprXYP51uzfMyndtWwxz96MnkCKkFc9x', + nType: 0, + isValid: true, + }), + new SimplifiedMNListEntry({ + proRegTxHash: 'a3e1edc6bd352eeaf0ae58e30781ef4b127854241a3fe7fddf36d5b7e1dc2b3f', + confirmedHash: '27a0b637b56af038c45e2fd1f06c2401c8dadfa28ca5e0d19ca836cc984a8378', + service: '192.168.65.2:20201', + pubKeyOperator: '987a4873caba62cd45a2f7d4aa6d94519ee6753e9bef777c927cb94ade768a542b0ff34a93231d3a92b4e75ffdaa366e', + votingAddress: 'ycL7L4mhYoaZdm9TH85svvpfeKtdfo249u', + nType: 0, + isValid: true, + }), + ]), + }; + + smlStoreMock = { + getSMLbyHeight: this.sinon.stub().returns(smlMock), + getCurrentSML: this.sinon.stub().returns(smlMock), + }; + + simplifiedMasternodeListMock = { + getStore: this.sinon.stub().returns(smlStoreMock), + }; + + rotationEntropy = Buffer.from('00000ac05a06682172d8b49be7c9ddc4189126d7200ebf0fc074c433ae74b596', 'hex'); + + quorumMembers = [ + { + proTxHash: 'c286807d463b06c7aba3b9a60acf64c1fc03da8c1422005cd9b4293f08cf0562', + pubKeyOperator: '06abc1c890c9da4e513d52f20da1882228bfa2db4bb29cbd064e1b2a61d9dcdadcf0784fd1371338c8ad1bf323d87ae6', + valid: true, + pubKeyShare: '00d7bb8d6753865c367824691610dcc313b661b7e024e36e82f8af33f5701caddb2668dadd1e647d8d7d5b30e37ebbcf', + }, + { + proTxHash: 'a3e1edc6bd352eeaf0ae58e30781ef4b127854241a3fe7fddf36d5b7e1dc2b3f', + pubKeyOperator: '04d748ba0efeb7a8f8548e0c22b4c188c293a19837a1c5440649279ba73ead0c62ac1e840050a10a35e0ae05659d2a8d', + valid: true, + pubKeyShare: '86d0992f5c73b8f57101c34a0c4ebb17d962bb935a738c1ef1e2bb1c25034d8e4a0a2cc96e0ebc69a7bf3b8b67b2de5f', + }, + { + proTxHash: 'a3e1edc6bd352eeaf0ae58e30781ef4b127854241a3fe7fddf36d5b7e1dc2b3f', + pubKeyOperator: '04d748ba0efeb7a8f8548e0c22b4c188c293a19837a1c5440649279ba73ead0c62ac1e840050a10a35e0ae05659d2a8d', + valid: false, + }, + ]; + + getRandomQuorumMock = this.sinon.stub().resolves(quorumEntry); + + fetchQuorumMembersMock = this.sinon.stub().resolves(quorumMembers); + + const notMasternodeError = new Error(); + notMasternodeError.code = -32603; + + coreRpcClientMock = { + masternode: this.sinon.stub().throws(notMasternodeError), + }; + + validatorNetworkPort = 26656; + + validatorSet = new ValidatorSet( + simplifiedMasternodeListMock, + getRandomQuorumMock, + fetchQuorumMembersMock, + validatorSetLLMQType, + coreRpcClientMock, + validatorNetworkPort, + ); + }); + + describe('initialize', () => { + it('should initialize with specified core height', async () => { + await validatorSet.initialize(coreHeight); + + expect(smlStoreMock.getSMLbyHeight).to.be.calledOnceWithExactly(coreHeight); + + expect(getRandomQuorumMock).to.be.calledOnceWithExactly( + smlMock, + validatorSetLLMQType, + Buffer.from(smlDiffMock.blockHash, 'hex'), + coreHeight, + ); + + expect(fetchQuorumMembersMock).to.be.calledOnceWithExactly( + validatorSetLLMQType, + quorumEntry.quorumHash, + ); + + expect(smlStoreMock.getCurrentSML().getValidMasternodesList).to.be.calledOnce(); + }); + + it('should throw an error if the node is a quorum member and doesn\'t receive public key shares', async () => { + coreRpcClientMock.masternode.resolves({ + result: { + proTxHash: quorumMembers[0].proTxHash, + }, + }); + + quorumMembers[2].valid = true; + + try { + await validatorSet.initialize(coreHeight); + + expect.fail('should throw PublicKeyShareIsNotPresentError'); + } catch (e) { + expect(e).to.be.instanceOf(PublicKeyShareIsNotPresentError); + expect(e.getMember()).to.be.equal(quorumMembers[2]); + } + }); + }); + + describe('rotate', () => { + it('should rotate validator set with specified core height and entropy if height divisible by ROTATION_BLOCK_INTERVAL', async () => { + const height = Long.fromInt(ValidatorSet.ROTATION_BLOCK_INTERVAL); + + const result = await validatorSet.rotate( + height, + coreHeight, + rotationEntropy, + ); + + expect(result).to.be.true(); + + expect(smlStoreMock.getSMLbyHeight).to.be.calledOnceWithExactly(coreHeight); + + expect(getRandomQuorumMock).to.be.calledOnceWithExactly( + smlMock, + validatorSetLLMQType, + rotationEntropy, + coreHeight, + ); + + expect(fetchQuorumMembersMock).to.be.calledOnceWithExactly( + validatorSetLLMQType, + quorumEntry.quorumHash, + ); + + expect(smlStoreMock.getCurrentSML().getValidMasternodesList).to.be.calledOnce(); + }); + + it('should not rotate validator set if height not divisible by ROTATION_BLOCK_INTERVAL', async () => { + const height = Long.fromInt(42); + + const result = await validatorSet.rotate( + height, + coreHeight, + rotationEntropy, + ); + + expect(result).to.be.false(); + + expect(smlStoreMock.getSMLbyHeight).to.be.calledOnceWithExactly(coreHeight); + + expect(getRandomQuorumMock).to.not.be.called(); + + expect(fetchQuorumMembersMock).to.not.be.called(); + }); + }); + + describe('getQuorum', () => { + it('should return QuorumEntry', async () => { + await validatorSet.initialize(coreHeight); + + const result = validatorSet.getQuorum(); + + expect(result).to.equals(quorumEntry); + }); + + it('should thrown an error if ValidatorSet is not initialized', () => { + try { + validatorSet.getQuorum(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(ValidatorSetIsNotInitializedError); + } + }); + }); + + describe('getValidators', () => { + it('should return array of validators', async () => { + await validatorSet.initialize(coreHeight); + + const result = validatorSet.getValidators(); + + expect(result).to.have.lengthOf(2); + expect(result[0]).to.be.instanceOf(Validator); + expect(result[1]).to.be.instanceOf(Validator); + }); + + it('should thrown an error if ValidatorSet is not initialized', () => { + try { + validatorSet.getValidators(); + + expect.fail('should throw an error'); + } catch (e) { + expect(e).to.be.instanceOf(ValidatorSetIsNotInitializedError); + } + }); + }); +}); diff --git a/packages/rs-drive-nodejs/.eslintrc b/packages/rs-drive-nodejs/.eslintrc new file mode 100644 index 00000000000..6de4b115694 --- /dev/null +++ b/packages/rs-drive-nodejs/.eslintrc @@ -0,0 +1,35 @@ +{ + "extends": "airbnb-base", + "env": { + "es2021": true, + "node": true + }, + "rules": { + "no-plusplus": 0, + "eol-last": [ + "error", + "always" + ], + "no-continue": "off", + "class-methods-use-this": "off", + "no-await-in-loop": "off", + "no-restricted-syntax": [ + "error", + { + "selector": "LabeledStatement", + "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." + }, + { + "selector": "WithStatement", + "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." + } + ], + "curly": [ + "error", + "all" + ] + }, + "globals": { + "BigInt": true + } +} diff --git a/packages/rs-drive-nodejs/.gitignore b/packages/rs-drive-nodejs/.gitignore new file mode 100644 index 00000000000..b645ad69ce8 --- /dev/null +++ b/packages/rs-drive-nodejs/.gitignore @@ -0,0 +1,2 @@ +prebuilds +test_data diff --git a/packages/rs-drive-nodejs/.mocharc.yml b/packages/rs-drive-nodejs/.mocharc.yml new file mode 100644 index 00000000000..53f2f870b5e --- /dev/null +++ b/packages/rs-drive-nodejs/.mocharc.yml @@ -0,0 +1,2 @@ +exit: true +timeout: 10000 diff --git a/packages/rs-drive-nodejs/Cargo.toml b/packages/rs-drive-nodejs/Cargo.toml new file mode 100644 index 00000000000..6c38ec29284 --- /dev/null +++ b/packages/rs-drive-nodejs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "drive-nodejs" +version = "0.24.0-dev.1" +description = "GroveDB node.js bindings" +edition = "2021" +license = "MIT" +private = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +drive = { path = "../rs-drive", features = ["fixtures-and-mocks"] } +drive-abci = { path = "../rs-drive-abci" } +num = "0.4.0" + +[dependencies.neon] +version = "0.10.1" +default-features = false +features = ["napi-6", "event-queue-api", "try-catch-api"] + +[features] +enable-mocking = [] diff --git a/packages/rs-drive-nodejs/Drive.js b/packages/rs-drive-nodejs/Drive.js new file mode 100644 index 00000000000..d1ac2239a74 --- /dev/null +++ b/packages/rs-drive-nodejs/Drive.js @@ -0,0 +1,940 @@ +const { promisify } = require('util'); +const cbor = require('cbor'); +const Document = require('@dashevo/dpp/lib/document/Document'); +const DataContract = require('@dashevo/dpp/lib/dataContract/DataContract'); +const Identity = require('@dashevo/dpp/lib/identity/Identity'); +const decodeProtocolEntityFactory = require('@dashevo/dpp/lib/decodeProtocolEntityFactory'); + +// This file is crated when run `npm run build`. The actual source file that +// exports those functions is ./src/lib.rs +const { + driveOpen, + driveClose, + driveCreateInitialStateStructure, + driveFetchContract, + driveCreateContract, + driveUpdateContract, + driveCreateDocument, + driveUpdateDocument, + driveDeleteDocument, + driveQueryDocuments, + driveProveDocumentsQuery, + driveInsertIdentity, + driveFetchIdentity, + driveFetchIdentityBalance, + driveFetchIdentityBalanceWithCosts, + driveFetchIdentityBalanceIncludeDebtWithCosts, + driveFetchProvedIdentity, + driveFetchManyProvedIdentities, + driveFetchIdentityWithCosts, + driveAddToIdentityBalance, + driveAddKeysToIdentity, + driveDisableIdentityKeys, + driveUpdateIdentityRevision, + driveRemoveFromIdentityBalance, + driveApplyFeesToIdentityBalance, + driveFetchLatestWithdrawalTransactionIndex, + abciInitChain, + abciBlockBegin, + abciBlockEnd, + abciAfterFinalizeBlock, + calculateStorageFeeDistributionAmountAndLeftovers, + driveFetchIdentitiesByPublicKeyHashes, + driveProveIdentitiesByPublicKeyHashes, + driveAddToSystemCredits, +} = require('neon-load-or-build')({ + dir: __dirname, +}); + +const GroveDB = require('./GroveDB'); +const FeeResult = require('./FeeResult'); + +const { appendStackAsync, appendStack } = require('./appendStack'); + +const decodeProtocolEntity = decodeProtocolEntityFactory(); + +// Convert the Drive methods from using callbacks to returning promises +const driveCloseAsync = appendStackAsync(promisify(driveClose)); +const driveCreateInitialStateStructureAsync = appendStackAsync( + promisify(driveCreateInitialStateStructure), +); +const driveFetchContractAsync = appendStackAsync(promisify(driveFetchContract)); +const driveCreateContractAsync = appendStackAsync(promisify(driveCreateContract)); +const driveUpdateContractAsync = appendStackAsync(promisify(driveUpdateContract)); +const driveCreateDocumentAsync = appendStackAsync(promisify(driveCreateDocument)); +const driveUpdateDocumentAsync = appendStackAsync(promisify(driveUpdateDocument)); +const driveDeleteDocumentAsync = appendStackAsync(promisify(driveDeleteDocument)); +const driveQueryDocumentsAsync = appendStackAsync(promisify(driveQueryDocuments)); +const driveProveDocumentsQueryAsync = appendStackAsync(promisify(driveProveDocumentsQuery)); +const driveFetchLatestWithdrawalTransactionIndexAsync = appendStackAsync( + promisify(driveFetchLatestWithdrawalTransactionIndex), +); +const driveInsertIdentityAsync = appendStackAsync(promisify(driveInsertIdentity)); +const driveFetchIdentityAsync = appendStackAsync(promisify(driveFetchIdentity)); +const driveFetchProvedIdentityAsync = appendStackAsync(promisify(driveFetchProvedIdentity)); +const driveFetchIdentityBalanceAsync = appendStackAsync(promisify(driveFetchIdentityBalance)); +const driveFetchIdentityBalanceWithCostsAsync = appendStackAsync( + promisify(driveFetchIdentityBalanceWithCosts), +); +const driveFetchIdentityBalanceIncludeDebtWithCostsAsync = appendStackAsync( + promisify(driveFetchIdentityBalanceIncludeDebtWithCosts), +); + +const driveFetchManyProvedIdentitiesAsync = appendStackAsync( + promisify(driveFetchManyProvedIdentities), +); +const driveFetchIdentityWithCostsAsync = appendStackAsync(promisify(driveFetchIdentityWithCosts)); +const driveAddToIdentityBalanceAsync = appendStackAsync(promisify(driveAddToIdentityBalance)); +const driveAddToSystemCreditsAsync = appendStackAsync(promisify(driveAddToSystemCredits)); +const driveFetchIdentitiesByPublicKeyHashesAsync = appendStackAsync( + promisify(driveFetchIdentitiesByPublicKeyHashes), +); +const driveProveIdentitiesByPublicKeyHashesAsync = appendStackAsync( + promisify(driveProveIdentitiesByPublicKeyHashes), +); +const driveAddKeysToIdentityAsync = appendStackAsync(promisify(driveAddKeysToIdentity)); +const driveDisableIdentityKeysAsync = appendStackAsync(promisify(driveDisableIdentityKeys)); +const driveUpdateIdentityRevisionAsync = appendStackAsync(promisify(driveUpdateIdentityRevision)); +const driveRemoveFromIdentityBalanceAsync = appendStackAsync( + promisify(driveRemoveFromIdentityBalance), +); +const driveApplyFeesToIdentityBalanceAsync = appendStackAsync( + promisify(driveApplyFeesToIdentityBalance), +); +const abciInitChainAsync = appendStackAsync(promisify(abciInitChain)); +const abciBlockBeginAsync = appendStackAsync(promisify(abciBlockBegin)); +const abciBlockEndAsync = appendStackAsync(promisify(abciBlockEnd)); +const abciAfterFinalizeBlockAsync = appendStackAsync(promisify(abciAfterFinalizeBlock)); + +const calculateStorageFeeDistributionAmountAndLeftoversWithStack = appendStack( + calculateStorageFeeDistributionAmountAndLeftovers, +); + +// Wrapper class for the boxed `Drive` for idiomatic JavaScript usage +class Drive { + /** + * @param {string} dbPath + * @param {Object} config + * @param {number} config.dataContractsGlobalCacheSize + * @param {number} config.dataContractsBlockCacheSize + */ + constructor(dbPath, config) { + this.drive = driveOpen(dbPath, config); + this.groveDB = new GroveDB(this.drive); + } + + /** + * @returns {GroveDB} + */ + getGroveDB() { + return this.groveDB; + } + + /** + * @returns {Promise} + */ + async close() { + return driveCloseAsync.call(this.drive); + } + + /** + * @param {boolean} [useTransaction=false] + * + * @returns {Promise<[number, number]>} + */ + async createInitialStateStructure(useTransaction = false) { + return driveCreateInitialStateStructureAsync.call(this.drive, useTransaction); + } + + /** + * @param {Buffer|Identifier} id + * @param {number} [epochIndex] + * @param {boolean} [useTransaction=false] + * + * @returns {Promise<[DataContract|null, FeeResult]>} + */ + async fetchContract(id, epochIndex = undefined, useTransaction = false) { + return driveFetchContractAsync.call( + this.drive, + Buffer.from(id), + epochIndex, + useTransaction, + ).then(([encodedDataContract, innerFeeResult]) => { + let dataContract = encodedDataContract; + + if (encodedDataContract !== null) { + const [protocolVersion, rawDataContract] = decodeProtocolEntity( + encodedDataContract, + ); + + rawDataContract.protocolVersion = protocolVersion; + + dataContract = new DataContract(rawDataContract); + } + + const result = [dataContract]; + + if (innerFeeResult) { + result.push(new FeeResult(innerFeeResult)); + } + + return result; + }); + } + + /** + * @param {DataContract} dataContract + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async createContract(dataContract, blockInfo, useTransaction = false, dryRun = false) { + return driveCreateContractAsync.call( + this.drive, + dataContract.toBuffer(), + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {DataContract} dataContract + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async updateContract(dataContract, blockInfo, useTransaction = false, dryRun = false) { + return driveUpdateContractAsync.call( + this.drive, + dataContract.toBuffer(), + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Document} document + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async createDocument(document, blockInfo, useTransaction = false, dryRun = false) { + return driveCreateDocumentAsync.call( + this.drive, + document.toBuffer(), + document.getDataContractId().toBuffer(), + document.getType(), + document.getOwnerId().toBuffer(), + true, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Document} document + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async updateDocument(document, blockInfo, useTransaction = false, dryRun = false) { + return driveUpdateDocumentAsync.call( + this.drive, + document.toBuffer(), + document.getDataContractId().toBuffer(), + document.getType(), + document.getOwnerId().toBuffer(), + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Identifier} dataContractId + * @param {string} documentType + * @param {Identifier} documentId + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async deleteDocument( + dataContractId, + documentType, + documentId, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveDeleteDocumentAsync.call( + this.drive, + documentId.toBuffer(), + dataContractId.toBuffer(), + documentType, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * + * @param {DataContract} dataContract + * @param {string} documentType + * @param {number} [epochIndex] + * @param [query] + * @param [query.where] + * @param [query.limit] + * @param [query.startAt] + * @param [query.startAfter] + * @param [query.orderBy] + * @param {boolean} [useTransaction=false] + * + * @returns {Promise<[Document[], number]>} + */ + async queryDocuments( + dataContract, + documentType, + epochIndex = undefined, + query = {}, + useTransaction = false, + ) { + const encodedQuery = await cbor.encodeAsync(query); + + const [encodedDocuments, , processingFee] = await driveQueryDocumentsAsync.call( + this.drive, + encodedQuery, + dataContract.getId().toBuffer(), + documentType, + epochIndex, + useTransaction, + ); + + const documents = encodedDocuments.map((encodedDocument) => { + const [protocolVersion, rawDocument] = decodeProtocolEntity(encodedDocument); + + rawDocument.$protocolVersion = protocolVersion; + + return new Document(rawDocument, dataContract); + }); + + return [ + documents, + processingFee, + ]; + } + + /** + * + * @param {DataContract} dataContract + * @param {string} documentType + * @param [query] + * @param [query.where] + * @param [query.limit] + * @param [query.startAt] + * @param [query.startAfter] + * @param [query.orderBy] + * @param {boolean} [useTransaction=false] + * + * @returns {Promise<[Document[], number]>} + */ + async proveDocumentsQuery(dataContract, documentType, query = {}, useTransaction = false) { + const encodedQuery = await cbor.encodeAsync(query); + + // eslint-disable-next-line no-return-await + return await driveProveDocumentsQueryAsync.call( + this.drive, + encodedQuery, + dataContract.getId().toBuffer(), + documentType, + useTransaction, + ); + } + + /** + * @param {Identity} identity + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async insertIdentity(identity, blockInfo, useTransaction = false, dryRun = false) { + return driveInsertIdentityAsync.call( + this.drive, + identity.toBuffer(), + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Buffer|Identifier} id + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async fetchIdentity(id, useTransaction = false) { + return driveFetchIdentityAsync.call( + this.drive, + Buffer.from(id), + useTransaction, + ).then((encodedIdentity) => { + if (encodedIdentity === null) { + return null; + } + + const [protocolVersion, rawIdentity] = decodeProtocolEntity( + encodedIdentity, + ); + + rawIdentity.protocolVersion = protocolVersion; + + return new Identity(rawIdentity); + }); + } + + /** + * @param {Buffer|Identifier} id + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async fetchIdentityBalance(id, useTransaction = false) { + return driveFetchIdentityBalanceAsync.call( + this.drive, + Buffer.from(id), + useTransaction, + ); + } + + /** + * @param {Buffer|Identifier} id + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise<[number, FeeResult]>} + */ + async fetchIdentityBalanceWithCosts(id, blockInfo, useTransaction = false, dryRun = false) { + return driveFetchIdentityBalanceWithCostsAsync.call( + this.drive, + Buffer.from(id), + blockInfo, + !dryRun, + useTransaction, + ).then(([balance, innerFeeResult]) => [balance, new FeeResult(innerFeeResult)]); + } + + /** + * @param {Buffer|Identifier} id + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise<[number|null, FeeResult]>} + */ + async fetchIdentityBalanceIncludeDebtWithCosts( + id, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveFetchIdentityBalanceIncludeDebtWithCostsAsync.call( + this.drive, + Buffer.from(id), + blockInfo, + !dryRun, + useTransaction, + ).then(([balance, innerFeeResult]) => [balance, new FeeResult(innerFeeResult)]); + } + + /** + * @param {Identifier} id + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async proveIdentity(id, useTransaction = false) { + return driveFetchProvedIdentityAsync.call( + this.drive, + Buffer.from(id), + useTransaction, + ); + } + + /** + * @param {Identifier[]} ids + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async proveManyIdentities(ids, useTransaction = false) { + return driveFetchManyProvedIdentitiesAsync.call( + this.drive, + ids.map((id) => Buffer.from(id)), + useTransaction, + ); + } + + /** + * @param {Buffer|Identifier} id + * @param {number} epochIndex + * @param {boolean} [useTransaction=false] + * + * @returns {Promise<[Identity|null, FeeResult]>} + */ + async fetchIdentityWithCosts(id, epochIndex, useTransaction = false) { + return driveFetchIdentityWithCostsAsync.call( + this.drive, + Buffer.from(id), + epochIndex, + useTransaction, + ).then(([encodedIdentity, innerFeeResult]) => { + let identity = encodedIdentity; + + if (encodedIdentity !== null) { + const [protocolVersion, rawIdentity] = decodeProtocolEntity( + encodedIdentity, + ); + + rawIdentity.protocolVersion = protocolVersion; + + identity = new Identity(rawIdentity); + } + + return [identity, new FeeResult(innerFeeResult)]; + }); + } + + /** + * @param {Identifier} identityId + * @param {number} amount + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async addToIdentityBalance( + identityId, + amount, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveAddToIdentityBalanceAsync.call( + this.drive, + identityId.toBuffer(), + amount, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Identifier} identityId + * @param {number} amount + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async removeFromIdentityBalance( + identityId, + amount, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveRemoveFromIdentityBalanceAsync.call( + this.drive, + identityId.toBuffer(), + amount, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Identifier} identityId + * @param {FeeResult} fees + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async applyFeesToIdentityBalance( + identityId, + fees, + useTransaction = false, + ) { + return driveApplyFeesToIdentityBalanceAsync.call( + this.drive, + identityId.toBuffer(), + fees.inner, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {number} amount + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async addToSystemCredits( + amount, + useTransaction = false, + ) { + return driveAddToSystemCreditsAsync.call( + this.drive, + amount, + useTransaction, + ); + } + + /** + * @param {Buffer[]} hashes + * @param {boolean} [useTransaction=false] + * + * @returns {Promise>} + */ + async fetchIdentitiesByPublicKeyHashes(hashes, useTransaction = false) { + return driveFetchIdentitiesByPublicKeyHashesAsync.call( + this.drive, + hashes.map((h) => Buffer.from(h)), + useTransaction, + ).then((encodedIdentities) => ( + encodedIdentities.map((encodedIdentity) => { + const [protocolVersion, rawIdentity] = decodeProtocolEntity( + encodedIdentity, + ); + + rawIdentity.protocolVersion = protocolVersion; + + return new Identity(rawIdentity); + }) + )); + } + + /** + * @param {Buffer[]} hashes + * @param {boolean} [useTransaction=false] + * + * @returns {Promise>} + */ + async proveIdentitiesByPublicKeyHashes(hashes, useTransaction = false) { + return driveProveIdentitiesByPublicKeyHashesAsync.call( + this.drive, + hashes.map((h) => Buffer.from(h)), + useTransaction, + ); + } + + /** + * @param {Identifier} identityId + * @param {IdentityPublicKey[]} keys + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async addKeysToIdentity( + identityId, + keys, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveAddKeysToIdentityAsync.call( + this.drive, + identityId.toBuffer(), + keys, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Identifier} identityId + * @param {number[]} keyIds + * @param {number} disableAt + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async disableIdentityKeys( + identityId, + keyIds, + disableAt, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveDisableIdentityKeysAsync.call( + this.drive, + identityId.toBuffer(), + keyIds, + disableAt, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * @param {Identifier} identityId + * @param {number} revision + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async updateIdentityRevision( + identityId, + revision, + blockInfo, + useTransaction = false, + dryRun = false, + ) { + return driveUpdateIdentityRevisionAsync.call( + this.drive, + identityId.toBuffer(), + revision, + blockInfo, + !dryRun, + useTransaction, + ).then((innerFeeResult) => new FeeResult(innerFeeResult)); + } + + /** + * Fetch the latest index of the withdrawal transaction in a queue + * + * @param {RawBlockInfo} blockInfo + * @param {boolean} [useTransaction=false] + * @param {boolean} [dryRun=false] + * + * @returns {Promise} + */ + async fetchLatestWithdrawalTransactionIndex(blockInfo, useTransaction = false, dryRun = false) { + return driveFetchLatestWithdrawalTransactionIndexAsync.call( + this.drive, + blockInfo, + !dryRun, + useTransaction, + ); + } + + /** + * Get the ABCI interface + * @returns {RSAbci} + */ + getAbci() { + const { drive } = this; + + /** + * @typedef RSAbci + */ + return { + /** + * ABCI init chain + * + * @param {InitChainRequest} request + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async initChain(request, useTransaction = false) { + const requestBytes = cbor.encode(request); + + const responseBytes = await abciInitChainAsync.call( + drive, + requestBytes, + useTransaction, + ); + + return cbor.decode(responseBytes); + }, + + /** + * ABCI block begin + * + * @param {BlockBeginRequest} request + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async blockBegin(request, useTransaction = false) { + const requestBytes = cbor.encode({ + ...request, + // cborium doesn't eat Buffers + proposerProTxHash: Array.from(request.proposerProTxHash), + validatorSetQuorumHash: Array.from(request.validatorSetQuorumHash), + }); + + const responseBytes = await abciBlockBeginAsync.call( + drive, + requestBytes, + useTransaction, + ); + + return cbor.decode(responseBytes); + }, + + /** + * ABCI block end + * + * @param {BlockEndRequest} request + * @param {boolean} [useTransaction=false] + * + * @returns {Promise} + */ + async blockEnd(request, useTransaction = false) { + const responseBytes = await abciBlockEndAsync.call( + drive, + request, + useTransaction, + ); + + return cbor.decode(responseBytes); + }, + + /** + * ABCI after finalize block + * + * @param {AfterFinalizeBlockRequest} request + * + * @returns {Promise} + */ + async afterFinalizeBlock(request) { + const requestBytes = cbor.encode({ + ...request, + // cborium doesn't eat Buffers + updatedDataContractIds: request.updatedDataContractIds + .map((identifier) => Array.from(identifier)), + }); + + const responseBytes = await abciAfterFinalizeBlockAsync.call( + drive, + requestBytes, + ); + + return cbor.decode(responseBytes); + }, + }; + } +} + +// eslint-disable-next-line max-len +Drive.calculateStorageFeeDistributionAmountAndLeftovers = calculateStorageFeeDistributionAmountAndLeftoversWithStack; +Drive.FeeResult = FeeResult; + +/** + * @typedef RawBlockInfo + * @property {number} height + * @property {number} epoch + * @property {number} timeMs + */ + +/** + * @typedef InitChainRequest + * @property {number} genesisTimeMs + * @property {SystemIdentityPublicKeys} systemIdentityPublicKeys + */ + +/** + * @typedef SystemIdentityPublicKeys + * @property {RequiredIdentityPublicKeysSet} masternodeRewardSharesContractOwner + * @property {RequiredIdentityPublicKeysSet} featureFlagsContractOwner + * @property {RequiredIdentityPublicKeysSet} dpnsContractOwner + * @property {RequiredIdentityPublicKeysSet} withdrawalsContractOwner + * @property {RequiredIdentityPublicKeysSet} dashpayContractOwner + */ + +/** + * @typedef RequiredIdentityPublicKeysSet + * @property {Buffer} master + * @property {Buffer} high + */ + +/** + * @typedef InitChainResponse + */ + +/** + * @typedef BlockBeginRequest + * @property {number} blockHeight + * @property {number} blockTimeMs - timestamp in milliseconds + * @property {number} [previousBlockTimeMs] - timestamp in milliseconds + * @property {Buffer} proposerProTxHash + * @property {Buffer} validatorSetQuorumHash + * @property {number} lastSyncedCoreHeight + * @property {number} coreChainLockedHeight, + * @property {number} proposedAppVersion + * @property {number} totalHpmns + */ + +/** + * @typedef BlockBeginResponse + * @property {Buffer[]} unsignedWithdrawalTransactions + * @property {EpochInfo} epochInfo + */ + +/** + * @typedef EpochInfo + * @property {number} currentEpochIndex + * @property {boolean} isEpochChange + * @property {number} [previousEpochIndex] - Available only on epoch change + */ + +/** + * @typedef BlockEndRequest + * @property {BlockFees} fees + */ + +/** + * @typedef BlockFees + * @property {number} storageFee + * @property {number} processingFee + * @property {Object} refundsPerEpoch + */ + +/** + * @typedef BlockEndResponse + * @property {number} [proposersPaidCount] + * @property {number} [paidEpochIndex] + * @property {number} [refundedEpochsCount] + */ + +/** + * @typedef AfterFinalizeBlockRequest + * @property {Identifier[]|Buffer[]} updatedDataContractIds + */ + +/** + * @typedef AfterFinalizeBlockResponse + */ + +module.exports = Drive; diff --git a/packages/rs-drive-nodejs/FeeResult.js b/packages/rs-drive-nodejs/FeeResult.js new file mode 100644 index 00000000000..50a7b120e10 --- /dev/null +++ b/packages/rs-drive-nodejs/FeeResult.js @@ -0,0 +1,95 @@ +// This file is crated when run `npm run build`. The actual source file that +// exports those functions is ./src/lib.rs +const { + feeResultAdd, + feeResultGetStorageFee, + feeResultGetProcessingFee, + feeResultAddFees, + feeResultCreate, + feeResultGetRefunds, + feeResultSumRefundsPerEpoch, +} = require('neon-load-or-build')({ + dir: __dirname, +}); + +const { appendStack } = require('./appendStack'); + +const feeResultAddWithStack = appendStack(feeResultAdd); +const feeResultAddFeesWithStack = appendStack(feeResultAddFees); +const feeResultGetStorageFeeWithStack = appendStack(feeResultGetStorageFee); +const feeResultGetProcessingFeeWithStack = appendStack(feeResultGetProcessingFee); +const feeResultCreateWithStack = appendStack(feeResultCreate); +const feeResultGetRefundsWithStack = appendStack(feeResultGetRefunds); +const feeResultSumRefundsPerEpochWithStack = appendStack(feeResultSumRefundsPerEpoch); + +class FeeResult { + constructor(inner) { + this.inner = inner; + } + + /** + * Processing fees + * + * @returns {number} + */ + get processingFee() { + return feeResultGetProcessingFeeWithStack.call(this.inner); + } + + /** + * Storage fees + * + * @returns {number} + */ + get storageFee() { + return feeResultGetStorageFeeWithStack.call(this.inner); + } + + /** + * Credit refunds + * + * @return {{identifier: Buffer, creditsPerEpoch: Object}[]} + */ + get feeRefunds() { + return feeResultGetRefundsWithStack.call(this.inner); + } + + /** + * Sum credit refunds per epoch + * + * @returns {Object}[]} + */ + sumFeeRefundsPerEpoch() { + return feeResultSumRefundsPerEpochWithStack.call(this.inner); + } + + /** + * Adds and self assigns result between two Fee Results + * + * @param {FeeResult} feeResult + */ + add(feeResult) { + this.inner = feeResultAddWithStack.call(this.inner, feeResult.inner); + } + + /** + * @param {number} storageFee + * @param {number} processingFee + */ + addFees(storageFee, processingFee) { + feeResultAddFeesWithStack.call(this.inner, storageFee, processingFee); + } + + /** + * Create new fee result + * + * @returns {FeeResult} + */ + static create(storageFee, processingFee, feeRefunds) { + const inner = feeResultCreateWithStack(storageFee, processingFee, feeRefunds); + + return new FeeResult(inner); + } +} + +module.exports = FeeResult; diff --git a/packages/rs-drive-nodejs/GroveDB.js b/packages/rs-drive-nodejs/GroveDB.js new file mode 100644 index 00000000000..b020affb1f4 --- /dev/null +++ b/packages/rs-drive-nodejs/GroveDB.js @@ -0,0 +1,342 @@ +const { promisify } = require('util'); + +// This file is crated when run `npm run build`. The actual source file that +// exports those functions is ./src/lib.rs +const { + groveDbInsert, + groveDbGet, + groveDbFlush, + groveDbStartTransaction, + groveDbCommitTransaction, + groveDbRollbackTransaction, + groveDbAbortTransaction, + groveDbIsTransactionStarted, + groveDbDelete, + groveDbInsertIfNotExists, + groveDbPutAux, + groveDbDeleteAux, + groveDbGetAux, + groveDbQuery, + groveDbProveQuery, + groveDbRootHash, + groveDbProveQueryMany, +} = require('neon-load-or-build')({ + dir: __dirname, +}); + +const { appendStackAsync } = require('./appendStack'); + +const groveDbGetAsync = appendStackAsync(promisify(groveDbGet)); +const groveDbInsertAsync = appendStackAsync(promisify(groveDbInsert)); +const groveDbInsertIfNotExistsAsync = appendStackAsync(promisify(groveDbInsertIfNotExists)); +const groveDbDeleteAsync = appendStackAsync(promisify(groveDbDelete)); +const groveDbFlushAsync = appendStackAsync(promisify(groveDbFlush)); +const groveDbStartTransactionAsync = appendStackAsync(promisify(groveDbStartTransaction)); +const groveDbCommitTransactionAsync = appendStackAsync(promisify(groveDbCommitTransaction)); +const groveDbRollbackTransactionAsync = appendStackAsync(promisify(groveDbRollbackTransaction)); +const groveDbIsTransactionStartedAsync = appendStackAsync(promisify(groveDbIsTransactionStarted)); +const groveDbAbortTransactionAsync = appendStackAsync(promisify(groveDbAbortTransaction)); +const groveDbPutAuxAsync = appendStackAsync(promisify(groveDbPutAux)); +const groveDbDeleteAuxAsync = appendStackAsync(promisify(groveDbDeleteAux)); +const groveDbGetAuxAsync = appendStackAsync(promisify(groveDbGetAux)); +const groveDbQueryAsync = appendStackAsync(promisify(groveDbQuery)); +const groveDbProveQueryAsync = appendStackAsync(promisify(groveDbProveQuery)); +const groveDbProveQueryManyAsync = appendStackAsync(promisify(groveDbProveQueryMany)); +const groveDbRootHashAsync = appendStackAsync(promisify(groveDbRootHash)); + +// Wrapper class for the boxed `GroveDB` for idiomatic JavaScript usage +class GroveDB { + /** + * @param drive + */ + constructor(drive) { + this.db = drive; + } + + /** + * @param {Buffer[]} path + * @param {Buffer} key + * @param {boolean} [useTransaction=false] + * @returns {Promise} + */ + async get(path, key, useTransaction = false) { + return groveDbGetAsync.call(this.db, path, key, useTransaction); + } + + /** + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Element} value + * @param {boolean} [useTransaction=false] + * @returns {Promise<*>} + */ + async insert(path, key, value, useTransaction = false) { + return groveDbInsertAsync.call(this.db, path, key, value, useTransaction); + } + + /** + * @param {Buffer[]} path + * @param {Buffer} key + * @param {Element} value + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async insertIfNotExists(path, key, value, useTransaction = false) { + return groveDbInsertIfNotExistsAsync.call(this.db, path, key, value, useTransaction); + } + + /** + * + * @param {Buffer[]} path + * @param {Buffer} key + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async delete(path, key, useTransaction = false) { + return groveDbDeleteAsync.call(this.db, path, key, useTransaction); + } + + /** + * Flush data on the disk + * + * @returns {Promise} + */ + async flush() { + return groveDbFlushAsync.call(this.db); + } + + /** + * Start a transaction with isolated scope + * + * Write operations will be allowed only for the transaction + * until it's committed + * + * @return {Promise} + */ + async startTransaction() { + return groveDbStartTransactionAsync.call(this.db); + } + + /** + * Commit transaction + * + * Transaction should be started before + * + * @return {Promise} + */ + async commitTransaction() { + return groveDbCommitTransactionAsync.call(this.db); + } + + /** + * Rollback transaction to this initial state when it was created + * + * @returns {Promise} + */ + async rollbackTransaction() { + return groveDbRollbackTransactionAsync.call(this.db); + } + + /** + * Returns true if transaction started + * + * @returns {Promise} + */ + async isTransactionStarted() { + return groveDbIsTransactionStartedAsync.call(this.db); + } + + /** + * Aborts transaction + * + * @returns {Promise} + */ + async abortTransaction() { + return groveDbAbortTransactionAsync.call(this.db); + } + + /** + * Put auxiliary data + * + * @param {Buffer} key + * @param {Buffer} value + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async putAux(key, value, useTransaction = false) { + return groveDbPutAuxAsync.call(this.db, key, value, useTransaction); + } + + /** + * Delete auxiliary data + * + * @param {Buffer} key + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async deleteAux(key, useTransaction = false) { + return groveDbDeleteAuxAsync.call(this.db, key, useTransaction); + } + + /** + * Get auxiliary data + * + * @param {Buffer} key + * @param {boolean} [useTransaction=false] + * @return {Promise} + */ + async getAux(key, useTransaction = false) { + return groveDbGetAuxAsync.call(this.db, key, useTransaction); + } + + /** + * Get data using query. + * + * @param {PathQuery} query + * @param {boolean} [skipCache=false] + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async query(query, skipCache = false, useTransaction = false) { + return groveDbQueryAsync.call(this.db, query, skipCache, useTransaction); + } + + /** + * Get proof using query. + * + * @param {PathQuery} query + * @param {boolean} [verbose=false] + * @param {boolean} [useTransaction=false] + * @return {Promise<*>} + */ + async proveQuery(query, verbose = false, useTransaction = false) { + return groveDbProveQueryAsync.call(this.db, query, verbose, useTransaction); + } + + /** + * Get proof using query. + * + * @param {PathQuery[]} queries + * @param {boolean} [useTransaction=false] + * @return {Promise} + */ + async proveQueryMany(queries, useTransaction = false) { + return groveDbProveQueryManyAsync.call(this.db, queries, useTransaction); + } + + /** + * Get root hash + * + * @param {boolean} [useTransaction=false] + * @returns {Promise} + */ + async getRootHash(useTransaction = false) { + return groveDbRootHashAsync.call(this.db, useTransaction); + } +} + +/** + * @typedef Element + * @property {"item"|"reference"|"tree"} type - element type. Can be "item", "reference" or "tree" + * @property {number} [epoch] - epoch storage flag + * @property {Buffer} [ownerId] - ownerId storage flag + * @property {Buffer|Buffer[]|Object} [value] - element value + */ + +/** + * @typedef PathQuery + * @property {Buffer[]} path + * @property {SizedQuery} query + */ + +/** + * @typedef SizedQuery + * @property {Query} query + * @property {number} [limit] + * @property {number} [offset] + */ + +/** + * @typedef Query + * @property {Array< + * QueryItemKey| + * QueryItemKey| + * QueryItemRange| + * QueryItemRangeInclusive| + * QueryItemRangeFull| + * QueryItemRangeFrom| + * QueryItemRangeTo| + * QueryItemRangeToInclusive| + * QueryItemRangeAfter| + * QueryItemRangeAfterTo| + * QueryItemRangeAfterToInclusive + * >} [items] + * @property {Buffer} [subqueryPath] + * @property {Query} [subquery] + * @property {boolean} [leftToRight] + */ + +/** + * @typedef QueryItemKey + * @property {"key"} type + * @property {Buffer} key + */ + +/** + * @typedef QueryItemRange + * @property {"range"} type + * @property {Buffer} from + * @property {Buffer} to + */ + +/** + * @typedef QueryItemRangeInclusive + * @property {"rangeInclusive"} type + * @property {Buffer} from + * @property {Buffer} to + */ + +/** + * @typedef QueryItemRangeFull + * @property {"rangeFull"} type + */ + +/** + * @typedef QueryItemRangeFrom + * @property {"rangeFrom"} type + * @property {Buffer} from + */ + +/** + * @typedef QueryItemRangeTo + * @property {"rangeTo"} type + * @property {Buffer} to + */ + +/** + * @typedef QueryItemRangeToInclusive + * @property {"rangeToInclusive"} type + * @property {Buffer} to + */ + +/** + * @typedef QueryItemRangeAfter + * @property {"rangeAfter"} type + * @property {Buffer} after + */ + +/** + * @typedef QueryItemRangeAfterTo + * @property {"rangeAfterTo"} type + * @property {Buffer} after + * @property {Buffer} to + */ + +/** + * @typedef QueryItemRangeAfterToInclusive + * @property {"rangeAfterToInclusive"} type + * @property {Buffer} after + * @property {Buffer} to + */ + +module.exports = GroveDB; diff --git a/packages/rs-drive-nodejs/appendStack.js b/packages/rs-drive-nodejs/appendStack.js new file mode 100644 index 00000000000..6e8489361cc --- /dev/null +++ b/packages/rs-drive-nodejs/appendStack.js @@ -0,0 +1,36 @@ +/** + * @param {Function} fn + * @returns {(function(...[*]): Promise<*>)} + */ +function appendStackAsync(fn) { + return async function appendStackWrapper(...args) { + try { + return await fn.call(this, ...args); + } catch (e) { + e.stack = (new Error(e.message)).stack; + + throw e; + } + }; +} + +/** + * @param {Function} fn + * @returns {(function(...[*]): *)} + */ +function appendStack(fn) { + return function appendStackWrapper(...args) { + try { + return fn.call(this, ...args); + } catch (e) { + e.stack = (new Error(e.message)).stack; + + throw e; + } + }; +} + +module.exports = { + appendStack, + appendStackAsync, +}; diff --git a/packages/rs-drive-nodejs/docker/build.sh b/packages/rs-drive-nodejs/docker/build.sh new file mode 100755 index 00000000000..5a69bcd6e32 --- /dev/null +++ b/packages/rs-drive-nodejs/docker/build.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +## Setup arguments +while getopts t:a:l: flag +do + case "${flag}" in + t) target=${OPTARG};; + a) arch=${OPTARG};; + l) libc=${OPTARG};; + *) echo "invalid arguments" && exit 1;; + esac +done + +## Install multilib +apt update +apt install -y gcc-multilib +if [[ $target = "aarch64-unknown-linux-gnu" ]] +then + apt install -y gcc-aarch64-linux-gnu libstdc++-11-dev-arm64-cross +fi + +## Update toolchain +rustup update stable + +## Install build target +rustup target install $target + +chmod 777 -R /root/.cargo +mkdir -p /github/workspace/target +chmod 777 -R /github/workspace/target + +## Install Node.JS +curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - +apt install -y nodejs + +corepack enable + +yarn install + +CARGO_BUILD_TARGET=$target \ +CARGO_BUILD_PROFILE=release \ +ARCH=$arch \ +LIBC=$libc \ +yarn workspace @dashevo/rs-drive run build diff --git a/packages/rs-drive-nodejs/package.json b/packages/rs-drive-nodejs/package.json new file mode 100644 index 00000000000..eb665132ece --- /dev/null +++ b/packages/rs-drive-nodejs/package.json @@ -0,0 +1,50 @@ +{ + "name": "@dashevo/rs-drive", + "version": "0.24.0-dev.16", + "description": "Node.JS binding for Rust Drive", + "main": "Drive.js", + "scripts": { + "build": "yarn exec scripts/build.sh", + "test": "NODE_ENV=test ultra --build && mocha test", + "lint": "eslint ." + }, + "files": [ + "prebuilds", + "Drive.js", + "GroveDB.js", + "appendStack.js", + "src" + ], + "license": "MIT", + "devDependencies": { + "@dashevo/dashcore-lib": "~0.20.0", + "@dashevo/withdrawals-contract": "workspace:*", + "chai": "^4.3.4", + "dirty-chai": "^2.0.1", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.24.2", + "mocha": "^9.1.2", + "neon-cli": "^0.10.1", + "ultra-runner": "^3.10.5" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dashevo/rs-drive.git" + }, + "keywords": [ + "Dash Platform", + "Drive" + ], + "bugs": { + "url": "https://github.com/dashevo/rs-drive/issues" + }, + "homepage": "https://github.com/dashevo/rs-drive#readme", + "dependencies": { + "@dashevo/dpp": "workspace:*", + "cargo-cp-artifact": "^0.1.6", + "cbor": "^8.0.0", + "neon-load-or-build": "^2.2.2", + "neon-tag-prebuild": "github:shumkov/neon-tag-prebuild#patch-1" + } +} diff --git a/packages/rs-drive-nodejs/scripts/build.sh b/packages/rs-drive-nodejs/scripts/build.sh new file mode 100755 index 00000000000..6e169fdf681 --- /dev/null +++ b/packages/rs-drive-nodejs/scripts/build.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +PROFILE_ARG="" +FEATURE_FLAG="" + +if [ -n "$CARGO_BUILD_PROFILE" ]; then + if [ "$CARGO_BUILD_PROFILE" == "release" ]; then + PROFILE_ARG="--release" + elif [ "$CARGO_BUILD_PROFILE" != "debug" ]; then + PROFILE_ARG="--profile $CARGO_BUILD_PROFILE" + fi +fi + +if [ -n "$NODE_ENV" ]; then + if [ "$NODE_ENV" == "test" ]; then + FEATURE_FLAG="--features enable-mocking" + fi +fi + +cargo-cp-artifact -ac drive-nodejs native/index.node -- \ + cargo build --message-format=json-render-diagnostics $PROFILE_ARG $FEATURE_FLAG \ + && neon-tag-prebuild \ + && rm -rf native diff --git a/packages/rs-drive-nodejs/src/converter.rs b/packages/rs-drive-nodejs/src/converter.rs new file mode 100644 index 00000000000..13d49f334c4 --- /dev/null +++ b/packages/rs-drive-nodejs/src/converter.rs @@ -0,0 +1,534 @@ +use drive::dpp::block::block_info::BlockInfo; +use drive::dpp::block::epoch::Epoch; +use drive::dpp::identity::{IdentityPublicKey, KeyID, KeyType, Purpose, SecurityLevel}; +use drive::dpp::platform_value::BinaryData; +use drive::drive::flags::StorageFlags; +use drive::fee::credits::Credits; +use drive::fee::epoch::CreditsPerEpoch; +use drive::grovedb::reference_path::ReferencePathType; +use drive::grovedb::{Element, PathQuery, Query, SizedQuery}; +use neon::prelude::*; +use neon::types::buffer::TypedArray; +use num::FromPrimitive; +use std::borrow::Borrow; +use std::num::ParseIntError; + +fn element_to_string(element: &Element) -> &'static str { + match element { + Element::Item(..) => "item", + Element::SumItem(..) => "sumItem", + Element::Reference(..) => "reference", + Element::Tree(..) => "tree", + Element::SumTree(..) => "sumTree", + } +} + +pub fn js_object_to_element<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let js_element_type: Handle = js_object.get(cx, "type")?; + + let element_type: String = js_element_type.value(cx); + + let js_element_epoch: Option> = js_object.get_opt(cx, "epoch")?; + + let element_flags = if let Some(js_epoch) = js_element_epoch { + let epoch = u16::try_from(js_epoch.value(cx) as i64) + .or_else(|_| cx.throw_range_error("`epochs` must fit in u16"))?; + + let js_maybe_owner_id: Option> = js_object.get_opt(cx, "ownerId")?; + + let maybe_owner_id = js_maybe_owner_id + .map(|js_buffer| js_buffer_to_identifier(cx, js_buffer)) + .transpose()?; + + let storage_flags = StorageFlags::new_single_epoch(epoch, maybe_owner_id); + + storage_flags.to_some_element_flags() + } else { + None + }; + + match element_type.as_str() { + "item" => { + let js_buffer: Handle = js_object.get(cx, "value")?; + let item = js_buffer_to_vec_u8(cx, js_buffer); + + Ok(Element::new_item_with_flags(item, element_flags)) + } + "reference" => { + let js_object: Handle = js_object.get(cx, "value")?; + let reference = js_object_to_reference(cx, js_object)?; + + Ok(Element::new_reference_with_flags(reference, element_flags)) + } + "tree" => Ok(Element::empty_tree_with_flags(element_flags)), + _ => cx.throw_error(format!("Unexpected element type {}", element_type)), + } +} + +fn js_object_to_reference<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let js_reference_type: Handle = js_object.get(cx, "type")?; + let reference_type: String = js_reference_type.value(cx); + + match reference_type.as_str() { + "absolutePathReference" => { + let js_path: Handle = js_object.get(cx, "path")?; + let path = js_array_of_buffers_to_vec(cx, js_path)?; + + Ok(ReferencePathType::AbsolutePathReference(path)) + } + "upstreamRootHeightReference" => { + let js_path: Handle = js_object.get(cx, "path")?; + let path = js_array_of_buffers_to_vec(cx, js_path)?; + + let js_relativity_index: Handle = js_object.get(cx, "relativityIndex")?; + let relativity_index_f64: f64 = js_relativity_index.value(cx); + let relativity_index_option: Option = FromPrimitive::from_f64(relativity_index_f64); + let relativity_index: u8 = relativity_index_option + .ok_or(()) + .or_else(|_| cx.throw_error("cannot convert relativity_index from f64 to u8"))?; + + Ok(ReferencePathType::UpstreamRootHeightReference( + relativity_index, + path, + )) + } + "upstreamFromElementHeightReference" => { + let js_path: Handle = js_object.get(cx, "path")?; + let path = js_array_of_buffers_to_vec(cx, js_path)?; + + let js_relativity_index: Handle = js_object.get(cx, "relativityIndex")?; + let relativity_index_f64: f64 = js_relativity_index.value(cx); + let relativity_index_option: Option = FromPrimitive::from_f64(relativity_index_f64); + let relativity_index: u8 = relativity_index_option + .ok_or(()) + .or_else(|_| cx.throw_error("cannot convert relativity_index from f64 to u8"))?; + + Ok(ReferencePathType::UpstreamFromElementHeightReference( + relativity_index, + path, + )) + } + "cousinReference" => { + let js_key: Handle = js_object.get(cx, "key")?; + let key = js_buffer_to_vec_u8(cx, js_key); + + Ok(ReferencePathType::CousinReference(key)) + } + "siblingReference" => { + let js_key: Handle = js_object.get(cx, "key")?; + let key = js_buffer_to_vec_u8(cx, js_key); + + Ok(ReferencePathType::SiblingReference(key)) + } + _ => cx.throw_error(format!("Unexpected reference type {}", reference_type)), + } +} + +pub fn element_to_js_object<'a, C: Context<'a>>( + cx: &mut C, + element: Element, +) -> NeonResult> { + let js_object = cx.empty_object(); + let js_type_string = cx.string(element_to_string(&element)); + js_object.set(cx, "type", js_type_string)?; + + let maybe_js_value: Option> = match element { + Element::Item(item, _) => { + let js_buffer = JsBuffer::external(cx, item); + Some(js_buffer.upcast()) + } + Element::SumItem(number, _) => { + let js_number = cx.number(number as f64).upcast(); + Some(js_number) + } + Element::Reference(reference, _, _) => { + let reference = reference_to_dictionary(cx, reference)?; + Some(reference) + } + Element::Tree(Some(tree), _) | Element::SumTree(Some(tree), ..) => { + let js_buffer = JsBuffer::external(cx, tree); + Some(js_buffer.upcast()) + } + Element::Tree(None, _) | Element::SumTree(None, ..) => None, + }; + + if let Some(js_value) = maybe_js_value { + js_object.set(cx, "value", js_value)?; + } + + Ok(js_object.upcast()) +} + +pub fn nested_vecs_to_js<'a, C: Context<'a>>( + cx: &mut C, + v: Vec>, +) -> NeonResult> { + let js_array: Handle = cx.empty_array(); + + for (index, bytes) in v.iter().enumerate() { + let js_buffer = JsBuffer::external(cx, bytes.clone()); + let js_value = js_buffer.as_value(cx); + js_array.set(cx, index as u32, js_value)?; + } + + Ok(js_array.upcast()) +} + +pub fn reference_to_dictionary<'a, C: Context<'a>>( + cx: &mut C, + reference: ReferencePathType, +) -> NeonResult> { + let js_object: Handle = cx.empty_object(); + + match reference { + ReferencePathType::AbsolutePathReference(path) => { + let js_type_name = cx.string("absolutePathReference"); + let js_path = nested_vecs_to_js(cx, path)?; + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "path", js_path)?; + } + ReferencePathType::UpstreamRootHeightReference(relativity_index, path) => { + let js_type_name = cx.string("upstreamRootHeightReference"); + let js_relativity_index = cx.number(relativity_index); + let js_path = nested_vecs_to_js(cx, path)?; + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "relativityIndex", js_relativity_index)?; + js_object.set(cx, "path", js_path)?; + } + ReferencePathType::UpstreamFromElementHeightReference(relativity_index, path) => { + let js_type_name = cx.string("upstreamFromElementHeightReference"); + let js_relativity_index = cx.number(relativity_index); + let js_path = nested_vecs_to_js(cx, path)?; + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "relativityIndex", js_relativity_index)?; + js_object.set(cx, "path", js_path)?; + } + ReferencePathType::CousinReference(key) => { + let js_type_name = cx.string("cousinReference"); + let js_key = JsBuffer::external(cx, key); + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "key", js_key)?; + } + ReferencePathType::SiblingReference(key) => { + let js_type_name = cx.string("siblingReference"); + let js_key = JsBuffer::external(cx, key); + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "key", js_key)?; + } + ReferencePathType::RemovedCousinReference(path) => { + let js_type_name = cx.string("removedCousinReference"); + let js_path = nested_vecs_to_js(cx, path)?; + + js_object.set(cx, "type", js_type_name)?; + js_object.set(cx, "path", js_path)?; + } + } + + Ok(js_object.upcast()) +} + +pub fn js_buffer_to_identifier<'a, C: Context<'a>>( + cx: &mut C, + js_buffer: Handle, +) -> NeonResult<[u8; 32]> { + // let guard = cx.lock(); + + let key_memory_view = js_buffer.borrow(); + + // let key_buffer = js_buffer.deref(); + // let key_memory_view = js_buffer.borrow(&guard); + let key_slice: &[u8] = key_memory_view.as_slice(cx); + <[u8; 32]>::try_from(key_slice).or_else(|_| cx.throw_type_error("hash must be 32 bytes long")) +} + +pub fn js_buffer_to_vec_u8<'a, C: Context<'a>>(cx: &mut C, js_buffer: Handle) -> Vec { + // let guard = cx.lock(); + + let key_memory_view = js_buffer.borrow(); + + // let key_buffer = js_buffer.deref(); + // let key_memory_view = js_buffer.borrow(&guard); + let key_slice: &[u8] = key_memory_view.as_slice(cx); + key_slice.to_vec() +} + +pub fn js_array_of_buffers_to_vec<'a, C: Context<'a>>( + cx: &mut C, + js_array: Handle, +) -> NeonResult>> { + let buf_vec = js_array.to_vec(cx)?; + let mut vec: Vec> = Vec::with_capacity(buf_vec.len()); + + for buf in buf_vec { + let js_buffer_handle = buf.downcast_or_throw::(cx)?; + vec.push(js_buffer_to_vec_u8(cx, js_buffer_handle)); + } + + Ok(vec) +} + +pub fn js_array_of_buffers_to_identifiers<'a, C: Context<'a>>( + cx: &mut C, + js_array: Handle, +) -> NeonResult> { + let buf_vec = js_array.to_vec(cx)?; + let mut vec: Vec<[u8; 32]> = Vec::with_capacity(buf_vec.len()); + + for buf in buf_vec { + let js_buffer_handle = buf.downcast_or_throw::(cx)?; + vec.push(js_buffer_to_identifier(cx, js_buffer_handle)?); + } + + Ok(vec) +} + +pub fn js_value_to_option<'a, T: Value, C: Context<'a>>( + cx: &mut C, + js_value: Handle<'a, JsValue>, +) -> NeonResult>> { + if js_value.is_a::(cx) || js_value.is_a::(cx) { + Ok(None) + } else { + Ok(Some(js_value.downcast_or_throw::(cx)?)) + } +} + +fn js_object_get_vec_u8<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, + field: &str, +) -> NeonResult> { + let buffer: Handle = js_object.get(cx, field)?; + + Ok(js_buffer_to_vec_u8(cx, buffer)) +} + +fn js_object_to_query<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let items: Handle = js_object.get(cx, "items")?; + let mut query = Query::new(); + for js_item in items.to_vec(cx)? { + let item = js_item.downcast_or_throw::(cx)?; + + let item_type: Handle = item.get(cx, "type")?; + let item_type = item_type.value(cx); + + match item_type.as_ref() { + "key" => { + query.insert_key(js_object_get_vec_u8(cx, item, "key")?); + } + "range" => { + let from = js_object_get_vec_u8(cx, item, "from")?; + let to = js_object_get_vec_u8(cx, item, "to")?; + query.insert_range(from..to); + } + "rangeInclusive" => { + let from = js_object_get_vec_u8(cx, item, "from")?; + let to = js_object_get_vec_u8(cx, item, "to")?; + query.insert_range_inclusive(from..=to); + } + "rangeFull" => { + query.insert_all(); + } + "rangeFrom" => { + query.insert_range_from(js_object_get_vec_u8(cx, item, "from")?..); + } + "rangeTo" => { + query.insert_range_to(..js_object_get_vec_u8(cx, item, "to")?); + } + "rangeToInclusive" => { + query.insert_range_to_inclusive(..=js_object_get_vec_u8(cx, item, "to")?); + } + "rangeAfter" => { + query.insert_range_after(js_object_get_vec_u8(cx, item, "after")?..); + } + "rangeAfterTo" => { + let after = js_object_get_vec_u8(cx, item, "after")?; + let to = js_object_get_vec_u8(cx, item, "to")?; + query.insert_range_after_to(after..to); + } + "rangeAfterToInclusive" => { + let after = js_object_get_vec_u8(cx, item, "after")?; + let to = js_object_get_vec_u8(cx, item, "to")?; + query.insert_range_after_to_inclusive(after..=to); + } + _ => { + cx.throw_range_error("query item type is not supported")?; + } + } + } + + let js_subquery_path = js_object.get(cx, "subqueryPath")?; + let subquery_path = js_value_to_option::(cx, js_subquery_path)? + .map(|x| js_array_of_buffers_to_vec(cx, x)) + .transpose()?; + let js_subquery = js_object.get(cx, "subquery")?; + let subquery = js_value_to_option::(cx, js_subquery)? + .map(|x| js_object_to_query(cx, x)) + .transpose()?; + let js_left_to_right = js_object.get(cx, "leftToRight")?; + let left_to_right = + js_value_to_option::(cx, js_left_to_right)?.map(|x| x.value(cx)); + + query.default_subquery_branch.subquery_path = subquery_path; + query.default_subquery_branch.subquery = subquery.map(Box::new); + query.left_to_right = left_to_right.unwrap_or(true); + + Ok(query) +} + +fn js_object_to_sized_query<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let query: Handle = js_object.get(cx, "query")?; + let query = js_object_to_query(cx, query)?; + + let js_limit = js_object.get(cx, "limit")?; + let limit: Option = js_value_to_option::(cx, js_limit)? + .map(|x| { + u16::try_from(x.value(cx) as i64) + .or_else(|_| cx.throw_range_error("`limit` must fit in u16")) + }) + .transpose()?; + let js_offset = js_object.get(cx, "offset")?; + let offset: Option = js_value_to_option::(cx, js_offset)? + .map(|x| { + u16::try_from(x.value(cx) as i64) + .or_else(|_| cx.throw_range_error("`offset` must fit in u16")) + }) + .transpose()?; + + Ok(SizedQuery::new(query, limit, offset)) +} + +pub fn js_path_query_to_path_query<'a, C: Context<'a>>( + cx: &mut C, + js_path_query: Handle, +) -> NeonResult { + let js_path = js_path_query.get(cx, "path")?; + let path = js_array_of_buffers_to_vec(cx, js_path)?; + let js_query = js_path_query.get(cx, "query")?; + let query = js_object_to_sized_query(cx, js_query)?; + + Ok(PathQuery::new(path, query)) +} + +pub fn js_object_to_block_info<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let js_height: Handle = js_object.get(cx, "height")?; + let js_epoch: Handle = js_object.get(cx, "epoch")?; + let js_time: Handle = js_object.get(cx, "timeMs")?; + + let epoch = Epoch::new(js_epoch.value(cx) as u16).unwrap(); + + let block_info = BlockInfo { + height: js_height.value(cx) as u64, + time_ms: js_time.value(cx) as u64, + epoch, + core_height: 1, + }; + + Ok(block_info) +} + +pub fn js_object_to_identity_public_key<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let js_id: Handle = js_object.get(cx, "id")?; + let js_purpose: Handle = js_object.get(cx, "purpose")?; + let js_security_level: Handle = js_object.get(cx, "securityLevel")?; + let js_key_type: Handle = js_object.get(cx, "type")?; + let js_read_only: Handle = js_object.get(cx, "readOnly")?; + let js_data: Handle = js_object.get(cx, "data")?; + let js_disabled_at: Handle = js_object.get(cx, "disabledAt")?; + + let id = js_id.value(cx) as KeyID; + + let purpose = Purpose::try_from(js_purpose.value(cx) as u8) + .or_else(|_| cx.throw_range_error("`purpose` value is incorrect"))?; + + let security_level = SecurityLevel::try_from(js_security_level.value(cx) as u8) + .or_else(|_| cx.throw_range_error("`securityLevel` value is incorrect"))?; + + let key_type = KeyType::try_from(js_key_type.value(cx) as u8) + .or_else(|_| cx.throw_range_error("`keyType` value is incorrect"))?; + + let read_only = js_read_only.value(cx); + + let data = js_buffer_to_vec_u8(cx, js_data); + + let disabled_at: Option = js_value_to_option::(cx, js_disabled_at)? + .map(|x| { + u64::try_from(x.value(cx) as i64) + .or_else(|_| cx.throw_range_error("`offset` must fit in u16")) + }) + .transpose()?; + + Ok(IdentityPublicKey { + id, + purpose, + security_level, + key_type, + read_only, + data: BinaryData::new(data), + disabled_at, + }) +} + +pub fn js_array_to_keys<'a, C: Context<'a>>( + cx: &mut C, + js_array: Handle, +) -> NeonResult> { + let keys = js_array + .to_vec(cx)? + .into_iter() + .map(|js_value| { + let js_key = js_value.downcast_or_throw::(cx)?; + let key = js_object_to_identity_public_key(cx, js_key)?; + + Ok(key) + }) + .collect::>()?; + + Ok(keys) +} + +pub fn js_object_to_fee_refunds<'a, C: Context<'a>>( + cx: &mut C, + js_object: Handle, +) -> NeonResult { + let mut fee_refunds: CreditsPerEpoch = Default::default(); + + for js_epoch_index_value in js_object.get_own_property_names(cx)?.to_vec(cx)? { + let js_epoch_index = js_epoch_index_value.downcast_or_throw::(cx)?; + + let epoch_index = js_epoch_index + .value(cx) + .parse() + .or_else(|e: ParseIntError| cx.throw_error(e.to_string()))?; + + let js_credits: Handle = js_object.get(cx, js_epoch_index)?; + let credits = js_credits.value(cx) as Credits; + + fee_refunds.insert(epoch_index, credits); + } + + Ok(fee_refunds) +} diff --git a/packages/rs-drive-nodejs/src/fee/mod.rs b/packages/rs-drive-nodejs/src/fee/mod.rs new file mode 100644 index 00000000000..2b0f5c68e78 --- /dev/null +++ b/packages/rs-drive-nodejs/src/fee/mod.rs @@ -0,0 +1,39 @@ +use drive::fee::credits::Credits; +use drive::fee::epoch::distribution::calculate_storage_fee_refund_amount_and_leftovers; +use drive::fee::epoch::EpochIndex; +use neon::prelude::*; + +pub mod result; + +pub fn js_calculate_storage_fee_distribution_amount_and_leftovers( + mut cx: FunctionContext, +) -> JsResult { + let js_storage_fees = cx.argument::(0)?; + let storage_fees = js_storage_fees.value(&mut cx) as Credits; + + let js_start_epoch_index = cx.argument::(1)?; + + let start_epoch_index = EpochIndex::try_from(js_start_epoch_index.value(&mut cx) as i64) + .or_else(|_| cx.throw_range_error("`startEpochIndex` must fit in u16"))?; + + let js_skip_up_to_epoch_index = cx.argument::(2)?; + let current_epoch_index = EpochIndex::try_from(js_skip_up_to_epoch_index.value(&mut cx) as i64) + .or_else(|_| cx.throw_range_error("`startEpochIndex` must fit in u16"))?; + + let (amount, leftovers) = calculate_storage_fee_refund_amount_and_leftovers( + storage_fees, + start_epoch_index, + current_epoch_index, + ) + .or_else(|e| cx.throw_error(e.to_string()))?; + + let js_array = cx.empty_array(); + + let js_amount = cx.number(amount as f64); + let js_leftovers = cx.number(leftovers as f64); + + js_array.set(&mut cx, 0, js_amount)?; + js_array.set(&mut cx, 1, js_leftovers)?; + + Ok(js_array) +} diff --git a/packages/rs-drive-nodejs/src/fee/result.rs b/packages/rs-drive-nodejs/src/fee/result.rs new file mode 100644 index 00000000000..759c8fa9756 --- /dev/null +++ b/packages/rs-drive-nodejs/src/fee/result.rs @@ -0,0 +1,167 @@ +use crate::converter::{js_buffer_to_identifier, js_object_to_fee_refunds}; +use drive::fee::result::refunds::{CreditsPerEpochByIdentifier, FeeRefunds}; +use drive::fee::result::FeeResult; +use neon::prelude::*; +use std::ops::Deref; + +pub struct FeeResultWrapper(FeeResult); + +impl FeeResultWrapper { + pub fn new(fee_result: FeeResult) -> Self { + FeeResultWrapper(fee_result) + } + + pub fn create(mut cx: FunctionContext) -> JsResult> { + let storage_fee = cx.argument::(0)?.value(&mut cx) as u64; + let processing_fee = cx.argument::(1)?.value(&mut cx) as u64; + let js_fee_refunds = cx.argument::(2)?.to_vec(&mut cx)?; + + let mut credits_per_epoch_by_identifier = CreditsPerEpochByIdentifier::new(); + for item in js_fee_refunds { + let js_refunds = item.downcast_or_throw::(&mut cx)?; + + let js_identifier: Handle = js_refunds.get(&mut cx, "identifier")?; + let identifier = js_buffer_to_identifier(&mut cx, js_identifier)?; + + let js_credits_per_epoch: Handle = + js_refunds.get(&mut cx, "creditsPerEpoch")?; + + let credits_per_epoch = js_object_to_fee_refunds(&mut cx, js_credits_per_epoch)?; + + credits_per_epoch_by_identifier.insert(identifier, credits_per_epoch); + } + + let fee_result = FeeResult { + storage_fee, + processing_fee, + fee_refunds: FeeRefunds(credits_per_epoch_by_identifier), + ..Default::default() + }; + + Ok(cx.boxed(Self::new(fee_result))) + } + + pub fn get_storage_fee(mut cx: FunctionContext) -> JsResult { + let fee_result_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // TODO: We might lose with the conversion + Ok(cx.number(fee_result_self.0.storage_fee as f64)) + } + + pub fn get_processing_fee(mut cx: FunctionContext) -> JsResult { + let fee_result_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // TODO: We might lose with the conversion + Ok(cx.number(fee_result_self.0.processing_fee as f64)) + } + + pub fn add(mut cx: FunctionContext) -> JsResult> { + let fee_result_wrapper_to_add = cx.argument::>(0)?; + + let fee_result_wrapper_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // TODO: Figure out how to get mutable link from JsBox + let mut fee_result_sum = fee_result_wrapper_self.deref().deref().deref().clone(); + + // TODO: To avoid clone we need to be able to pass a reference to + // FeeResult#checked_add_assign + let fee_result_to_add = fee_result_wrapper_to_add.deref().deref().deref().clone(); + + fee_result_sum + .checked_add_assign(fee_result_to_add) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.boxed(Self::new(fee_result_sum))) + } + + pub fn add_fees(mut cx: FunctionContext) -> JsResult> { + let storage_fee = cx.argument::(0)?.value(&mut cx) as u64; + let processing_fee = cx.argument::(1)?.value(&mut cx) as u64; + + let fee_result_wrapper_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // TODO: Figure out how to get mutable link from JsBox + let mut fee_result_sum = fee_result_wrapper_self.deref().deref().deref().clone(); + + let fee_result_to_add = FeeResult::default_with_fees(storage_fee, processing_fee); + + fee_result_sum + .checked_add_assign(fee_result_to_add) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.boxed(Self::new(fee_result_sum))) + } + + pub fn get_refunds(mut cx: FunctionContext) -> JsResult { + let fee_result_wrapper_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // Clone fee result because IntMap doesn't implement iterator for reference + let fee_result = fee_result_wrapper_self.deref().deref().deref().clone(); + + let js_fee_refunds: Handle = cx.empty_array(); + + for (index, (identifier, credits_per_epoch)) in + fee_result.fee_refunds.into_iter().enumerate() + { + let js_epoch_index_map = cx.empty_object(); + + for (epoch, credits) in credits_per_epoch { + // TODO: We could miss fees here + let js_credits = cx.number(credits as f64); + + js_epoch_index_map.set(&mut cx, epoch.to_string().as_str(), js_credits)?; + } + + let js_identity_to_epochs = cx.empty_object(); + + let js_identifier = JsBuffer::external(&mut cx, identifier); + + js_identity_to_epochs.set(&mut cx, "identifier", js_identifier)?; + js_identity_to_epochs.set(&mut cx, "creditsPerEpoch", js_epoch_index_map)?; + + js_fee_refunds.set(&mut cx, index as u32, js_identity_to_epochs)?; + } + + Ok(js_fee_refunds) + } + + pub fn get_refunds_per_epoch(mut cx: FunctionContext) -> JsResult { + let fee_result_wrapper_self = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + // Clone fee result because IntMap doesn't implement iterator for reference + let fee_result = fee_result_wrapper_self.deref().deref().deref().clone(); + + let js_credits_per_epoch = cx.empty_object(); + + for (epoch_index, epoch_credits) in fee_result.fee_refunds.sum_per_epoch() { + // TODO: We could miss fees here + let js_credits = cx.number(epoch_credits as f64); + + js_credits_per_epoch.set(&mut cx, epoch_index.to_string().as_str(), js_credits)?; + } + + Ok(js_credits_per_epoch) + } +} + +impl Finalize for FeeResultWrapper {} + +impl Deref for FeeResultWrapper { + type Target = FeeResult; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/packages/rs-drive-nodejs/src/lib.rs b/packages/rs-drive-nodejs/src/lib.rs new file mode 100644 index 00000000000..eeb250c775e --- /dev/null +++ b/packages/rs-drive-nodejs/src/lib.rs @@ -0,0 +1,3682 @@ +mod converter; +mod fee; + +use drive::drive::config::DriveConfig; +use drive_abci::config::{CoreConfig, CoreRpcConfig, PlatformConfig}; + +use std::ops::Deref; +use std::{option::Option::None, path::Path, sync::mpsc, thread}; + +use crate::converter::js_object_to_fee_refunds; +use crate::fee::result::FeeResultWrapper; + +use drive::dpp::block::epoch::Epoch; +use drive::dpp::identity::{KeyID, TimestampMillis}; +use drive::dpp::prelude::Revision; +use drive::dpp::Convertible; +use drive::drive::flags::StorageFlags; +use drive::drive::query::QuerySerializedDocumentsOutcome; +use drive::error::Error; +use drive::fee::credits::Credits; +use drive::grovedb::{PathQuery, Transaction}; +use drive::query::TransactionArg; +use drive_abci::abci::messages::{ + AfterFinalizeBlockRequest, BlockBeginRequest, BlockEndRequest, BlockFees, InitChainRequest, + Serializable, +}; +use drive_abci::platform::Platform; +use drive_abci::rpc::core::DefaultCoreRPC; +use fee::js_calculate_storage_fee_distribution_amount_and_leftovers; +use neon::prelude::*; + +type PlatformCallback = + Box FnOnce(&'a Platform, TransactionArg, &Channel) + Send>; +type UnitCallback = Box; +type ErrorCallback = Box) + Send>; +type TransactionCallback = + Box, Result<(), String>, &Channel) + Send>; + +// Messages sent on the drive channel +enum PlatformWrapperMessage { + // Callback to be executed + Callback(PlatformCallback), + // Indicates that the thread should be stopped and connection closed + Close(UnitCallback), + StartTransaction(TransactionCallback), + CommitTransaction(ErrorCallback), + RollbackTransaction(ErrorCallback), + AbortTransaction(ErrorCallback), + Flush(UnitCallback), +} + +struct PlatformWrapper { + tx: mpsc::Sender, +} + +// Internal wrapper logic. Needed to avoid issues with passing threads to +// node.js. Avoiding thread conflicts by having a dedicated thread for the +// groveDB instance and uses events to communicate with it +impl PlatformWrapper { + // Creates a new instance of `DriveWrapper` + // + // 1. Creates a connection and a channel + // 2. Spawns a thread and moves the channel receiver and connection to it + // 3. On a separate thread, read closures off the channel and execute with + // access to the connection. + fn new(cx: &mut FunctionContext) -> NeonResult { + // Drive's configuration + let path_string = cx.argument::(0)?.value(cx); + let platform_config = cx.argument::(1)?; + + let drive_config: Handle = platform_config.get(cx, "drive")?; + let core_config: Handle = platform_config.get(cx, "core")?; + let core_rpc_config: Handle = core_config.get(cx, "rpc")?; + + let js_core_rpc_url: Handle = core_rpc_config.get(cx, "url")?; + let js_core_rpc_username: Handle = core_rpc_config.get(cx, "username")?; + let js_core_rpc_password: Handle = core_rpc_config.get(cx, "password")?; + + let core_rpc_url = js_core_rpc_url.value(cx); + let core_rpc_username = js_core_rpc_username.value(cx); + let core_rpc_password = js_core_rpc_password.value(cx); + + let js_data_contracts_cache_size: Handle = + drive_config.get(cx, "dataContractsGlobalCacheSize")?; + let data_contracts_global_cache_size = + u64::try_from(js_data_contracts_cache_size.value(cx) as i64).or_else(|_| { + cx.throw_range_error("`dataContractsGlobalCacheSize` must fit in u64") + })?; + + let js_data_contracts_transactional_cache_size: Handle = + drive_config.get(cx, "dataContractsBlockCacheSize")?; + let data_contracts_block_cache_size = u64::try_from( + js_data_contracts_transactional_cache_size.value(cx) as i64, + ) + .or_else(|_| cx.throw_range_error("`dataContractsBlockCacheSize` must fit in u64"))?; + + // Channel for sending callbacks to execute on the Drive connection thread + let (tx, rx) = mpsc::channel::(); + + // Create an `Channel` for calling back to JavaScript. It is more efficient + // to create a single channel and re-use it for all database callbacks. + // The JavaScript process will not exit as long as this channel has not been + // dropped. + let channel = cx.channel(); + + let sender = tx.clone(); + + // Spawn a thread for processing database queries + // This will not block the JavaScript main thread and will continue executing + // concurrently. + thread::spawn(move || { + let path = Path::new(&path_string); + // Open a connection to groveDb, this will be moved to a separate thread + + let drive_config = DriveConfig { + data_contracts_global_cache_size, + data_contracts_block_cache_size, + ..Default::default() + }; + //core_addr: Vec<&str> + let core_addr: Vec<&str> = core_rpc_url.split(':').collect(); + let core_host = core_addr.first().expect("missing core address"); + let core_port = core_addr + .get(1) + .expect("missing port number in core address"); + let core_config = CoreConfig { + rpc: CoreRpcConfig { + host: core_host.to_string(), + port: core_port.to_string(), + + username: core_rpc_username, + password: core_rpc_password, + }, + ..Default::default() + }; + + let platform_config = PlatformConfig { + drive: Some(drive_config), + core: core_config, + verify_sum_trees: true, + ..Default::default() + }; + + // TODO: think how to pass this error to JS + let mut platform: Platform = + Platform::::open(path, Some(platform_config)).unwrap(); + + // if cfg!(feature = "enable-mocking") { + // platform.mock_core_rpc_client(); + // } + + let mut maybe_transaction: Option = None; + + // Blocks until a callback is available + // When the instance of `Database` is dropped, the channel will be closed + // and `rx.recv()` will return an `Err`, ending the loop and terminating + // the thread. + while let Ok(message) = rx.recv() { + match message { + PlatformWrapperMessage::Callback(callback) => { + // The connection and channel are owned by the thread, but _lent_ to + // the callback. The callback has exclusive access to the connection + // for the duration of the callback. + callback(&platform, maybe_transaction.as_ref(), &channel); + } + // Immediately close the connection, even if there are pending messages + PlatformWrapperMessage::Close(callback) => { + drop(maybe_transaction); + drop(platform); + + callback(&channel); + break; + } + // Flush message + PlatformWrapperMessage::Flush(callback) => { + platform.drive.grove.flush().unwrap(); + callback(&channel); + } + PlatformWrapperMessage::StartTransaction(callback) => { + let result = if maybe_transaction.is_some() { + Err("transaction is already started".to_string()) + } else { + let transaction = platform.drive.grove.start_transaction(); + + maybe_transaction = Some(transaction); + + Ok(()) + }; + + callback(sender.clone(), result, &channel); + } + PlatformWrapperMessage::CommitTransaction(callback) => { + let result = if maybe_transaction.is_some() { + let mut drive_cache = platform.drive.cache.write().unwrap(); + + drive_cache.cached_contracts.merge_block_cache(); + + drive_cache.cached_contracts.clear_block_cache(); + + platform + .drive + .commit_transaction(maybe_transaction.take().unwrap()) + .map_err(|err| err.to_string()) + } else { + Err("transaction is not started".to_string()) + }; + + callback(&channel, result); + } + PlatformWrapperMessage::RollbackTransaction(callback) => { + let result = if let Some(transaction) = &maybe_transaction { + let mut drive_cache = platform.drive.cache.write().unwrap(); + + drive_cache.cached_contracts.clear_block_cache(); + + platform + .drive + .rollback_transaction(transaction) + .map_err(|err| err.to_string()) + } else { + Err("transaction is not started".to_string()) + }; + + callback(&channel, result); + } + PlatformWrapperMessage::AbortTransaction(callback) => { + let result = if maybe_transaction.is_some() { + let mut drive_cache = platform.drive.cache.write().unwrap(); + + drive_cache.cached_contracts.clear_block_cache(); + + drop(maybe_transaction.take()); + + Ok(()) + } else { + Err("transaction is not started".to_string()) + }; + + callback(&channel, result); + } + } + } + }); + + Ok(Self { tx }) + } + + // Idiomatic rust would take an owned `self` to prevent use after close + // However, it's not possible to prevent JavaScript from continuing to hold a + // closed database + fn close( + &self, + callback: impl FnOnce(&Channel) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::Close(Box::new(callback))) + } + + fn send_to_drive_thread( + &self, + callback: impl for<'a> FnOnce(&'a Platform, TransactionArg, &Channel) + + Send + + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::Callback(Box::new(callback))) + } + + fn start_transaction( + &self, + callback: impl FnOnce(mpsc::Sender, Result<(), String>, &Channel) + + Send + + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::StartTransaction(Box::new(callback))) + } + + fn commit_transaction( + &self, + callback: impl FnOnce(&Channel, Result<(), String>) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::CommitTransaction(Box::new( + callback, + ))) + } + + fn rollback_transaction( + &self, + callback: impl FnOnce(&Channel, Result<(), String>) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::RollbackTransaction(Box::new( + callback, + ))) + } + + fn abort_transaction( + &self, + callback: impl FnOnce(&Channel, Result<(), String>) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::AbortTransaction(Box::new(callback))) + } + + // Idiomatic rust would take an owned `self` to prevent use after close + // However, it's not possible to prevent JavaScript from continuing to hold a + // closed database + fn flush( + &self, + callback: impl FnOnce(&Channel) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(PlatformWrapperMessage::Flush(Box::new(callback))) + } +} + +// Ensures that DriveWrapper is properly disposed when the corresponding JS +// object gets garbage collected +impl Finalize for PlatformWrapper {} + +// External wrapper logic +impl PlatformWrapper { + // Create a new instance of `Drive` and place it inside a `JsBox` + // JavaScript can hold a reference to a `JsBox`, but the contents are opaque + fn js_open(mut cx: FunctionContext) -> JsResult> { + let drive_wrapper = + PlatformWrapper::new(&mut cx).or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.boxed(drive_wrapper)) + } + + /// Sends a message to the DB thread to stop the thread and dispose the + /// groveDb instance owned by it, then calls js callback passed as a first + /// argument to the function + fn js_close(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + drive + .close(|channel| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = + vec![task_context.null().upcast()]; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_create_initial_state_structure(mut cx: FunctionContext) -> JsResult { + let js_using_transaction = cx.argument::(0)?; + let js_callback = cx.argument::(1)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let execution_result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .create_initial_state_structure(transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match execution_result { + Ok(_) => vec![task_context.null().upcast()], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_contract(mut cx: FunctionContext) -> JsResult { + let js_contract_id = cx.argument::(0)?; + let js_maybe_epoch_index = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + let js_callback = cx.argument::(3)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + + let maybe_epoch: Option = if !js_maybe_epoch_index.is_a::(&mut cx) { + let js_epoch_index = js_maybe_epoch_index.downcast_or_throw::(&mut cx)?; + + let epoch_index = u16::try_from(js_epoch_index.value(&mut cx) as i64) + .or_else(|_| cx.throw_range_error("`epochs` must fit in u16"))?; + + let epoch = Epoch::new(epoch_index).unwrap(); + + Some(epoch) + } else { + None + }; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .get_contract_with_fetch_info( + contract_id, + maybe_epoch.as_ref(), + true, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok((maybe_fee_result, maybe_contract_fetch_info)) => { + let js_result = task_context.empty_array(); + + let js_contract: Handle = + if let Some(contract_fetch_info) = maybe_contract_fetch_info { + let contract_cbor = contract_fetch_info + .contract + .to_buffer() + .or_else(|_| { + task_context + .throw_range_error("can't serialize contract") + })?; + + JsBuffer::external(&mut task_context, contract_cbor) + .upcast() + } else { + task_context.null().upcast() + }; + + js_result.set(&mut task_context, 0, js_contract)?; + + if let Some(fee_result) = maybe_fee_result { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + js_result.set(&mut task_context, 1, js_fee_result)?; + } + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_create_contract(mut cx: FunctionContext) -> JsResult { + let js_contract_cbor = cx.argument::(0)?; + let js_block_info = cx.argument::(1)?; + let js_apply = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let contract_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_contract_cbor); + let apply = js_apply.value(&mut cx); + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .insert_contract_cbor( + contract_cbor, + None, + block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_update_contract(mut cx: FunctionContext) -> JsResult { + let js_contract_cbor = cx.argument::(0)?; + let js_block_info = cx.argument::(1)?; + let js_apply = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let contract_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_contract_cbor); + let apply = js_apply.value(&mut cx); + + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .update_contract_cbor( + contract_cbor, + None, + block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_create_document(mut cx: FunctionContext) -> JsResult { + let js_document_cbor = cx.argument::(0)?; + let js_contract_id = cx.argument::(1)?; + let js_document_type_name = cx.argument::(2)?; + let js_owner_id = cx.argument::(3)?; + let js_override_document = cx.argument::(4)?; + let js_block_info = cx.argument::(5)?; + let js_apply = cx.argument::(6)?; + let js_using_transaction = cx.argument::(7)?; + let js_callback = cx.argument::(8)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let document_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_document_cbor); + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + let document_type_name = js_document_type_name.value(&mut cx); + let owner_id = converter::js_buffer_to_identifier(&mut cx, js_owner_id)?; + let override_document = js_override_document.value(&mut cx); + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let storage_flags = + StorageFlags::new_single_epoch(block_info.epoch.index, Some(owner_id)); + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .add_serialized_document_for_contract_id( + &document_cbor, + contract_id, + &document_type_name, + Some(owner_id), + override_document, + block_info, + apply, + storage_flags.into_optional_cow(), + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_update_document(mut cx: FunctionContext) -> JsResult { + let js_document_cbor = cx.argument::(0)?; + let js_contract_id = cx.argument::(1)?; + let js_document_type_name = cx.argument::(2)?; + let js_owner_id = cx.argument::(3)?; + let js_block_info = cx.argument::(4)?; + let js_apply = cx.argument::(5)?; + let js_using_transaction = cx.argument::(6)?; + let js_callback = cx.argument::(7)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let document_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_document_cbor); + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + let document_type_name = js_document_type_name.value(&mut cx); + let owner_id = converter::js_buffer_to_identifier(&mut cx, js_owner_id)?; + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let storage_flags = + StorageFlags::new_single_epoch(block_info.epoch.index, Some(owner_id)); + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .update_document_for_contract_id( + &document_cbor, + contract_id, + &document_type_name, + Some(owner_id), + block_info, + apply, + storage_flags.into_optional_cow(), + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_delete_document(mut cx: FunctionContext) -> JsResult { + let js_document_id = cx.argument::(0)?; + let js_contract_id = cx.argument::(1)?; + let js_document_type_name = cx.argument::(2)?; + let js_block_info = cx.argument::(3)?; + let js_apply = cx.argument::(4)?; + let js_using_transaction = cx.argument::(5)?; + let js_callback = cx.argument::(6)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let document_id = converter::js_buffer_to_identifier(&mut cx, js_document_id)?; + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + let document_type_name = js_document_type_name.value(&mut cx); + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .delete_document_for_contract_id( + document_id, + contract_id, + &document_type_name, + None, + block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_insert_identity_cbor(mut cx: FunctionContext) -> JsResult { + let js_identity_cbor = cx.argument::(0)?; + let js_block_info = cx.argument::(1)?; + let js_apply = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_identity_cbor); + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .add_new_identity_from_cbor_encoded_bytes( + identity_cbor, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identity(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_full_identity(identity_id, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(maybe_identity) => { + if let Some(identity) = maybe_identity { + match identity.to_buffer() { + Ok(serialized_identity) => { + let js_serialized_identity = JsBuffer::external( + &mut task_context, + serialized_identity, + ); + + vec![ + task_context.null().upcast(), + js_serialized_identity.upcast(), + ] + } + Err(e) => { + let err_message = + format!("can't serialise identities: {}", e); + + vec![task_context.error(err_message)?.upcast()] + } + } + } else { + vec![task_context.null().upcast(), task_context.null().upcast()] + } + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identity_balance(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_identity_balance(identity_id, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(maybe_balance) => { + if let Some(credits) = maybe_balance { + let value = JsNumber::new(&mut task_context, credits as f64); + vec![task_context.null().upcast(), value.upcast()] + } else { + vec![task_context.null().upcast(), task_context.null().upcast()] + } + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identity_balance_include_debt_with_costs( + mut cx: FunctionContext, + ) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_block_info = cx.argument::(1)?; + let js_apply = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + + let apply = js_apply.value(&mut cx); + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_identity_balance_include_debt_with_costs( + identity_id, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok((maybe_balance, fee_result)) => { + let js_result = task_context.empty_array(); + + let js_balance: Handle = if let Some(credits) = + maybe_balance + { + let value = JsNumber::new(&mut task_context, credits as f64); + value.upcast() + } else { + task_context.null().upcast() + }; + + js_result.set(&mut task_context, 0, js_balance)?; + + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + js_result.set(&mut task_context, 1, js_fee_result)?; + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identity_balance_with_costs(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_block_info = cx.argument::(1)?; + let js_apply = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + + let apply = js_apply.value(&mut cx); + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_identity_balance_with_costs( + identity_id, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok((maybe_balance, fee_result)) => { + let js_result = task_context.empty_array(); + + let js_balance: Handle = if let Some(credits) = + maybe_balance + { + let value = JsNumber::new(&mut task_context, credits as f64); + value.upcast() + } else { + task_context.null().upcast() + }; + + js_result.set(&mut task_context, 0, js_balance)?; + + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + js_result.set(&mut task_context, 1, js_fee_result)?; + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_proved_identity(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .prove_full_identity(identity_id, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(identity_proof) => { + let js_identity_proof = + JsBuffer::external(&mut task_context, identity_proof); + + vec![task_context.null().upcast(), js_identity_proof.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_many_proved_identities(mut cx: FunctionContext) -> JsResult { + let js_identity_ids = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_ids = converter::js_array_of_buffers_to_identifiers(&mut cx, js_identity_ids)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .proved_full_identities(&identity_ids, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(identities_proof) => { + let js_identities_proof = + JsBuffer::external(&mut task_context, identities_proof); + + vec![task_context.null().upcast(), js_identities_proof.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identities_by_public_key_hashes(mut cx: FunctionContext) -> JsResult { + let js_public_key_hashes = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let public_key_hashes: Vec<[u8; 20]> = + converter::js_array_of_buffers_to_vec(&mut cx, js_public_key_hashes)? + .into_iter() + .map(|hash| { + hash.try_into() + .or_else(|_| cx.throw_error("invalid hash size")) + }) + .collect::>()?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result: Result>>, String> = + transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_full_identities_by_unique_public_key_hashes( + &public_key_hashes, + transaction_arg, + ) + .map_err(|err| err.to_string()) + .and_then(|hashes_to_identities| { + hashes_to_identities + .into_values() + .filter(|identity| identity.is_some()) + .map(|identity| identity.map(|i| i.to_buffer()).transpose()) + .collect::>() + .map_err(|err| err.to_string()) + }) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(hashes_to_identities) => { + let js_array = task_context.empty_array(); + + hashes_to_identities.into_iter().enumerate().try_for_each( + |(i, identity_bytes)| { + let value: Handle = if let Some(bytes) = + identity_bytes + { + JsBuffer::external(&mut task_context, bytes).upcast() + } else { + task_context.null().upcast() + }; + + js_array.set(&mut task_context, i as u32, value).map(|_| ()) + }, + )?; + + vec![task_context.null().upcast(), js_array.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_prove_identities_by_public_key_hashes(mut cx: FunctionContext) -> JsResult { + let js_public_key_hashes = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let public_key_hashes: Vec<[u8; 20]> = + converter::js_array_of_buffers_to_vec(&mut cx, js_public_key_hashes)? + .into_iter() + .map(|hash| { + hash.try_into() + .or_else(|_| cx.throw_error("invalid hash size")) + }) + .collect::>()?; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result: Result, String> = + transaction_result.and_then(|transaction_arg| { + platform + .drive + .prove_full_identities_by_unique_public_key_hashes( + &public_key_hashes, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(proof) => { + let js_proof = JsBuffer::external(&mut task_context, proof); + + vec![task_context.null().upcast(), js_proof.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_fetch_identity_with_costs(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_epoch_index = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + let js_callback = cx.argument::(3)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let epoch_index = u16::try_from(js_epoch_index.value(&mut cx) as i64) + .or_else(|_| cx.throw_range_error("`epochs` must fit in u16"))?; + + let epoch = Epoch::new(epoch_index).unwrap(); + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .fetch_full_identity_with_costs(identity_id, &epoch, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok((maybe_identity, fee_result)) => { + let js_result = task_context.empty_array(); + + let js_identity: Handle = + if let Some(identity) = maybe_identity { + let serialized_identity = + identity.to_buffer().or_else(|e| { + task_context.throw_error(format!( + "can't serialize identity: {}", + e + )) + })?; + + JsBuffer::external(&mut task_context, serialized_identity) + .upcast() + } else { + task_context.null().upcast() + }; + + js_result.set(&mut task_context, 0, js_identity)?; + + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + js_result.set(&mut task_context, 1, js_fee_result)?; + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_add_to_identity_balance(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_balance_to_add = cx.argument::(1)?; + let js_block_info = cx.argument::(2)?; + let js_apply = cx.argument::(3)?; + let js_using_transaction = cx.argument::(4)?; + let js_callback = cx.argument::(5)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + let balance_to_add = js_balance_to_add.value(&mut cx) as u64; + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .add_to_identity_balance( + identity_id, + balance_to_add, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_remove_from_identity_balance(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_amount = cx.argument::(1)?; + let js_block_info = cx.argument::(2)?; + let js_apply = cx.argument::(3)?; + let js_using_transaction = cx.argument::(4)?; + let js_callback = cx.argument::(5)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + let amount = js_amount.value(&mut cx) as u64; + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .remove_from_identity_balance( + identity_id, + amount, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_apply_fees_to_identity_balance(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_fee_result = cx.argument::>(1)?; + let js_using_transaction = cx.argument::(2)?; + let js_callback = cx.argument::(3)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let fee_result = js_fee_result.deref().deref().deref().clone(); + let balance_change = fee_result.into_balance_change(identity_id); + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .apply_balance_change_from_fee_to_identity( + balance_change, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(outcome) => { + let js_fee_result = task_context + .boxed(FeeResultWrapper::new(outcome.actual_fee_paid)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_add_to_system_credits(mut cx: FunctionContext) -> JsResult { + let js_amount = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + let js_callback = cx.argument::(2)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let amount = js_amount.value(&mut cx) as Credits; + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .add_to_system_credits(amount, transaction_arg) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(()) => { + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_add_keys_to_identity(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_keys_to_add = cx.argument::(1)?; + let js_block_info = cx.argument::(2)?; + let js_apply = cx.argument::(3)?; + let js_using_transaction = cx.argument::(4)?; + let js_callback = cx.argument::(5)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + let keys_to_add = converter::js_array_to_keys(&mut cx, js_keys_to_add)?; + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .add_new_keys_to_identity( + identity_id, + keys_to_add, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_disable_identity_keys(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_key_ids = cx.argument::(1)?; + let js_disable_at = cx.argument::(2)?; + let js_block_info = cx.argument::(3)?; + let js_apply = cx.argument::(4)?; + let js_using_transaction = cx.argument::(5)?; + let js_callback = cx.argument::(6)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let key_ids = js_key_ids + .to_vec(&mut cx)? + .into_iter() + .map(|js_value| { + let js_key = js_value.downcast_or_throw::(&mut cx)?; + let key = KeyID::try_from(js_key.value(&mut cx) as u64) + .or_else(|_| cx.throw_range_error("key id must be u32"))?; + + Ok(key) + }) + .collect::, _>>()?; + + let disabled_at = js_disable_at.value(&mut cx) as TimestampMillis; + + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .disable_identity_keys( + identity_id, + key_ids, + disabled_at, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_update_identity_revision(mut cx: FunctionContext) -> JsResult { + let js_identity_id = cx.argument::(0)?; + let js_revision = cx.argument::(1)?; + let js_block_info = cx.argument::(2)?; + let js_apply = cx.argument::(3)?; + let js_using_transaction = cx.argument::(4)?; + let js_callback = cx.argument::(5)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let identity_id = converter::js_buffer_to_identifier(&mut cx, js_identity_id)?; + + let revision = js_revision.value(&mut cx) as Revision; + + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let apply = js_apply.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .update_identity_revision( + identity_id, + revision, + &block_info, + apply, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(fee_result) => { + let js_fee_result = + task_context.boxed(FeeResultWrapper::new(fee_result)); + + // First parameter of JS callbacks is error, which is null in this case + vec![task_context.null().upcast(), js_fee_result.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_query_documents(mut cx: FunctionContext) -> JsResult { + let js_query_cbor = cx.argument::(0)?; + let js_contract_id = cx.argument::(1)?; + let js_document_type_name = cx.argument::(2)?; + let js_maybe_epoch_index = cx.argument::(3)?; + // TODO We need dry run for validation + let js_using_transaction = cx.argument::(4)?; + let js_callback = cx.argument::(5)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let query_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_query_cbor); + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + let document_type_name = js_document_type_name.value(&mut cx); + + let maybe_epoch: Option = if !js_maybe_epoch_index.is_a::(&mut cx) { + let js_epoch_index = js_maybe_epoch_index.downcast_or_throw::(&mut cx)?; + + let epoch_index = u16::try_from(js_epoch_index.value(&mut cx) as i64) + .or_else(|_| cx.throw_range_error("`epochs` must fit in u16"))?; + + let epoch = Epoch::new(epoch_index).unwrap(); + + Some(epoch) + } else { + None + }; + + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .query_documents_cbor_with_document_type_lookup( + &query_cbor, + contract_id, + document_type_name.as_str(), + maybe_epoch.as_ref(), + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(QuerySerializedDocumentsOutcome { + items, + skipped, + cost, + }) => { + let js_array: Handle = task_context.empty_array(); + let js_vecs = + converter::nested_vecs_to_js(&mut task_context, items)?; + let js_num = task_context.number(skipped).upcast::(); + let js_cost = task_context.number(cost as f64).upcast::(); + + js_array.set(&mut task_context, 0, js_vecs)?; + js_array.set(&mut task_context, 1, js_num)?; + js_array.set(&mut task_context, 2, js_cost)?; + + vec![task_context.null().upcast(), js_array.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_prove_documents_query(mut cx: FunctionContext) -> JsResult { + let js_query_cbor = cx.argument::(0)?; + let js_contract_id = cx.argument::(1)?; + let js_document_type_name = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + + let js_callback = cx.argument::(4)?.root(&mut cx); + + let drive = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let query_cbor = converter::js_buffer_to_vec_u8(&mut cx, js_query_cbor); + let contract_id = converter::js_buffer_to_identifier(&mut cx, js_contract_id)?; + let document_type_name = js_document_type_name.value(&mut cx); + let using_transaction = js_using_transaction.value(&mut cx); + + drive + .send_to_drive_thread(move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + platform + .drive + .query_proof_of_documents_using_contract_id_using_cbor_encoded_query_with_cost( + &query_cbor, + contract_id, + document_type_name.as_str(), + None, + None, + transaction_arg, + ) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok((proof, processing_cost)) => { + let js_array: Handle = task_context.empty_array(); + let js_buffer = JsBuffer::external(&mut task_context, proof); + let js_processing_cost = task_context.number(processing_cost as f64); + + js_array.set(&mut task_context, 0, js_buffer)?; + js_array.set(&mut task_context, 1, js_processing_cost)?; + + vec![task_context.null().upcast(), js_array.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_start_transaction(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.start_transaction(|_, result, channel| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(_) => { + vec![task_context.null().upcast()] + } + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_commit_transaction(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.commit_transaction(|channel, result| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(_) => vec![task_context.null().upcast()], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_rollback_transaction(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.rollback_transaction(|channel, result| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(_) => vec![task_context.null().upcast()], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_abort_transaction(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.abort_transaction(|channel, result| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(_) => vec![task_context.null().upcast()], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_is_transaction_started(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |_platform: &Platform, transaction, channel| { + let result = transaction.is_some(); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + // First parameter of JS callbacks is error, which is null in this case + let callback_arguments: Vec> = vec![ + task_context.null().upcast(), + task_context.boolean(result).upcast(), + ]; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_get(mut cx: FunctionContext) -> JsResult { + let js_path = cx.argument::(0)?; + let js_key = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + + let js_callback = cx.argument::(3)?.root(&mut cx); + + let path = converter::js_array_of_buffers_to_vec(&mut cx, js_path)?; + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + + let using_transaction = js_using_transaction.value(&mut cx); + + // Get the `this` value as a `JsBox` + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + let path_slice = path.iter().map(|fragment| fragment.as_slice()); + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .get(path_slice, &key, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(element) => { + // First parameter of JS callbacks is error, which is null in this case + vec![ + task_context.null().upcast(), + converter::element_to_js_object(&mut task_context, element)?, + ] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_insert(mut cx: FunctionContext) -> JsResult { + let js_path = cx.argument::(0)?; + let js_key = cx.argument::(1)?; + let js_element = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + + let js_callback = cx.argument::(4)?.root(&mut cx); + + let path = converter::js_array_of_buffers_to_vec(&mut cx, js_path)?; + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + let element = converter::js_object_to_element(&mut cx, js_element)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + // Get the `this` value as a `JsBox` + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + let path_slice = path.iter().map(|fragment| fragment.as_slice()); + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .insert(path_slice, &key, element, None, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(_) => vec![task_context.null().upcast()], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_insert_if_not_exists(mut cx: FunctionContext) -> JsResult { + let js_path = cx.argument::(0)?; + let js_key = cx.argument::(1)?; + let js_element = cx.argument::(2)?; + let js_using_transaction = cx.argument::(3)?; + + let js_callback = cx.argument::(4)?.root(&mut cx); + + let path = converter::js_array_of_buffers_to_vec(&mut cx, js_path)?; + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + let element = converter::js_object_to_element(&mut cx, js_element)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + // Get the `this` value as a `JsBox` + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let path_slice: Vec<&[u8]> = + path.iter().map(|fragment| fragment.as_slice()).collect(); + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .insert_if_not_exists(path_slice, key.as_slice(), element, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(is_inserted) => vec![ + task_context.null().upcast(), + task_context + .boolean(is_inserted) + .as_value(&mut task_context), + ], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + fn js_grove_db_put_aux(mut cx: FunctionContext) -> JsResult { + let js_key = cx.argument::(0)?; + let js_value = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + + let js_callback = cx.argument::(3)?.root(&mut cx); + + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + let value = converter::js_buffer_to_vec_u8(&mut cx, js_value); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .put_aux(&key, &value, None, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(()) => { + vec![task_context.null().upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_delete_aux(mut cx: FunctionContext) -> JsResult { + let js_key = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + + let js_callback = cx.argument::(2)?.root(&mut cx); + + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .delete_aux(&key, None, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(()) => { + vec![task_context.null().upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_get_aux(mut cx: FunctionContext) -> JsResult { + let js_key = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + + let js_callback = cx.argument::(2)?.root(&mut cx); + + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + let using_transaction = js_using_transaction.value(&mut cx); + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .get_aux(&key, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(value) => { + if let Some(value) = value { + vec![ + task_context.null().upcast(), + JsBuffer::external(&mut task_context, value).upcast(), + ] + } else { + vec![task_context.null().upcast(), task_context.null().upcast()] + } + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_query(mut cx: FunctionContext) -> JsResult { + let js_path_query = cx.argument::(0)?; + let js_skip_cache = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + + let js_callback = cx.argument::(3)?.root(&mut cx); + + let path_query = converter::js_path_query_to_path_query(&mut cx, js_path_query)?; + + let skip_cache = js_skip_cache.value(&mut cx); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .query_item_value(&path_query, !skip_cache, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok((values, skipped)) => { + let js_array: Handle = task_context.empty_array(); + let js_vecs = converter::nested_vecs_to_js(&mut task_context, values)?; + let js_num = task_context.number(skipped).upcast::(); + + js_array.set(&mut task_context, 0, js_vecs)?; + js_array.set(&mut task_context, 1, js_num)?; + + vec![task_context.null().upcast(), js_array.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_prove_query(mut cx: FunctionContext) -> JsResult { + let js_path_query = cx.argument::(0)?; + let js_get_verbose_proof = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + + let js_callback = cx.argument::(3)?.root(&mut cx); + + let path_query = converter::js_path_query_to_path_query(&mut cx, js_path_query)?; + + let get_verbose_proof = js_get_verbose_proof.value(&mut cx); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .get_proved_path_query(&path_query, get_verbose_proof, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(proof) => { + let js_buffer = JsBuffer::external(&mut task_context, proof); + let js_value = js_buffer.as_value(&mut task_context); + + vec![task_context.null().upcast(), js_value.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_prove_query_many(mut cx: FunctionContext) -> JsResult { + let js_path_queries = cx.argument::(0)?; + let js_using_transaction = cx.argument::(1)?; + + if js_using_transaction.value(&mut cx) { + cx.throw_type_error("transaction is not supported")?; + } + + let js_callback = cx.argument::(2)?.root(&mut cx); + + let js_path_queries = js_path_queries.to_vec(&mut cx)?; + let mut path_queries: Vec = Vec::with_capacity(js_path_queries.len()); + + for js_path_query in js_path_queries { + let js_path_query = js_path_query.downcast_or_throw::(&mut cx)?; + path_queries.push(converter::js_path_query_to_path_query( + &mut cx, + js_path_query, + )?); + } + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread(move |platform: &Platform, _, channel| { + let grove_db = &platform.drive.grove; + + let path_queries = path_queries.iter().collect(); + + let result = grove_db.prove_query_many(path_queries).unwrap(); + + channel.send(move |mut task_context| { + let this = task_context.undefined(); + let callback = js_callback.into_inner(&mut task_context); + + let callback_arguments = match result { + Ok(proof) => { + let js_buffer = JsBuffer::external(&mut task_context, proof); + let js_value = js_buffer.as_value(&mut task_context); + + vec![task_context.null().upcast(), js_value.upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err.to_string())?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + /// Flush data on disc and then calls js callback passed as a first + /// argument to the function + fn js_grove_db_flush(mut cx: FunctionContext) -> JsResult { + let js_callback = cx.argument::(0)?.root(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.flush(|channel| { + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = vec![task_context.null().upcast()]; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + /// Returns root hash or empty buffer + fn js_grove_db_root_hash(mut cx: FunctionContext) -> JsResult { + let js_using_transaction = cx.argument::(0)?; + + let js_callback = cx.argument::(1)?.root(&mut cx); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .root_hash(transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(hash) => vec![ + task_context.null().upcast(), + JsBuffer::external(&mut task_context, hash).upcast(), + ], + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_grove_db_delete(mut cx: FunctionContext) -> JsResult { + let js_path = cx.argument::(0)?; + let js_key = cx.argument::(1)?; + + let js_using_transaction = cx.argument::(2)?; + + let js_callback = cx.argument::(3)?.root(&mut cx); + + let path = converter::js_array_of_buffers_to_vec(&mut cx, js_path)?; + let key = converter::js_buffer_to_vec_u8(&mut cx, js_key); + + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let grove_db = &platform.drive.grove; + + let path_slice: Vec<&[u8]> = + path.iter().map(|fragment| fragment.as_slice()).collect(); + let result = transaction_result.and_then(|transaction_arg| { + grove_db + .delete(path_slice, key.as_slice(), None, transaction_arg) + .unwrap() + .map_err(Error::GroveDB) + .map_err(|err| err.to_string()) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + let callback_arguments: Vec> = match result { + Ok(()) => { + vec![task_context.null().upcast()] + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } + + fn js_abci_init_chain(mut cx: FunctionContext) -> JsResult { + //this needs to be removed + todo!(); + // let js_request = cx.argument::(0)?; + // let js_using_transaction = cx.argument::(1)?; + // + // let js_callback = cx.argument::(2)?.root(&mut cx); + // + // let using_transaction = js_using_transaction.value(&mut cx); + // + // let db = cx + // .this() + // .downcast_or_throw::, _>(&mut cx)?; + // + // let request_bytes = converter::js_buffer_to_vec_u8(&mut cx, js_request); + // + // db.send_to_drive_thread(move |platform: &Platform, transaction, channel| { + // let transaction_result = if using_transaction { + // if transaction.is_none() { + // Err("transaction is not started".to_string()) + // } else { + // Ok(transaction) + // } + // } else { + // Ok(None) + // }; + // + // let result = transaction_result.and_then(|transaction_arg| { + // InitChainRequest::from_bytes(&request_bytes) + // .and_then(|request| platform.init_chain(request.into())) + // .map_err(|e| e.to_string()) + // }); + // + // channel.send(move |mut task_context| { + // let callback = js_callback.into_inner(&mut task_context); + // let this = task_context.undefined(); + // + // let callback_arguments: Vec> = match result { + // Ok(response_bytes) => { + // let value = JsBuffer::external(&mut task_context, response_bytes); + // + // vec![task_context.null().upcast(), value.upcast()] + // } + // + // // Convert the error to a JavaScript exception on failure + // Err(err) => vec![task_context.error(err)?.upcast()], + // }; + // + // callback.call(&mut task_context, this, callback_arguments)?; + // + // Ok(()) + // }); + // }) + // .or_else(|err| cx.throw_error(err.to_string()))?; + // + // // The result is returned through the callback, not through direct return + // Ok(cx.undefined()) + } + + fn js_abci_block_begin(mut cx: FunctionContext) -> JsResult { + //this needs to be removed + todo!() + // let js_request = cx.argument::(0)?; + // let js_using_transaction = cx.argument::(1)?; + // + // let js_callback = cx.argument::(2)?.root(&mut cx); + // + // let using_transaction = js_using_transaction.value(&mut cx); + // + // let db = cx + // .this() + // .downcast_or_throw::, _>(&mut cx)?; + // + // let request_bytes = converter::js_buffer_to_vec_u8(&mut cx, js_request); + // + // db.send_to_drive_thread(move |platform: &Platform, transaction, channel| { + // let transaction_result = if using_transaction { + // if transaction.is_none() { + // Err("transaction is not started".to_string()) + // } else { + // Ok(transaction) + // } + // } else { + // Ok(None) + // }; + // + // let result = transaction_result.and_then(|transaction_arg| { + // BlockBeginRequest::from_bytes(&request_bytes) + // .and_then(|request| platform.block_begin(request, transaction_arg)) + // .and_then(|response| response.to_bytes()) + // .map_err(|e| e.to_string()) + // }); + // + // channel.send(move |mut task_context| { + // let callback = js_callback.into_inner(&mut task_context); + // let this = task_context.undefined(); + // + // let callback_arguments: Vec> = match result { + // Ok(response_bytes) => { + // let value = JsBuffer::external(&mut task_context, response_bytes); + // + // vec![task_context.null().upcast(), value.upcast()] + // } + // + // // Convert the error to a JavaScript exception on failure + // Err(err) => vec![task_context.error(err)?.upcast()], + // }; + // + // callback.call(&mut task_context, this, callback_arguments)?; + // + // Ok(()) + // }); + // }) + // .or_else(|err| cx.throw_error(err.to_string()))?; + // + // // The result is returned through the callback, not through direct return + // Ok(cx.undefined()) + } + + fn js_abci_block_end(mut cx: FunctionContext) -> JsResult { + //this needs to be removed + todo!() + // let js_request = cx.argument::(0)?; + // + // let js_using_transaction = cx.argument::(1)?; + // + // let js_callback = cx.argument::(2)?.root(&mut cx); + // + // let using_transaction = js_using_transaction.value(&mut cx); + // + // let db = cx + // .this() + // .downcast_or_throw::, _>(&mut cx)?; + // + // let js_fees: Handle = js_request.get(&mut cx, "fees")?; + // + // let js_processing_fee: Handle = js_fees.get(&mut cx, "processingFee")?; + // let processing_fee = js_processing_fee.value(&mut cx) as u64; + // + // let js_storage_fee: Handle = js_fees.get(&mut cx, "storageFee")?; + // let storage_fee = js_storage_fee.value(&mut cx) as u64; + // + // let js_refunds_per_epoch: Handle = js_fees.get(&mut cx, "refundsPerEpoch")?; + // + // let refunds_per_epoch = js_object_to_fee_refunds(&mut cx, js_refunds_per_epoch)?; + // + // db.send_to_drive_thread(move |platform: &Platform, transaction, channel| { + // let transaction_result = if using_transaction { + // if transaction.is_none() { + // Err("transaction is not started".to_string()) + // } else { + // Ok(transaction) + // } + // } else { + // Ok(None) + // }; + // + // let result = transaction_result.and_then(|transaction_arg| { + // let request = BlockEndRequest { + // fees: BlockFees { + // processing_fee, + // storage_fee, + // refunds_per_epoch, + // }, + // }; + // + // platform + // .block_end(request, transaction_arg) + // .and_then(|response| response.to_bytes()) + // .map_err(|e| e.to_string()) + // }); + // + // channel.send(move |mut task_context| { + // let callback = js_callback.into_inner(&mut task_context); + // let this = task_context.undefined(); + // + // let callback_arguments: Vec> = match result { + // Ok(response_bytes) => { + // let value = JsBuffer::external(&mut task_context, response_bytes); + // + // vec![task_context.null().upcast(), value.upcast()] + // } + // + // // Convert the error to a JavaScript exception on failure + // Err(err) => vec![task_context.error(err)?.upcast()], + // }; + // + // callback.call(&mut task_context, this, callback_arguments)?; + // + // Ok(()) + // }); + // }) + // .or_else(|err| cx.throw_error(err.to_string()))?; + // + // // The result is returned through the callback, not through direct return + // Ok(cx.undefined()) + } + + fn js_abci_after_finalize_block(mut cx: FunctionContext) -> JsResult { + //this needs to be removed + todo!(); + // let js_request = cx.argument::(0)?; + // let js_callback = cx.argument::(1)?.root(&mut cx); + // + // let db = cx + // .this() + // .downcast_or_throw::, _>(&mut cx)?; + // + // let request_bytes = converter::js_buffer_to_vec_u8(&mut cx, js_request); + // + // db.send_to_drive_thread(move |platform: &Platform, _, channel| { + // let result = AfterFinalizeBlockRequest::from_bytes(&request_bytes) + // .and_then(|request| platform.after_finalize_block(request)) + // .and_then(|response| response.to_bytes()); + // + // channel.send(move |mut task_context| { + // let callback = js_callback.into_inner(&mut task_context); + // let this = task_context.undefined(); + // + // let callback_arguments: Vec> = match result { + // Ok(response_bytes) => { + // let value = JsBuffer::external(&mut task_context, response_bytes); + // + // vec![task_context.null().upcast(), value.upcast()] + // } + // + // // Convert the error to a JavaScript exception on failure + // Err(err) => vec![task_context.error(err.to_string())?.upcast()], + // }; + // + // callback.call(&mut task_context, this, callback_arguments)?; + // + // Ok(()) + // }); + // }) + // .or_else(|err| cx.throw_error(err.to_string()))?; + // + // // The result is returned through the callback, not through direct return + // Ok(cx.undefined()) + } + + fn js_fetch_latest_withdrawal_transaction_index( + mut cx: FunctionContext, + ) -> JsResult { + let js_block_info = cx.argument::(0)?; + let js_apply = cx.argument::(1)?; + let js_using_transaction = cx.argument::(2)?; + let js_callback = cx.argument::(3)?.root(&mut cx); + + let apply = js_apply.value(&mut cx); + let block_info = converter::js_object_to_block_info(&mut cx, js_block_info)?; + let using_transaction = js_using_transaction.value(&mut cx); + + let db = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + + db.send_to_drive_thread( + move |platform: &Platform, transaction, channel| { + let transaction_result = if using_transaction { + if transaction.is_none() { + Err("transaction is not started".to_string()) + } else { + Ok(transaction) + } + } else { + Ok(None) + }; + + let result = transaction_result.and_then(|transaction_arg| { + let mut drive_operation_types = vec![]; + + let result = platform + .drive + .fetch_and_remove_latest_withdrawal_transaction_index_operations( + &mut drive_operation_types, + transaction_arg, + ) + .map_err(|err| err.to_string())?; + + platform + .drive + .apply_drive_operations( + drive_operation_types, + apply, + &block_info, + transaction_arg, + ) + .map_err(|err| err.to_string())?; + + Ok(result) + }); + + channel.send(move |mut task_context| { + let callback = js_callback.into_inner(&mut task_context); + let this = task_context.undefined(); + + let callback_arguments: Vec> = match result { + Ok(index) => { + let index_f64 = index as f64; + if index_f64 as u64 != index { + vec![task_context + .error("could not convert withdrawal transaction index to f64")? + .upcast()] + } else { + let value = JsNumber::new(&mut task_context, index_f64); + + vec![task_context.null().upcast(), value.upcast()] + } + } + + // Convert the error to a JavaScript exception on failure + Err(err) => vec![task_context.error(err)?.upcast()], + }; + + callback.call(&mut task_context, this, callback_arguments)?; + + Ok(()) + }); + }, + ) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // The result is returned through the callback, not through direct return + Ok(cx.undefined()) + } +} + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + cx.export_function("driveOpen", PlatformWrapper::js_open)?; + cx.export_function("driveClose", PlatformWrapper::js_close)?; + cx.export_function( + "driveCreateInitialStateStructure", + PlatformWrapper::js_create_initial_state_structure, + )?; + cx.export_function("driveFetchContract", PlatformWrapper::js_fetch_contract)?; + cx.export_function("driveCreateContract", PlatformWrapper::js_create_contract)?; + cx.export_function("driveUpdateContract", PlatformWrapper::js_update_contract)?; + cx.export_function("driveCreateDocument", PlatformWrapper::js_create_document)?; + cx.export_function("driveUpdateDocument", PlatformWrapper::js_update_document)?; + cx.export_function("driveDeleteDocument", PlatformWrapper::js_delete_document)?; + cx.export_function( + "driveInsertIdentity", + PlatformWrapper::js_insert_identity_cbor, + )?; + cx.export_function("driveFetchIdentity", PlatformWrapper::js_fetch_identity)?; + cx.export_function( + "driveFetchIdentityBalance", + PlatformWrapper::js_fetch_identity_balance, + )?; + cx.export_function( + "driveFetchIdentityBalanceWithCosts", + PlatformWrapper::js_fetch_identity_balance_with_costs, + )?; + cx.export_function( + "driveFetchIdentityBalanceIncludeDebtWithCosts", + PlatformWrapper::js_fetch_identity_balance_include_debt_with_costs, + )?; + cx.export_function( + "driveFetchProvedIdentity", + PlatformWrapper::js_fetch_proved_identity, + )?; + cx.export_function( + "driveFetchManyProvedIdentities", + PlatformWrapper::js_fetch_many_proved_identities, + )?; + cx.export_function( + "driveFetchIdentityWithCosts", + PlatformWrapper::js_fetch_identity_with_costs, + )?; + cx.export_function( + "driveAddToIdentityBalance", + PlatformWrapper::js_add_to_identity_balance, + )?; + cx.export_function( + "driveRemoveFromIdentityBalance", + PlatformWrapper::js_remove_from_identity_balance, + )?; + cx.export_function( + "driveApplyFeesToIdentityBalance", + PlatformWrapper::js_apply_fees_to_identity_balance, + )?; + cx.export_function( + "driveAddToSystemCredits", + PlatformWrapper::js_add_to_system_credits, + )?; + cx.export_function( + "driveFetchIdentitiesByPublicKeyHashes", + PlatformWrapper::js_fetch_identities_by_public_key_hashes, + )?; + cx.export_function( + "driveProveIdentitiesByPublicKeyHashes", + PlatformWrapper::js_prove_identities_by_public_key_hashes, + )?; + cx.export_function( + "driveAddKeysToIdentity", + PlatformWrapper::js_add_keys_to_identity, + )?; + cx.export_function( + "driveDisableIdentityKeys", + PlatformWrapper::js_disable_identity_keys, + )?; + cx.export_function( + "driveUpdateIdentityRevision", + PlatformWrapper::js_update_identity_revision, + )?; + + cx.export_function("driveQueryDocuments", PlatformWrapper::js_query_documents)?; + + cx.export_function( + "driveProveDocumentsQuery", + PlatformWrapper::js_prove_documents_query, + )?; + + cx.export_function( + "driveFetchLatestWithdrawalTransactionIndex", + PlatformWrapper::js_fetch_latest_withdrawal_transaction_index, + )?; + + cx.export_function("groveDbInsert", PlatformWrapper::js_grove_db_insert)?; + cx.export_function( + "groveDbInsertIfNotExists", + PlatformWrapper::js_grove_db_insert_if_not_exists, + )?; + cx.export_function("groveDbGet", PlatformWrapper::js_grove_db_get)?; + cx.export_function("groveDbDelete", PlatformWrapper::js_grove_db_delete)?; + cx.export_function("groveDbFlush", PlatformWrapper::js_grove_db_flush)?; + cx.export_function( + "groveDbStartTransaction", + PlatformWrapper::js_grove_db_start_transaction, + )?; + cx.export_function( + "groveDbIsTransactionStarted", + PlatformWrapper::js_grove_db_is_transaction_started, + )?; + cx.export_function( + "groveDbCommitTransaction", + PlatformWrapper::js_grove_db_commit_transaction, + )?; + cx.export_function( + "groveDbRollbackTransaction", + PlatformWrapper::js_grove_db_rollback_transaction, + )?; + cx.export_function( + "groveDbAbortTransaction", + PlatformWrapper::js_grove_db_abort_transaction, + )?; + cx.export_function("groveDbPutAux", PlatformWrapper::js_grove_db_put_aux)?; + cx.export_function("groveDbDeleteAux", PlatformWrapper::js_grove_db_delete_aux)?; + cx.export_function("groveDbGetAux", PlatformWrapper::js_grove_db_get_aux)?; + cx.export_function("groveDbQuery", PlatformWrapper::js_grove_db_query)?; + cx.export_function( + "groveDbProveQuery", + PlatformWrapper::js_grove_db_prove_query, + )?; + cx.export_function( + "groveDbProveQueryMany", + PlatformWrapper::js_grove_db_prove_query_many, + )?; + cx.export_function("groveDbRootHash", PlatformWrapper::js_grove_db_root_hash)?; + + cx.export_function("abciInitChain", PlatformWrapper::js_abci_init_chain)?; + cx.export_function("abciBlockBegin", PlatformWrapper::js_abci_block_begin)?; + cx.export_function("abciBlockEnd", PlatformWrapper::js_abci_block_end)?; + cx.export_function( + "abciAfterFinalizeBlock", + PlatformWrapper::js_abci_after_finalize_block, + )?; + + cx.export_function( + "feeResultGetProcessingFee", + FeeResultWrapper::get_processing_fee, + )?; + cx.export_function("feeResultGetStorageFee", FeeResultWrapper::get_storage_fee)?; + cx.export_function("feeResultAdd", FeeResultWrapper::add)?; + cx.export_function("feeResultAddFees", FeeResultWrapper::add_fees)?; + cx.export_function("feeResultCreate", FeeResultWrapper::create)?; + cx.export_function("feeResultGetRefunds", FeeResultWrapper::get_refunds)?; + cx.export_function( + "feeResultSumRefundsPerEpoch", + FeeResultWrapper::get_refunds_per_epoch, + )?; + + cx.export_function( + "calculateStorageFeeDistributionAmountAndLeftovers", + js_calculate_storage_fee_distribution_amount_and_leftovers, + )?; + + Ok(()) +} diff --git a/packages/rs-drive-nodejs/test/.eslintrc b/packages/rs-drive-nodejs/test/.eslintrc new file mode 100644 index 00000000000..720ced73852 --- /dev/null +++ b/packages/rs-drive-nodejs/test/.eslintrc @@ -0,0 +1,12 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" + }, + "globals": { + "expect": true + } +} diff --git a/packages/rs-drive-nodejs/test/Drive.spec.js b/packages/rs-drive-nodejs/test/Drive.spec.js new file mode 100644 index 00000000000..931d7d34a07 --- /dev/null +++ b/packages/rs-drive-nodejs/test/Drive.spec.js @@ -0,0 +1,1181 @@ +const fs = require('fs'); + +const { PrivateKey } = require('@dashevo/dashcore-lib'); + +const { expect, use } = require('chai'); +use(require('dirty-chai')); + +const DashPlatformProtocol = require('@dashevo/dpp'); + +const Document = require('@dashevo/dpp/lib/document/Document'); +const Identifier = require('@dashevo/dpp/lib/Identifier'); + +const getDataContractFixture = require('@dashevo/dpp/lib/test/fixtures/getDataContractFixture'); +const getDocumentsFixture = require('@dashevo/dpp/lib/test/fixtures/getDocumentsFixture'); +const getIdentityFixture = require('@dashevo/dpp/lib/test/fixtures/getIdentityFixture'); +const generateRandomIdentifier = require('@dashevo/dpp/lib/test/utils/generateRandomIdentifier'); + +const withdrawalContractDocumentsSchema = require('@dashevo/withdrawals-contract/schema/withdrawals-documents.json'); +const withdrawalContractIds = require('@dashevo/withdrawals-contract/lib/systemIds'); + +const { + expectFeeResult, +} = require('./utils'); + +const Drive = require('../Drive'); + +const FeeResult = require('../FeeResult'); + +const TEST_DATA_PATH = './test_data'; + +describe('Drive', function main() { + this.timeout(10000); + + let drive; + let dataContract; + let identity; + let blockInfo; + let documents; + let initialRootHash; + let withdrawalsDataContract; + + beforeEach(async () => { + drive = new Drive(TEST_DATA_PATH, { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + const dpp = new DashPlatformProtocol({ + stateRepository: { + fetchDataContract: () => { }, + }, + }); + + await dpp.initialize(); + + withdrawalsDataContract = dpp.dataContract.create( + generateRandomIdentifier(), + withdrawalContractDocumentsSchema, + ); + + withdrawalsDataContract.id = Identifier.from(withdrawalContractIds.contractId); + + dataContract = getDataContractFixture(); + identity = getIdentityFixture(); + + blockInfo = { + height: 1, + epoch: 1, + timeMs: new Date().getTime(), + }; + + documents = getDocumentsFixture(dataContract); + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + afterEach(async () => { + await drive.close(); + + fs.rmSync(TEST_DATA_PATH, { recursive: true }); + }); + + describe('#createInitialStateStructure', () => { + it('should create initial tree structure', async () => { + const result = await drive.createInitialStateStructure(); + + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.undefined; + }); + }); + + describe('#fetchContract', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if contract not exists', async () => { + const result = await drive.fetchContract(Buffer.alloc(32), blockInfo.epoch); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [fetchedDataContract, feeResult] = result; + + expect(fetchedDataContract).to.be.null(); + + expect(feeResult).to.be.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + + it('should return contract if contract is present', async () => { + await drive.createContract(dataContract, blockInfo); + + const result = await drive.fetchContract(dataContract.getId(), blockInfo.epoch); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [fetchedDataContract, feeResult] = result; + + expect(fetchedDataContract.toBuffer()).to.deep.equal(dataContract.toBuffer()); + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + + it('should return contract without fee result if epoch is not passed', async () => { + await drive.createContract(dataContract, blockInfo); + + const result = await drive.fetchContract(dataContract.getId()); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(1); + + const [fetchedDataContract] = result; + + expect(fetchedDataContract.toBuffer()).to.deep.equal(dataContract.toBuffer()); + }); + }); + + describe('#createContract', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should create contract', async () => { + const result = await drive.createContract(dataContract, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + + it('should not create contract with dry run flag', async () => { + const result = await drive.createContract(dataContract, blockInfo, undefined, true); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#updateContract', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + + dataContract.setDocumentSchema('newDocumentType', { + type: 'object', + properties: { + newProperty: { + type: 'string', + }, + }, + additionalProperties: false, + }); + }); + + it('should update existing contract', async () => { + const result = await drive.updateContract(dataContract, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + + it('should not create contract with dry run flag', async () => { + const result = await drive.updateContract(dataContract, blockInfo, undefined, true); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#createDocument', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + context('without indices', () => { + it('should create a document', async () => { + const documentWithoutIndices = documents[0]; + + const result = await drive.createDocument(documentWithoutIndices, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + context('with indices', () => { + it('should create a document', async () => { + const documentWithIndices = documents[3]; + + const result = await drive.createDocument(documentWithIndices, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + it('should not create a document with dry run flag', async () => { + const documentWithoutIndices = documents[0]; + + const result = await drive.createDocument(documentWithoutIndices, blockInfo, undefined, true); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#updateDocument', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + context('without indices', () => { + it('should update a document', async () => { + // Create a document + const documentWithoutIndices = documents[0]; + + await drive.createDocument(documentWithoutIndices, blockInfo); + + // Update the document + documentWithoutIndices.set('name', 'Boooooooooooob'); + + const result = await drive.updateDocument(documentWithoutIndices, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + context('with indices', () => { + it('should update the document', async () => { + // Create a document + const documentWithIndices = documents[3]; + + await drive.createDocument(documentWithIndices, blockInfo); + + // Update the document + documentWithIndices.set('firstName', 'Bob'); + + const result = await drive.updateDocument(documentWithIndices, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + it('should not update a document with dry run flag', async () => { + const documentWithoutIndices = documents[0]; + + documentWithoutIndices.set('name', 'Boooooooooooooooooooooob'); + + const result = await drive.updateDocument(documentWithoutIndices, blockInfo, undefined, true); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.be.greaterThan(0); + expect(result.storageFee).to.be.greaterThan(0, 'storage fee must be higher than 0'); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#deleteDocument', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + context('without indices', () => { + it('should delete the document', async () => { + // Create a document + const documentWithoutIndices = documents[3]; + + await drive.createDocument(documentWithoutIndices, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + + const result = await drive.deleteDocument( + dataContract.getId(), + documentWithoutIndices.getType(), + documentWithoutIndices.getId(), + blockInfo, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.be.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + context('with indices', () => { + it('should delete the document', async () => { + // Create a document + const documentWithIndices = documents[3]; + + await drive.createDocument(documentWithIndices, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + + const result = await drive.deleteDocument( + dataContract.getId(), + documentWithIndices.getType(), + documentWithIndices.getId(), + blockInfo, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.be.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + it('should not delete the document with dry run flag', async () => { + // Create a document + const documentWithoutIndices = documents[3]; + + await drive.createDocument(documentWithoutIndices, blockInfo); + + initialRootHash = await drive.getGroveDB().getRootHash(); + + const result = await drive.deleteDocument( + dataContract.getId(), + documentWithoutIndices.getType(), + documentWithoutIndices.getId(), + blockInfo, + undefined, + true, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.be.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#queryDocuments', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + }); + + it('should query existing documents', async () => { + // Create documents + await Promise.all( + documents.map((document) => drive.createDocument(document, blockInfo)), + ); + + // eslint-disable-next-line no-unused-vars + const [fetchedDocuments, processingCost] = await drive.queryDocuments(dataContract, 'indexedDocument', undefined, { + where: [['lastName', '==', 'Kennedy']], + }); + + expect(fetchedDocuments).to.have.lengthOf(1); + expect(fetchedDocuments[0]).to.be.an.instanceOf(Document); + expect(fetchedDocuments[0].toObject()).to.deep.equal(documents[4].toObject()); + + // costs are not calculating without block info + expect(processingCost).to.be.equal(0); + }); + + it('should query existing documents again', async () => { + // Create documents + await Promise.all( + documents.map((document) => drive.createDocument(document, blockInfo)), + ); + + // eslint-disable-next-line no-unused-vars + const [fetchedDocuments, processingCost] = await drive.queryDocuments(dataContract, 'indexedDocument', blockInfo.epoch, { + where: [['lastName', '==', 'Kennedy']], + }); + + expect(fetchedDocuments).to.have.lengthOf(1); + expect(fetchedDocuments[0]).to.be.an.instanceOf(Document); + expect(fetchedDocuments[0].toObject()).to.deep.equal(documents[4].toObject()); + + expect(processingCost).to.be.greaterThan(0); + }); + + it('should return empty array if documents are not exist', async () => { + // eslint-disable-next-line no-unused-vars + const [fetchedDocuments, processingCost] = await drive.queryDocuments(dataContract, 'indexedDocument', blockInfo.epoch, { + where: [['lastName', '==', 'Kennedy']], + }); + + expect(fetchedDocuments).to.have.lengthOf(0); + expect(processingCost).to.be.greaterThan(0); + }); + }); + + describe('#proveDocumentsQuery', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + await drive.createContract(dataContract, blockInfo); + }); + + it('should query existing documents', async () => { + // Create documents + await Promise.all( + documents.map((document) => drive.createDocument(document, blockInfo)), + ); + + const result = await drive.proveDocumentsQuery(dataContract, 'indexedDocument', { + where: [['lastName', '==', 'Kennedy']], + }); + + expect(result).to.have.lengthOf(2); + + const [proofs, processingCost] = result; + expect(proofs).to.be.an.instanceOf(Buffer); + expect(proofs.length).to.be.greaterThan(0); + + // TODO: We do not calculating processing costs for poofs yet + expect(processingCost).to.equal(0); + }); + }); + + describe('#insertIdentity', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should create identity if not exists', async () => { + const result = await drive.insertIdentity(identity, blockInfo); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.not.deep.equals(initialRootHash); + }); + }); + + describe('#fetchIdentity', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if identity not exists', async () => { + const result = await drive.fetchIdentity(Buffer.alloc(32)); + + expect(result).to.be.null(); + }); + + it('should return identity if it is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.fetchIdentity(identity.getId()); + + expect(result.toBuffer()).to.deep.equal(identity.toBuffer()); + }); + }); + + describe('#fetchIdentityWithCosts', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if contract not exists', async () => { + const result = await drive.fetchIdentityWithCosts(Buffer.alloc(32), blockInfo.epoch); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [fetchedIdentity, feeResult] = result; + + expect(fetchedIdentity).to.be.null(); + + expect(feeResult).to.be.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + + it('should return contract if contract is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.fetchIdentityWithCosts(identity.getId(), blockInfo.epoch); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [fetchedIdentity, feeResult] = result; + + expect(fetchedIdentity.toBuffer()).to.deep.equal(identity.toBuffer()); + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + }); + + describe('#fetchIdentityBalance', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if identity not exists', async () => { + const result = await drive.fetchIdentityBalance(Buffer.alloc(32)); + + expect(result).to.be.null(); + }); + + it('should return balance if identity is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const balance = await drive.fetchIdentityBalance(identity.getId()); + + expect(balance).to.deep.equal(identity.getBalance()); + }); + }); + + describe('#fetchIdentityBalanceWithCosts', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if identity not exists', async () => { + const result = await drive.fetchIdentityBalanceWithCosts(Buffer.alloc(32), blockInfo); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [balance, feeResult] = result; + + expect(balance).to.be.null(); + + expect(feeResult).to.be.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + + it('should return contract if identity is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.fetchIdentityBalanceWithCosts(identity.getId(), blockInfo); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [balance, feeResult] = result; + + expect(balance).to.equal(identity.getBalance()); + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + }); + + describe('#fetchIdentityBalanceIncludeDebtWithCosts', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return null if identity not exists', async () => { + const result = await drive.fetchIdentityBalanceIncludeDebtWithCosts( + Buffer.alloc(32), + blockInfo, + ); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [balance, feeResult] = result; + + expect(balance).to.be.null(); + + expect(feeResult).to.be.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + + it('should return balance if identity is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.fetchIdentityBalanceIncludeDebtWithCosts( + identity.getId(), + blockInfo, + ); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.have.lengthOf(2); + + const [balance, feeResult] = result; + + expect(balance).to.equal(identity.getBalance()); + + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + }); + }); + + describe('#addToIdentityBalance', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should add to balance', async () => { + await drive.insertIdentity(identity, blockInfo); + + const amount = 100; + + const result = await drive.addToIdentityBalance( + identity.getId(), + amount, + blockInfo, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getBalance()).to.equals(identity.getBalance() + amount); + }); + + it('should not update state with dry run', async () => { + initialRootHash = await drive.getGroveDB().getRootHash(); + + const result = await drive.addToIdentityBalance( + identity.getId(), + 100, + blockInfo, + false, + true, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#removeFromIdentitiyBalance', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should remove from balance', async () => { + await drive.insertIdentity(identity, blockInfo); + + const amount = 2; + + const result = await drive.removeFromIdentityBalance( + identity.getId(), + amount, + blockInfo, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getBalance()).to.equals( + identity.getBalance() - amount, + ); + }); + + it('should not update state with dry run', async () => { + initialRootHash = await drive.getGroveDB().getRootHash(); + + const amount = 2; + + const result = await drive.removeFromIdentityBalance( + identity.getId(), + amount, + blockInfo, + false, + true, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#applyFeesToIdentityBalance', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should change balance according to provided fees', async () => { + blockInfo.epoch = 3; + + await drive.insertIdentity(identity, blockInfo); + + const feeResult = FeeResult.create(10000, 10, [{ + identifier: identity.getId().toBuffer(), + creditsPerEpoch: { 0: 1000000 }, + }]); + + const result = await drive.applyFeesToIdentityBalance( + identity.getId(), + feeResult, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getBalance()).to.not.equals( + identity.getBalance(), + ); + }); + }); + + describe('#addToSystemCredits', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should add to system credits', async () => { + await drive.addToSystemCredits( + 100, + ); + }); + }); + + describe('#fetchIdentitiesByPublicKeyHashes', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should return empty array if identity not exists', async () => { + const result = await drive.fetchIdentitiesByPublicKeyHashes([Buffer.alloc(20)]); + + expect(result).to.be.instanceOf(Array); + expect(result).to.be.empty(); + }); + + it('should return identities if it is present', async () => { + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.fetchIdentitiesByPublicKeyHashes( + identity.getPublicKeys().map((k) => k.hash()), + ); + + expect(result).to.deep.equal(identity.getPublicKeys().map(() => identity)); + }); + }); + + describe('#addKeysToIdentity', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should add keys to identity', async () => { + const keysToAdd = [identity.getPublicKeys().pop()]; + + await drive.insertIdentity(identity, blockInfo); + + const result = await drive.addKeysToIdentity( + identity.getId(), + keysToAdd, + blockInfo, + ); + + expectFeeResult(result); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getPublicKeys()).to.have.lengthOf( + identity.getPublicKeys().length + keysToAdd.length, + ); + }); + + it('should not update state with dry run', async () => { + initialRootHash = await drive.getGroveDB().getRootHash(); + + const keysToAdd = [identity.getPublicKeys().pop()]; + + const result = await drive.addKeysToIdentity( + identity.getId(), + keysToAdd, + blockInfo, + false, + true, + ); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#disableIdentityKeys', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should disable specified identity keys', async () => { + await drive.insertIdentity(identity, blockInfo); + + const keyIds = identity.getPublicKeys().map((key) => key.getId()); + const disableAt = Date.now(); + + const result = await drive.disableIdentityKeys( + identity.getId(), + keyIds, + disableAt, + blockInfo, + ); + + expectFeeResult(result); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getPublicKeys()).to.have.lengthOf(identity.getPublicKeys().length); + + fetchedIdentity.getPublicKeys().forEach((key) => { + expect(key.getDisabledAt()).to.equals(disableAt); + }); + }); + + it('should not update state with dry run', async () => { + const keyIds = identity.getPublicKeys().map((key) => key.getId()); + const disableAt = Date.now(); + + const result = await drive.disableIdentityKeys( + identity.getId(), + keyIds, + disableAt, + blockInfo, + false, + true, + ); + + expectFeeResult(result); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#updateIdentityRevision', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + + initialRootHash = await drive.getGroveDB().getRootHash(); + }); + + it('should update identity revision', async () => { + await drive.insertIdentity(identity, blockInfo); + + const revision = 2; + + const result = await drive.updateIdentityRevision( + identity.getId(), + revision, + blockInfo, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + const fetchedIdentity = await drive.fetchIdentity(identity.getId()); + + expect(fetchedIdentity.getRevision()).to.equals(revision); + }); + + it('should not update state with dry run', async () => { + const result = await drive.updateIdentityRevision( + identity.getId(), + 2, + blockInfo, + false, + true, + ); + + expect(result).to.be.an.instanceOf(FeeResult); + + expect(result.processingFee).to.greaterThan(0, 'processing fee must be higher than 0'); + expect(result.storageFee).to.be.equal(0, 'storage fee must be equal to 0'); + + expect(await drive.getGroveDB().getRootHash()).to.deep.equals(initialRootHash); + }); + }); + + describe('#fetchLatestWithdrawalTransactionIndex', () => { + beforeEach(async () => { + await drive.createInitialStateStructure(); + }); + + it('should return 0 on the first call', async () => { + const result = await drive.fetchLatestWithdrawalTransactionIndex({ + height: 1, + epoch: 1, + timeMs: (new Date()).getTime(), + }); + + expect(result).to.equal(0); + }); + }); + + describe('ABCI', () => { + let initChainRequest; + + function generateRequiredIdentityPublicKeysSet() { + return { + master: new PrivateKey().toPublicKey().toBuffer(), + high: new PrivateKey().toPublicKey().toBuffer(), + }; + } + + beforeEach(() => { + initChainRequest = { + genesisTimeMs: Date.now(), + systemIdentityPublicKeys: { + masternodeRewardSharesContractOwner: generateRequiredIdentityPublicKeysSet(), + featureFlagsContractOwner: generateRequiredIdentityPublicKeysSet(), + dpnsContractOwner: generateRequiredIdentityPublicKeysSet(), + withdrawalsContractOwner: generateRequiredIdentityPublicKeysSet(), + dashpayContractOwner: generateRequiredIdentityPublicKeysSet(), + }, + }; + }); + + describe('InitChain', () => { + it('should successfully init chain', async () => { + const response = await drive.getAbci().initChain(initChainRequest); + + expect(response).to.be.empty('object'); + }); + }); + + describe('BlockBegin', () => { + beforeEach(async () => { + await drive.getAbci().initChain(initChainRequest); + }); + + it('should process a block without previous block time', async () => { + const request = { + blockHeight: 1, + blockTimeMs: (new Date()).getTime(), + proposerProTxHash: Buffer.alloc(32, 1), + proposedAppVersion: 1, + validatorSetQuorumHash: Buffer.alloc(32, 2), + lastSyncedCoreHeight: 1, + coreChainLockedHeight: 1, + totalHpmns: 100, + }; + + const response = await drive.getAbci().blockBegin(request); + + expect(response.unsignedWithdrawalTransactions).to.be.empty(); + expect(response.epochInfo).to.deep.equal({ + currentEpochIndex: 0, + isEpochChange: true, + previousEpochIndex: null, + }); + }); + + it('should process a block with previous block time', async () => { + const blockTimeMs = (new Date()).getTime(); + + await drive.getAbci().blockBegin({ + blockHeight: 1, + blockTimeMs, + proposerProTxHash: Buffer.alloc(32, 1), + proposedAppVersion: 1, + validatorSetQuorumHash: Buffer.alloc(32, 2), + lastSyncedCoreHeight: 1, + coreChainLockedHeight: 1, + totalHpmns: 100, + }); + + const response = await drive.getAbci().blockBegin({ + blockHeight: 2, + blockTimeMs: blockTimeMs + 100, + proposerProTxHash: Buffer.alloc(32, 1), + previousBlockTimeMs: blockTimeMs, + proposedAppVersion: 1, + validatorSetQuorumHash: Buffer.alloc(32, 2), + lastSyncedCoreHeight: 1, + coreChainLockedHeight: 1, + totalHpmns: 100, + }); + + expect(response.unsignedWithdrawalTransactions).to.be.empty(); + expect(response.epochInfo).to.deep.equal({ + currentEpochIndex: 0, + isEpochChange: false, + previousEpochIndex: null, + }); + }); + }); + + describe('BlockEnd', () => { + beforeEach(async () => { + await drive.getAbci().initChain(initChainRequest); + + await drive.getAbci().blockBegin({ + blockHeight: 1, + blockTimeMs: (new Date()).getTime(), + proposedAppVersion: 1, + proposerProTxHash: Buffer.alloc(32, 1), + validatorSetQuorumHash: Buffer.alloc(32, 2), + lastSyncedCoreHeight: 1, + coreChainLockedHeight: 1, + totalHpmns: 100, + }); + }); + + it('should process a block', async () => { + const request = { + fees: { + storageFee: 0, + processingFee: 0, + refundsPerEpoch: {}, + }, + }; + + const response = await drive.getAbci().blockEnd(request); + + expect(response).to.have.property('proposersPaidCount'); + expect(response).to.have.property('paidEpochIndex'); + expect(response).to.have.property('refundedEpochsCount'); + }); + }); + + describe('AfterFinalizeBlock', () => { + beforeEach(async function beforeEach() { + this.timeout(10000); + + await drive.getAbci().initChain(initChainRequest); + + await drive.createContract(dataContract, blockInfo); + await drive.createContract(withdrawalsDataContract, blockInfo); + + await drive.getAbci().blockBegin({ + blockHeight: 1, + blockTimeMs: (new Date()).getTime(), + proposedAppVersion: 1, + proposerProTxHash: Buffer.alloc(32, 1), + validatorSetQuorumHash: Buffer.alloc(32, 2), + lastSyncedCoreHeight: 1, + coreChainLockedHeight: 1, + totalHpmns: 100, + }); + + await drive.getAbci().blockEnd({ + fees: { + storageFee: 0, + processingFee: 0, + refundsPerEpoch: {}, + }, + }); + }); + + it('should process a block', async function it() { + this.timeout(10000); + + const request = { + updatedDataContractIds: [dataContract.getId()], + }; + + const response = await drive.getAbci().afterFinalizeBlock(request); + + expect(response).to.be.empty(); + }); + }); + }); + + describe('.calculateStorageFeeDistributionAmountAndLeftovers', () => { + it('should calculate amount and leftovers', () => { + const result = Drive.calculateStorageFeeDistributionAmountAndLeftovers(1000, 1, 2); + + expect(result).to.be.an.instanceOf(Array); + expect(result).to.be.lengthOf(2); + + const [amount, leftovers] = result; + + expect(amount).to.equals(556); + expect(leftovers).to.equals(440); + }); + }); +}); diff --git a/packages/rs-drive-nodejs/test/GroveDB.spec.js b/packages/rs-drive-nodejs/test/GroveDB.spec.js new file mode 100644 index 00000000000..0e2000d161a --- /dev/null +++ b/packages/rs-drive-nodejs/test/GroveDB.spec.js @@ -0,0 +1,1240 @@ +const fs = require('fs'); + +const { expect, use } = require('chai'); +use(require('dirty-chai')); + +const Drive = require('../Drive'); + +const TEST_DATA_PATH = './test_data'; + +describe('GroveDB', () => { + let drive; + let groveDb; + let treeKey; + let itemKey; + let itemValue; + let rootTreePath; + let itemTreePath; + let otherTreeKey; + + beforeEach(() => { + drive = new Drive(TEST_DATA_PATH, { + drive: { + dataContractsGlobalCacheSize: 500, + dataContractsBlockCacheSize: 500, + }, + core: { + rpc: { + url: '127.0.0.1', + username: '', + password: '', + }, + }, + }); + + groveDb = drive.getGroveDB(); + + treeKey = Buffer.from('test_tree'); + itemKey = Buffer.from('test_key'); + itemValue = Buffer.from('very nice test value'); + + rootTreePath = []; + itemTreePath = [treeKey]; + + otherTreeKey = Buffer.from('other_tree'); + }); + + afterEach(async () => { + await drive.close(); + + fs.rmSync(TEST_DATA_PATH, { recursive: true }); + }); + + it('should store and retrieve reference', async () => { + await groveDb.insert( + rootTreePath, + otherTreeKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + await groveDb.insert( + [otherTreeKey], + itemKey, + { + type: 'reference', + epoch: 0, + value: { + type: 'absolutePathReference', + path: [...itemTreePath, itemKey], + }, + }, + ); + + const result = await groveDb.get( + [otherTreeKey], + itemKey, + ); + + expect(result).to.exist(); + expect(result.type).to.equals('item'); + expect(result.value).to.deep.equals(itemValue); + }); + + it('should store and retrieve a value', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + const element = await groveDb.get(itemTreePath, itemKey); + + expect(element.type).to.be.equal('item'); + expect(element.value).to.deep.equal(itemValue); + }); + + it('should store and delete a value', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + // Get item + const element = await groveDb.get(itemTreePath, itemKey); + + expect(element.type).to.be.equal('item'); + expect(element.value).to.deep.equal(itemValue); + + // Delete an item from the subtree + await groveDb.delete( + itemTreePath, + itemKey, + ); + + try { + await groveDb.get(itemTreePath, itemKey); + + expect.fail('Expected to throw en error'); + } catch (e) { + expect(e.message).to.be.equal('grovedb: path key not found: key not found in Merk for get: 746573745f6b6579'); + } + }); + + describe('#get', () => { + it('should return error if key is not exist', async () => { + try { + await groveDb.get([], Buffer.from('nothing')); + + expect.fail('should throw an error'); + } catch (e) { + expect(e.message).to.equal('grovedb: path key not found: key not found in Merk for get: 6e6f7468696e67'); + // appendStack wrapper should add call stack to neon binding errors + expect(e.stack).to.not.equal('grovedb: path key not found: key not found in Merk for get: 6e6f7468696e67'); + expect(e.stack).to.be.a('string').and.satisfy((msg) => ( + msg.startsWith('Error: grovedb: path key not found: key not found in Merk for get: 6e6f7468696e67') + )); + } + }); + }); + + describe('#startTransaction', () => { + it('should not allow to read transactional data from main database until it\'s committed', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree' }, + ); + + await groveDb.startTransaction(); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + true, + ); + + // Inserted value is not yet commited, but can be retrieved by `get` + // with `useTransaction` flag. + const elementInTransaction = await groveDb.get(itemTreePath, itemKey, true); + + expect(elementInTransaction.type).to.be.equal('item'); + expect(elementInTransaction.value).to.deep.equal(itemValue); + + // ... and using `get` without the flag should return no value + try { + await groveDb.get(itemTreePath, itemKey); + + expect.fail('Expected to throw an error'); + } catch (e) { + expect(e.message).to.be.equal('grovedb: path key not found: key not found in Merk for get: 746573745f6b6579'); + } + }); + }); + + describe('#commitTransaction', () => { + it('should commit transactional data to main database', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.startTransaction(); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + true, + ); + + // ... and using `get` without the flag should return no value + try { + await groveDb.get(itemTreePath, itemKey); + + expect.fail('Expected to throw an error'); + } catch (e) { + expect(e.message).to.be.equal('grovedb: path key not found: key not found in Merk for get: 746573745f6b6579'); + } + + await groveDb.commitTransaction(); + + // When committed, the value should be accessible without running transaction + const element = await groveDb.get(itemTreePath, itemKey); + expect(element.type).to.be.equal('item'); + expect(element.value).to.deep.equal(itemValue); + }); + }); + + describe('#rollbackTransaction', () => { + it('should rollaback transaction state to its initial state', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.startTransaction(); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + true, + ); + + // Should rollback inserted item + await groveDb.rollbackTransaction(); + + try { + await groveDb.get(itemTreePath, itemKey); + + expect.fail('Expected to throw an error'); + } catch (e) { + expect(e.message).to.be.equal('grovedb: path key not found: key not found in Merk for get: 746573745f6b6579'); + } + }); + }); + + describe('#isTransactionStarted', () => { + it('should return true if transaction is started', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0, value: Buffer.alloc(32) }, + ); + + await groveDb.startTransaction(); + + const result = await groveDb.isTransactionStarted(); + + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.true; + }); + + it('should return false if transaction is not started', async () => { + const result = await groveDb.isTransactionStarted(); + + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.false; + }); + }); + + describe('#abortTransaction', () => { + it('should abort transaction', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.startTransaction(); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + true, + ); + + // Should abort inserted item + await groveDb.abortTransaction(); + + const isTransactionStarted = await groveDb.isTransactionStarted(); + + // eslint-disable-next-line no-unused-expressions + expect(isTransactionStarted).to.be.false; + + try { + await groveDb.get(itemTreePath, itemKey); + + expect.fail('Expected to throw an error'); + } catch (e) { + expect(e.message).to.be.equal('grovedb: path key not found: key not found in Merk for get: 746573745f6b6579'); + } + }); + }); + + describe('#insertIfNotExists', () => { + it('should insert a value if key is not exist yet', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + // Inserting an item into the subtree + await groveDb.insertIfNotExists( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + const element = await groveDb.get(itemTreePath, itemKey); + + expect(element.type).to.equal('item'); + expect(element.value).to.deep.equal(itemValue); + }); + + it('shouldn\'t overwrite already stored value', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + const newItemValue = Buffer.from('replaced item value'); + + // Inserting an item into the subtree + await groveDb.insertIfNotExists( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: newItemValue }, + ); + + const element = await groveDb.get(itemTreePath, itemKey); + + expect(element.type).to.equal('item'); + expect(element.value).to.deep.equal(itemValue); + }); + }); + + describe('#insert', () => { + it('should be able to insert a tree', async () => { + await groveDb.insert( + [], + Buffer.from('test_tree'), + { type: 'tree', epoch: 0 }, + ); + }); + + it('should throw when trying to insert non-existent element type', async () => { + const path = []; + const key = Buffer.from('test_key'); + + try { + await groveDb.insert( + path, + key, + { type: 'not_a_tree', epoch: 0 }, + ); + + expect.fail('Expected to throw en error'); + } catch (e) { + expect(e.message).to.be.equal('Unexpected element type not_a_tree'); + } + }); + }); + + describe('auxiliary data methods', () => { + let key; + let value; + + beforeEach(() => { + key = Buffer.from('aux_key'); + value = Buffer.from('ayy'); + }); + + it('should be able to store and get aux data', async () => { + await groveDb.putAux(key, value); + + const result = await groveDb.getAux(key); + + expect(result).to.deep.equal(value); + }); + + it('should be able to insert and delete aux data', async () => { + await groveDb.putAux(key, value); + + await groveDb.deleteAux(key); + + const result = await groveDb.getAux(key); + + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + }); + }); + + describe('#query', () => { + let aValue; + let aKey; + let bValue; + let bKey; + let cValue; + let cKey; + + beforeEach(async () => { + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + aValue = Buffer.from('a'); + aKey = Buffer.from('aKey'); + bValue = Buffer.from('b'); + bKey = Buffer.from('bKey'); + cValue = Buffer.from('c'); + cKey = Buffer.from('cKey'); + + await groveDb.insert( + itemTreePath, + aKey, + { type: 'item', epoch: 0, value: aValue }, + ); + + await groveDb.insert( + itemTreePath, + bKey, + { type: 'item', epoch: 0, value: bValue }, + ); + + await groveDb.insert( + itemTreePath, + cKey, + { type: 'item', epoch: 0, value: cValue }, + ); + }); + + it('should be able to use limit', async () => { + const query = { + path: itemTreePath, + query: { + limit: 1, + query: { + items: [ + { + type: 'rangeFrom', + from: bValue, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to use offset', async () => { + const query = { + path: itemTreePath, + query: { + offset: 1, + query: { + items: [ + { + type: 'rangeFrom', + from: bValue, + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + cValue, + ]); + + expect(skipped).to.equals(1); + }); + + it('should be able to retrieve data using `key`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'key', + key: aKey, + }, + { + type: 'key', + key: bKey, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `range`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'range', + from: aKey, + to: cKey, + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeInclusive`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeInclusive', + from: aKey, + to: bKey, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeFull`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeFull', + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + cValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeFrom`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeFrom', + from: bKey, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + bValue, + cValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeTo`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeTo', + to: cKey, + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeToInclusive`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeToInclusive', + to: cKey, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + aValue, + bValue, + cValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeAfter`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeAfter', + after: aKey, + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + bValue, + cValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeAfterTo`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeAfterTo', + after: aKey, + to: cKey, + }, + ], + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + bValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data using `rangeAfterToInclusive`', async () => { + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeAfterToInclusive', + after: aKey, + to: cKey, + }, + ], + }, + + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + bValue, + cValue, + ]); + + expect(skipped).to.equals(0); + }); + + it('should be able to retrieve data with subquery', async () => { + // Prepare tree structure + const dKey = Buffer.from('dKey'); + const daValue = Buffer.from('da'); + const dbValue = Buffer.from('db'); + const dcValue = Buffer.from('dc'); + const eaValue = Buffer.from('ea'); + const eaKey = Buffer.from('eaKey'); + const ebValue = Buffer.from('eb'); + + const dPath = [...itemTreePath]; + + dPath.push(dKey); + + await groveDb.insert( + itemTreePath, + dKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + dPath, + Buffer.from('daKey'), + { type: 'item', epoch: 0, value: daValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dbKey'), + { type: 'item', epoch: 0, value: dbValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dcKey'), + { type: 'item', epoch: 0, value: dcValue }, + ); + + const eKey = Buffer.from('eKey'); + + const ePath = [...itemTreePath]; + ePath.push(eKey); + await groveDb.insert( + itemTreePath, + eKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + ePath, + Buffer.from('eaKey'), + { type: 'item', epoch: 0, value: eaValue }, + ); + + await groveDb.insert( + ePath, + Buffer.from('ebKey'), + { type: 'item', epoch: 0, value: ebValue }, + ); + + // This should give us only last subtree and apply subquery to it + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeAfter', + after: dKey, + }, + ], + subquery: { + items: [ + { + type: 'rangeAfter', + after: eaKey, + }, + ], + }, + }, + }, + }; + + const result = await groveDb.query(query); + + expect(result).to.have.a.lengthOf(2); + + const [elementValues, skipped] = result; + + expect(elementValues).to.deep.equals([ + ebValue, + ]); + + expect(skipped).to.equals(0); + }); + }); + + describe('#proveQuery', () => { + let dPath; + let dKey; + let ePath; + + let daValue; + let dbValue; + let dcValue; + let eaValue; + let eaKey; + let ebValue; + + beforeEach(async () => { + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + dKey = Buffer.from('dKey'); + daValue = Buffer.from('da'); + dbValue = Buffer.from('db'); + dcValue = Buffer.from('dc'); + eaValue = Buffer.from('ea'); + eaKey = Buffer.from('eaKey'); + ebValue = Buffer.from('eb'); + + dPath = [...itemTreePath]; + dPath.push(dKey); + await groveDb.insert( + itemTreePath, + dKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + dPath, + Buffer.from('daKey'), + { type: 'item', epoch: 0, value: daValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dbKey'), + { type: 'item', epoch: 0, value: dbValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dcKey'), + { type: 'item', epoch: 0, value: dcValue }, + ); + + const eKey = Buffer.from('eKey'); + ePath = [...itemTreePath]; + ePath.push(eKey); + await groveDb.insert( + itemTreePath, + eKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + ePath, + Buffer.from('eaKey'), + { type: 'item', epoch: 0, value: eaValue }, + ); + + await groveDb.insert( + ePath, + Buffer.from('ebKey'), + { type: 'item', epoch: 0, value: ebValue }, + ); + }); + + it('should be able to retrieve provable data with subquery', async () => { + // This should give us only last subtree and apply subquery to it + const query = { + path: itemTreePath, + query: { + query: { + items: [ + { + type: 'rangeAfter', + after: dKey, + }, + ], + subquery: { + items: [ + { + type: 'rangeAfter', + after: eaKey, + }, + ], + }, + }, + }, + }; + + const result = await groveDb.proveQuery(query); + + expect(result).to.exist(); + + expect(result).to.be.instanceOf(Buffer); + expect(result).to.have.lengthOf(213); + }); + }); + + describe('#proveQueryMany', () => { + let dPath; + let dKey; + let ePath; + + let daValue; + let dbValue; + let dcValue; + let eaValue; + let eaKey; + let ebValue; + + beforeEach(async () => { + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + dKey = Buffer.from('dKey'); + daValue = Buffer.from('da'); + dbValue = Buffer.from('db'); + dcValue = Buffer.from('dc'); + eaValue = Buffer.from('ea'); + eaKey = Buffer.from('eaKey'); + ebValue = Buffer.from('eb'); + + dPath = [...itemTreePath]; + dPath.push(dKey); + await groveDb.insert( + itemTreePath, + dKey, + { type: 'tree', epoch: 0 }, + ); + + await groveDb.insert( + dPath, + Buffer.from('daKey'), + { type: 'item', epoch: 0, value: daValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dbKey'), + { type: 'item', epoch: 0, value: dbValue }, + ); + + await groveDb.insert( + dPath, + Buffer.from('dcKey'), + { type: 'item', epoch: 0, value: dcValue }, + ); + + const eKey = Buffer.from('eKey'); + ePath = [...itemTreePath]; + ePath.push(eKey); + await groveDb.insert( + itemTreePath, + eKey, + { type: 'tree' }, + ); + + await groveDb.insert( + ePath, + Buffer.from('eaKey'), + { type: 'item', epoch: 0, value: eaValue }, + ); + + const ownerId = Buffer.alloc(32).fill('c'); + + await groveDb.insert( + ePath, + Buffer.from('ebKey'), + { + type: 'item', + epoch: 0, + ownerId, + value: ebValue, + }, + ); + }); + + it('should be able to prove query', async () => { + const query1 = { + path: ePath, + query: { + query: { + items: [ + { + type: 'key', + key: dKey, + }, + ], + }, + }, + }; + + const query2 = { + path: dPath, + query: { + query: { + items: [ + { + type: 'key', + key: eaKey, + }, + ], + }, + }, + }; + + const result = await groveDb.proveQueryMany([query1, query2]); + + expect(result).to.exist(); + + expect(result).to.be.instanceOf(Buffer); + expect(result).to.have.lengthOf(348); + }); + }); + + describe('#flush', () => { + it('should flush data on disc', async () => { + await groveDb.insert( + [], + Buffer.from('test_tree'), + { type: 'tree', epoch: 0 }, + ); + + await groveDb.flush(); + }); + }); + + describe('#getRootHash', () => { + it('should return empty root hash if there is no data', async () => { + const result = await groveDb.getRootHash(); + + expect(result).to.deep.equal(Buffer.alloc(32)); + + // Get root hash for transaction too + await groveDb.startTransaction(); + + const transactionalResult = await groveDb.getRootHash(true); + + expect(transactionalResult).to.deep.equal(Buffer.alloc(32)); + }); + + it('should root hash', async () => { + // Making a subtree to insert items into + await groveDb.insert( + rootTreePath, + treeKey, + { type: 'tree', epoch: 0 }, + ); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + itemKey, + { type: 'item', epoch: 0, value: itemValue }, + ); + + await groveDb.startTransaction(); + + // Inserting an item into the subtree + await groveDb.insert( + itemTreePath, + Buffer.from('transactional_test_key'), + { type: 'item', epoch: 0, value: itemValue }, + true, + ); + + const result = await groveDb.getRootHash(); + const transactionalResult = await groveDb.getRootHash(true); + + // Hashes shouldn't be equal + expect(result).to.not.deep.equal(transactionalResult); + + // Hashes shouldn't be empty + + // eslint-disable-next-line no-unused-expressions + expect(result >= Buffer.alloc(32)).to.be.true; + + // eslint-disable-next-line no-unused-expressions + expect(transactionalResult >= Buffer.alloc(32)).to.be.true; + }); + }); +}); diff --git a/packages/rs-drive-nodejs/test/utils.js b/packages/rs-drive-nodejs/test/utils.js new file mode 100644 index 00000000000..c5e0fd37539 --- /dev/null +++ b/packages/rs-drive-nodejs/test/utils.js @@ -0,0 +1,16 @@ +const { expect } = require('chai'); +const FeeResult = require('../FeeResult'); + +/** + * @param {FeeResult} feeResult + */ +function expectFeeResult(feeResult) { + expect(feeResult).to.be.an.instanceOf(FeeResult); + + expect(feeResult.processingFee).to.be.greaterThan(0, 'processing fee must be higher than 0'); + expect(feeResult.storageFee).to.be.greaterThan(0, 'storage fee must be higher than 0'); +} + +module.exports = { + expectFeeResult, +}; diff --git a/yarn.lock b/yarn.lock index c9f1dbf8da1..b95d8d8fb9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2939,13 +2939,6 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.9": - version: 7.0.11 - resolution: "@types/json-schema@npm:7.0.11" - checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d - languageName: node - linkType: hard - "@types/json5@npm:^0.0.29": version: 0.0.29 resolution: "@types/json5@npm:0.0.29" @@ -3864,16 +3857,6 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.0": - version: 1.0.0 - resolution: "array-buffer-byte-length@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - is-array-buffer: ^3.0.1 - checksum: 044e101ce150f4804ad19c51d6c4d4cfa505c5b2577bd179256e4aa3f3f6a0a5e9874c78cd428ee566ac574c8a04d7ce21af9fe52e844abfdccb82b33035a7c3 - languageName: node - linkType: hard - "array-differ@npm:^3.0.0": version: 3.0.0 resolution: "array-differ@npm:3.0.0" @@ -6003,16 +5986,6 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.4": - version: 1.2.0 - resolution: "define-properties@npm:1.2.0" - dependencies: - has-property-descriptors: ^1.0.0 - object-keys: ^1.1.1 - checksum: e60aee6a19b102df4e2b1f301816804e81ab48bb91f00d0d935f269bf4b3f79c88b39e4f89eaa132890d23267335fd1140dfcd8d5ccd61031a0a2c41a54e33a6 - languageName: node - linkType: hard - "delay@npm:^5.0.0": version: 5.0.0 resolution: "delay@npm:5.0.0" @@ -6520,48 +6493,6 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.20.4": - version: 1.21.2 - resolution: "es-abstract@npm:1.21.2" - dependencies: - array-buffer-byte-length: ^1.0.0 - available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 - es-set-tostringtag: ^2.0.1 - es-to-primitive: ^1.2.1 - function.prototype.name: ^1.1.5 - get-intrinsic: ^1.2.0 - get-symbol-description: ^1.0.0 - globalthis: ^1.0.3 - gopd: ^1.0.1 - has: ^1.0.3 - has-property-descriptors: ^1.0.0 - has-proto: ^1.0.1 - has-symbols: ^1.0.3 - internal-slot: ^1.0.5 - is-array-buffer: ^3.0.2 - is-callable: ^1.2.7 - is-negative-zero: ^2.0.2 - is-regex: ^1.1.4 - is-shared-array-buffer: ^1.0.2 - is-string: ^1.0.7 - is-typed-array: ^1.1.10 - is-weakref: ^1.0.2 - object-inspect: ^1.12.3 - object-keys: ^1.1.1 - object.assign: ^4.1.4 - regexp.prototype.flags: ^1.4.3 - safe-regex-test: ^1.0.0 - string.prototype.trim: ^1.2.7 - string.prototype.trimend: ^1.0.6 - string.prototype.trimstart: ^1.0.6 - typed-array-length: ^1.0.4 - unbox-primitive: ^1.0.2 - which-typed-array: ^1.1.9 - checksum: 037f55ee5e1cdf2e5edbab5524095a4f97144d95b94ea29e3611b77d852fd8c8a40e7ae7101fa6a759a9b9b1405f188c3c70928f2d3cd88d543a07fc0d5ad41a - languageName: node - linkType: hard - "es-module-lexer@npm:^0.9.0": version: 0.9.3 resolution: "es-module-lexer@npm:0.9.3" @@ -6826,13 +6757,6 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": - version: 3.3.0 - resolution: "eslint-visitor-keys@npm:3.3.0" - checksum: d59e68a7c5a6d0146526b0eec16ce87fbf97fe46b8281e0d41384224375c4e52f5ffb9e16d48f4ea50785cde93f766b0c898e31ab89978d88b0e1720fbfb7808 - languageName: node - linkType: hard - "eslint@npm:^7.32.0": version: 7.32.0 resolution: "eslint@npm:7.32.0" @@ -7310,7 +7234,7 @@ __metadata: languageName: node linkType: hard -"foreach@npm:^2.0.4, foreach@npm:^2.0.5": +"foreach@npm:^2.0.4": version: 2.0.5 resolution: "foreach@npm:2.0.5" checksum: dab4fbfef0b40b69ee5eab81bcb9626b8fa8b3469c8cfa26480f3e5e1ee08c40eae07048c9a967c65aeda26e774511ccc70b3f10a604c01753c6ef24361f0fc8 @@ -7521,17 +7445,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0": - version: 1.2.0 - resolution: "get-intrinsic@npm:1.2.0" - dependencies: - function-bind: ^1.1.1 - has: ^1.0.3 - has-symbols: ^1.0.3 - checksum: 78fc0487b783f5c58cf2dccafc3ae656ee8d2d8062a8831ce4a95e7057af4587a1d4882246c033aca0a7b4965276f4802b45cc300338d1b77a73d3e3e3f4877d - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -7882,13 +7795,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"has-bigints@npm:^1.0.2": - version: 1.0.2 - resolution: "has-bigints@npm:1.0.2" - checksum: 390e31e7be7e5c6fe68b81babb73dfc35d413604d7ee5f56da101417027a4b4ce6a27e46eff97ad040c835b5d228676eae99a9b5c3bc0e23c8e81a49241ff45b - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -7919,7 +7825,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.2": +"has-symbols@npm:^1.0.2": version: 1.0.2 resolution: "has-symbols@npm:1.0.2" checksum: 2309c426071731be792b5be43b3da6fb4ed7cbe8a9a6bcfca1862587709f01b33d575ce8f5c264c1eaad09fca2f9a8208c0a2be156232629daa2dd0c0740976b @@ -8362,17 +8268,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"internal-slot@npm:^1.0.5": - version: 1.0.5 - resolution: "internal-slot@npm:1.0.5" - dependencies: - get-intrinsic: ^1.2.0 - has: ^1.0.3 - side-channel: ^1.0.4 - checksum: 97e84046bf9e7574d0956bd98d7162313ce7057883b6db6c5c7b5e5f05688864b0978ba07610c726d15d66544ffe4b1050107d93f8a39ebc59b15d8b429b497a - languageName: node - linkType: hard - "interpret@npm:^1.0.0": version: 1.4.0 resolution: "interpret@npm:1.4.0" @@ -8471,7 +8366,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": +"is-callable@npm:^1.1.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" checksum: 1a28d57dc435797dae04b173b65d6d1e77d4f16276e9eff973f994eadcfdc30a017e6a597f092752a083c1103cceb56c91e3dadc6692fedb9898dfaba701575f @@ -8593,13 +8488,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.2": - version: 2.0.2 - resolution: "is-negative-zero@npm:2.0.2" - checksum: f3232194c47a549da60c3d509c9a09be442507616b69454716692e37ae9f37c4dea264fb208ad0c9f3efd15a796a46b79df07c7e53c6227c32170608b809149a - languageName: node - linkType: hard - "is-number-object@npm:^1.0.4": version: 1.0.6 resolution: "is-number-object@npm:1.0.6" @@ -8688,15 +8576,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.2": - version: 1.0.2 - resolution: "is-shared-array-buffer@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - checksum: 9508929cf14fdc1afc9d61d723c6e8d34f5e117f0bffda4d97e7a5d88c3a8681f633a74f8e3ad1fe92d5113f9b921dc5ca44356492079612f9a247efbce7032a - languageName: node - linkType: hard - "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -8744,7 +8623,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.7": +"is-typed-array@npm:^1.1.3": version: 1.1.8 resolution: "is-typed-array@npm:1.1.8" dependencies: @@ -8753,7 +8632,7 @@ fsevents@~2.3.2: for-each: ^0.3.3 gopd: ^1.0.1 has-tostringtag: ^1.0.0 - checksum: aac6ecb59d4c56a1cdeb69b1f129154ef462bbffe434cb8a8235ca89b42f258b7ae94073c41b3cb7bce37f6a1733ad4499f07882d5d5093a7ba84dfc4ebb8017 + checksum: aa0f9f0716e19e2fb8aef69e69e4205479d25ace778e2339fc910948115cde4b0d9aff9d5d1e8b80f09a5664998278e05e54ad3dc9cb12cefcf86db71084ed00 languageName: node linkType: hard @@ -8787,15 +8666,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"is-weakref@npm:^1.0.2": - version: 1.0.2 - resolution: "is-weakref@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - checksum: 95bd9a57cdcb58c63b1c401c60a474b0f45b94719c30f548c891860f051bc2231575c290a6b420c6bc6e7ed99459d424c652bd5bf9a1d5259505dc35b4bf83de - languageName: node - linkType: hard - "is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" @@ -10958,13 +10828,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"object-inspect@npm:^1.12.3": - version: 1.12.3 - resolution: "object-inspect@npm:1.12.3" - checksum: dabfd824d97a5f407e6d5d24810d888859f6be394d8b733a77442b277e0808860555176719c5905e765e3743a7cada6b8b0a3b85e5331c530fd418cc8ae991db - languageName: node - linkType: hard - "object-is@npm:^1.0.1": version: 1.1.5 resolution: "object-is@npm:1.1.5" @@ -11001,18 +10864,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"object.assign@npm:^4.1.4": - version: 4.1.4 - resolution: "object.assign@npm:4.1.4" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - has-symbols: ^1.0.3 - object-keys: ^1.1.1 - checksum: 76cab513a5999acbfe0ff355f15a6a125e71805fcf53de4e9d4e082e1989bdb81d1e329291e1e4e0ae7719f0e4ef80e88fb2d367ae60500d79d25a6224ac8864 - languageName: node - linkType: hard - "object.entries@npm:^1.1.2": version: 1.1.5 resolution: "object.entries@npm:1.1.5" @@ -11020,7 +10871,7 @@ fsevents@~2.3.2: call-bind: ^1.0.2 define-properties: ^1.1.4 es-abstract: ^1.20.4 - checksum: 0f8c47517e6a9a980241eafe3b73de11e59511883173c2b93d67424a008e47e11b77c80e431ad1d8a806f6108b225a1cab9223e53e555776c612a24297117d28 + checksum: d658696f74fd222060d8428d2a9fda2ce736b700cb06f6bdf4a16a1892d145afb746f453502b2fa55d1dca8ead6f14ddbcf66c545df45adadea757a6c4cd86c7 languageName: node linkType: hard @@ -12442,17 +12293,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"safe-regex2@npm:^2.0.0": - version: 2.0.0 - resolution: "safe-regex2@npm:2.0.0" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.1.3 - is-regex: ^1.1.4 - checksum: bc566d8beb8b43c01b94e67de3f070fd2781685e835959bbbaaec91cc53381145ca91f69bd837ce6ec244817afa0a5e974fc4e40a2957f0aca68ac3add1ddd34 - languageName: node - linkType: hard - "safe-stable-stringify@npm:^1.1.0": version: 1.1.1 resolution: "safe-stable-stringify@npm:1.1.1" @@ -13161,20 +13001,20 @@ fsevents@~2.3.2: languageName: node linkType: hard -"string.prototype.trim@npm:^1.2.7": - version: 1.2.7 - resolution: "string.prototype.trim@npm:1.2.7" +"string-width@npm:^5.0.1": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 05b7b2d6af63648e70e44c4a8d10d8cc457536df78b55b9d6230918bde75c5987f6b8604438c4c8652eb55e4fc9725d2912789eb4ec457d6995f3495af190c09 + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.4": - version: 1.0.4 - resolution: "string.prototype.trimend@npm:1.0.4" +"string.prototype.trim@npm:^1.2.7": + version: 1.2.7 + resolution: "string.prototype.trim@npm:1.2.7" dependencies: call-bind: ^1.0.2 define-properties: ^1.1.4 @@ -13194,28 +13034,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"string.prototype.trimstart@npm:^1.0.4": - version: 1.0.4 - resolution: "string.prototype.trimstart@npm:1.0.4" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 0fdc34645a639bd35179b5a08227a353b88dc089adf438f46be8a7c197fc3f22f8514c1c9be4629b3cd29c281582730a8cbbad6466c60f76b5f99cf2addb132e - languageName: node - linkType: hard - -"string.prototype.trimstart@npm:^1.0.6": - version: 1.0.6 - resolution: "string.prototype.trimstart@npm:1.0.6" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 89080feef416621e6ef1279588994305477a7a91648d9436490d56010a1f7adc39167cddac7ce0b9884b8cdbef086987c4dcb2960209f2af8bac0d23ceff4f41 - languageName: node - linkType: hard - "string.prototype.trimstart@npm:^1.0.6": version: 1.0.6 resolution: "string.prototype.trimstart@npm:1.0.6" @@ -14096,18 +13914,6 @@ typescript@^3.9.5: languageName: node linkType: hard -"unbox-primitive@npm:^1.0.2": - version: 1.0.2 - resolution: "unbox-primitive@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - has-bigints: ^1.0.2 - has-symbols: ^1.0.3 - which-boxed-primitive: ^1.0.2 - checksum: b7a1cf5862b5e4b5deb091672ffa579aa274f648410009c81cca63fed3b62b610c4f3b773f912ce545bb4e31edc3138975b5bc777fc6e4817dca51affb6380e9 - languageName: node - linkType: hard - "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" @@ -14578,20 +14384,6 @@ typescript@^3.9.5: languageName: node linkType: hard -"which-typed-array@npm:^1.1.9": - version: 1.1.9 - resolution: "which-typed-array@npm:1.1.9" - dependencies: - available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 - for-each: ^0.3.3 - gopd: ^1.0.1 - has-tostringtag: ^1.0.0 - is-typed-array: ^1.1.10 - checksum: fe0178ca44c57699ca2c0e657b64eaa8d2db2372a4e2851184f568f98c478ae3dc3fdb5f7e46c384487046b0cf9e23241423242b277e03e8ba3dabc7c84c98ef - languageName: node - linkType: hard - "which@npm:2.0.2, which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2"