diff --git a/lib/DashPlatformProtocol.js b/lib/DashPlatformProtocol.js index 09e639479..5c22a9b13 100644 --- a/lib/DashPlatformProtocol.js +++ b/lib/DashPlatformProtocol.js @@ -48,6 +48,7 @@ class DashPlatformProtocol { this.identity = new IdentityFacade( this.jsonSchemaValidator, + this.dataProvider, ); } diff --git a/lib/errors/InvalidIdentityLockTransactionError.js b/lib/errors/InvalidIdentityLockTransactionError.js deleted file mode 100644 index 0e23953f2..000000000 --- a/lib/errors/InvalidIdentityLockTransactionError.js +++ /dev/null @@ -1,24 +0,0 @@ -const ConsensusError = require('./ConsensusError'); - -class InvalidIdentityLockTransactionError extends ConsensusError { - /** - * @param {string} message - * @param {Transaction} transaction - */ - constructor(message, transaction) { - super(`Invalid identity lock transaction: ${message}`); - - this.transaction = transaction; - } - - /** - * Get lock transaction - * - * @return {Transaction} - */ - getTransaction() { - return this.transaction; - } -} - -module.exports = InvalidIdentityLockTransactionError; diff --git a/lib/errors/InvalidIdentityLockTransactionOutputError.js b/lib/errors/InvalidIdentityLockTransactionOutputError.js new file mode 100644 index 000000000..1ba324ed8 --- /dev/null +++ b/lib/errors/InvalidIdentityLockTransactionOutputError.js @@ -0,0 +1,24 @@ +const ConsensusError = require('./ConsensusError'); + +class InvalidIdentityLockTransactionOutputError extends ConsensusError { + /** + * @param {string} message + * @param {Object} output + */ + constructor(message, output) { + super(`Invalid identity lock transaction output: ${message}`); + + this.output = output; + } + + /** + * Get lock transaction output + * + * @return {Object} + */ + getOutput() { + return this.output; + } +} + +module.exports = InvalidIdentityLockTransactionOutputError; diff --git a/lib/identity/Identity.js b/lib/identity/Identity.js index 428a86b65..435037dd6 100644 --- a/lib/identity/Identity.js +++ b/lib/identity/Identity.js @@ -90,6 +90,18 @@ class Identity { getBalance() { return this.balance; } + + /** + * Set Identity balance + * + * @param {number} balance + * @return {Identity} + */ + setBalance(balance) { + this.balance = balance; + + return this; + } } module.exports = Identity; diff --git a/lib/identity/IdentityFacade.js b/lib/identity/IdentityFacade.js index db9e93bce..2963c3c23 100644 --- a/lib/identity/IdentityFacade.js +++ b/lib/identity/IdentityFacade.js @@ -1,6 +1,8 @@ +const { Transaction } = require('@dashevo/dashcore-lib'); const IdentityFactory = require('./IdentityFactory'); const validateIdentityFactory = require('./validation/validateIdentityFactory'); -const applyIdentityStateTransition = require('./stateTransitions/applyIdentityStateTransition'); +const applyIdentityStateTransitionFactory = require('./stateTransitions/applyIdentityStateTransitionFactory'); +const getLockedTransactionOutputFactory = require('../stateTransition/getLockedTransactionOutputFactory'); const validatePublicKeysFactory = require('./validation/validatePublicKeysFactory'); /** @@ -9,9 +11,10 @@ const validatePublicKeysFactory = require('./validation/validatePublicKeysFactor */ class IdentityFacade { /** + * @param {DataProvider} dataProvider * @param {JsonSchemaValidator} validator */ - constructor(validator) { + constructor(validator, dataProvider) { const validatePublicKeys = validatePublicKeysFactory( validator, ); @@ -20,7 +23,15 @@ class IdentityFacade { validatePublicKeys, ); this.factory = new IdentityFactory(this.validateIdentity); - this.applyIdentityStateTransition = applyIdentityStateTransition; + + const getLockedTransactionOutput = getLockedTransactionOutputFactory( + dataProvider, + Transaction.parseOutPointBuffer, + ); + + this.applyIdentityStateTransition = applyIdentityStateTransitionFactory( + getLockedTransactionOutput, + ); } /** @@ -65,7 +76,7 @@ class IdentityFacade { * @param {Identity|null} identity * @return {Identity|null} */ - applyStateTransition(stateTransition, identity) { + async applyStateTransition(stateTransition, identity) { return this.applyIdentityStateTransition(stateTransition, identity); } diff --git a/lib/identity/stateTransitions/applyIdentityStateTransition.js b/lib/identity/stateTransitions/applyIdentityStateTransition.js deleted file mode 100644 index 430cd323b..000000000 --- a/lib/identity/stateTransitions/applyIdentityStateTransition.js +++ /dev/null @@ -1,37 +0,0 @@ -const Identity = require('../Identity'); - -const stateTransitionTypes = require('../../stateTransition/stateTransitionTypes'); - -const WrongStateTransitionTypeError = require('../errors/WrongStateTransitionTypeError'); -const IdentityAlreadyExistsError = require('../../errors/IdentityAlreadyExistsError'); - -/** - * Applies a state transition to the identity model. - * Only identity state transitions are allowed - * - * @param {IdentityCreateTransition} stateTransition - * @param {Identity|null} identity - * @return {Identity|null} - */ -function applyIdentityStateTransition(stateTransition, identity) { - // noinspection JSRedundantSwitchStatement - switch (stateTransition.getType()) { - case stateTransitionTypes.IDENTITY_CREATE: { - if (identity) { - throw new IdentityAlreadyExistsError(stateTransition); - } - - const newIdentity = new Identity({ - id: stateTransition.getIdentityId(), - }); - - newIdentity.setPublicKeys(stateTransition.getPublicKeys()); - - return newIdentity; - } - default: - throw new WrongStateTransitionTypeError(stateTransition); - } -} - -module.exports = applyIdentityStateTransition; diff --git a/lib/identity/stateTransitions/applyIdentityStateTransitionFactory.js b/lib/identity/stateTransitions/applyIdentityStateTransitionFactory.js new file mode 100644 index 000000000..f913a9710 --- /dev/null +++ b/lib/identity/stateTransitions/applyIdentityStateTransitionFactory.js @@ -0,0 +1,56 @@ +const Identity = require('../Identity'); + +const stateTransitionTypes = require('../../stateTransition/stateTransitionTypes'); + +const WrongStateTransitionTypeError = require('../errors/WrongStateTransitionTypeError'); +const IdentityAlreadyExistsError = require('../../errors/IdentityAlreadyExistsError'); + +const { convertSatoshiToCredits } = require('../creditsConverter'); +const calculateStateTransitionFee = require('../../stateTransition/calculateStateTransitionFee'); + +/** + * + * @param {getLockedTransactionOutput} getLockedTransactionOutput + * @return {applyIdentityStateTransition} + */ +function applyIdentityStateTransitionFactory( + getLockedTransactionOutput, +) { + /** + * Applies a state transition to the identity model. + * Only identity state transitions are allowed + * + * @typedef applyIdentityStateTransition + * @param {IdentityCreateTransition} stateTransition + * @param {Identity|null} identity + * @return {Identity|null} + */ + async function applyIdentityStateTransition(stateTransition, identity) { + // noinspection JSRedundantSwitchStatement + switch (stateTransition.getType()) { + case stateTransitionTypes.IDENTITY_CREATE: { + if (identity) { + throw new IdentityAlreadyExistsError(stateTransition); + } + + const fee = calculateStateTransitionFee(stateTransition); + const output = await getLockedTransactionOutput(stateTransition.getLockedOutPoint()); + const creditsAmount = convertSatoshiToCredits(output.satoshi); + + const balance = creditsAmount - fee; + + return new Identity({ + id: stateTransition.getIdentityId(), + publicKeys: stateTransition.getPublicKeys().map((key) => key.toJSON()), + balance, + }); + } + default: + throw new WrongStateTransitionTypeError(stateTransition); + } + } + + return applyIdentityStateTransition; +} + +module.exports = applyIdentityStateTransitionFactory; diff --git a/lib/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.js b/lib/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.js index 7255564e7..211cc8d99 100644 --- a/lib/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.js +++ b/lib/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.js @@ -1,37 +1,34 @@ -const { Transaction, Signer: { verifyHashSignature } } = require('@dashevo/dashcore-lib'); -const WrongOutPointError = require('@dashevo/dashcore-lib/lib/errors/WrongOutPointError'); +const { Signer: { verifyHashSignature } } = require('@dashevo/dashcore-lib'); const ValidationResult = require('../../../validation/ValidationResult'); -const InvalidIdentityLockTransactionError = require('../../../errors/InvalidIdentityLockTransactionError'); +const ConsensusError = require('../../../errors/ConsensusError'); +const InvalidIdentityLockTransactionOutputError = require('../../../errors/InvalidIdentityLockTransactionOutputError'); const InvalidStateTransitionSignatureError = require('../../../errors/InvalidStateTransitionSignatureError'); -const IdentityLockTransactionNotFoundError = require('../../../errors/IdentityLockTransactionNotFoundError'); -const InvalidIdentityOutPointError = require('../../../errors/InvalidIdentityOutPointError'); /** * - * @param {DataProvider} dataProvider - * @param {function} parseTransactionOutPointBuffer + * @param {getLockedTransactionOutput} getLockedTransactionOutput * @return {validateLockTransaction} */ -function validateLockTransactionFactory(dataProvider, parseTransactionOutPointBuffer) { +function validateLockTransactionFactory(getLockedTransactionOutput) { /** + * Validates identityCreateTransition signature against lock transaction + * * @typedef validateLockTransaction * @param {IdentityCreateTransition} identityCreateTransition * @returns {Promise} */ async function validateLockTransaction(identityCreateTransition) { - const result = new ValidationResult(); - let transactionHash; - let outputIndex; + // fetch lock transaction output, extract pubkey from it and verify signature - // fetch lock transaction, extract pubkey from it and verify signature + const result = new ValidationResult(); - const lockedOutBuffer = Buffer.from(identityCreateTransition.getLockedOutPoint(), 'base64'); + let output; try { - ({ transactionHash, outputIndex } = parseTransactionOutPointBuffer(lockedOutBuffer)); + output = await getLockedTransactionOutput(identityCreateTransition.getLockedOutPoint()); } catch (e) { - if (e instanceof WrongOutPointError) { - result.addError(new InvalidIdentityOutPointError(e.message)); + if (e instanceof ConsensusError) { + result.addError(e); } else { throw e; } @@ -41,36 +38,16 @@ function validateLockTransactionFactory(dataProvider, parseTransactionOutPointBu return result; } - const rawTransaction = await dataProvider.fetchTransaction(transactionHash); - - if (!rawTransaction) { - result.addError(new IdentityLockTransactionNotFoundError(transactionHash)); - } - - if (!result.isValid()) { - return result; - } - - const transaction = new Transaction(rawTransaction); - - if (!transaction.outputs[outputIndex]) { - result.addError(new InvalidIdentityOutPointError(`Output with index ${outputIndex} not found`)); - } - - if (!result.isValid()) { - return result; - } - - const { script } = transaction.outputs[outputIndex]; + const { script } = output; if (!script.isDataOut()) { - result.addError(new InvalidIdentityLockTransactionError('Output is not a valid standard OP_RETURN output', transaction)); + result.addError(new InvalidIdentityLockTransactionOutputError('Output is not a valid standard OP_RETURN output', output)); } const publicKeyHash = script.getData(); if (publicKeyHash.length !== 20) { - result.addError(new InvalidIdentityLockTransactionError('Output has invalid public key hash', transaction)); + result.addError(new InvalidIdentityLockTransactionOutputError('Output has invalid public key hash', output)); } if (!result.isValid()) { @@ -93,10 +70,6 @@ function validateLockTransactionFactory(dataProvider, parseTransactionOutPointBu result.addError(new InvalidStateTransitionSignatureError(identityCreateTransition)); } - if (result.isValid()) { - result.setData(transaction.outputs[outputIndex]); - } - return result; } diff --git a/lib/stateTransition/StateTransitionFacade.js b/lib/stateTransition/StateTransitionFacade.js index cc69673bc..bba110ec8 100644 --- a/lib/stateTransition/StateTransitionFacade.js +++ b/lib/stateTransition/StateTransitionFacade.js @@ -25,6 +25,7 @@ const validateLockTransactionFactory = require('../identity/stateTransitions/ide const validateIdentityCreateSTStructureFactory = require('../identity/stateTransitions/identityCreateTransition/validateIdentityCreateSTStructureFactory'); const validateStateTransitionSignatureFactory = require('../stateTransition/validation/validateStateTransitionSignatureFactory'); const validateStateTransitionFeeFactory = require('./validation/validateStateTransitionFeeFactory'); +const getLockedTransactionOutputFactory = require('./getLockedTransactionOutputFactory'); const enrichDataContractWithBaseSchema = require('../dataContract/enrichDataContractWithBaseSchema'); const findDuplicatesById = require('../document/stateTransition/validation/structure/findDuplicatesById'); @@ -115,11 +116,15 @@ class StateTransitionFacade { dataProvider, ); - const validateLockTransaction = validateLockTransactionFactory( + const getLockedTransactionOutput = getLockedTransactionOutputFactory( dataProvider, Transaction.parseOutPointBuffer, ); + const validateLockTransaction = validateLockTransactionFactory( + getLockedTransactionOutput, + ); + const validateIdentityCreateSTData = validateIdentityCreateSTDataFactory( dataProvider, validateLockTransaction, @@ -154,7 +159,7 @@ class StateTransitionFacade { this.validateStateTransitionFee = validateStateTransitionFeeFactory( dataProvider, - validateLockTransaction, + getLockedTransactionOutput, ); this.factory = new StateTransitionFactory( diff --git a/lib/stateTransition/calculateStateTransitionFee.js b/lib/stateTransition/calculateStateTransitionFee.js new file mode 100644 index 000000000..3edfc61e1 --- /dev/null +++ b/lib/stateTransition/calculateStateTransitionFee.js @@ -0,0 +1,18 @@ +const PRICE_PER_BYTE = 1; + +/** + * Get State Transition fee size + * + * @typedef calculateStateTransitionFee + * @param { DataContractStateTransition| + * DocumentsBatchTransition| + * IdentityCreateTransition} stateTransition + * @return {number} + */ +function calculateStateTransitionFee(stateTransition) { + const serializedStateTransition = stateTransition.serialize({ skipSignature: true }); + const byteSize = Buffer.byteLength(serializedStateTransition); + return byteSize * PRICE_PER_BYTE; +} + +module.exports = calculateStateTransitionFee; diff --git a/lib/stateTransition/getLockedTransactionOutputFactory.js b/lib/stateTransition/getLockedTransactionOutputFactory.js new file mode 100644 index 000000000..a7e08d703 --- /dev/null +++ b/lib/stateTransition/getLockedTransactionOutputFactory.js @@ -0,0 +1,58 @@ +const { Transaction } = require('@dashevo/dashcore-lib'); +const WrongOutPointError = require('@dashevo/dashcore-lib/lib/errors/WrongOutPointError'); + +const IdentityLockTransactionNotFoundError = require('../errors/IdentityLockTransactionNotFoundError'); +const InvalidIdentityOutPointError = require('../errors/InvalidIdentityOutPointError'); + +/** + * + * @param {DataProvider} dataProvider + * @param {function} parseTransactionOutPointBuffer + * @return {getLockedTransactionOutput} + */ +function getLockedTransactionOutputFactory( + dataProvider, + parseTransactionOutPointBuffer, +) { + /** + * Returns lock transaction output for provided lockedOutPoint + * + * @typedef getLockedTransactionOutput + * @param {string} lockedOutPoint + * @return {Promise} + */ + async function getLockedTransactionOutput(lockedOutPoint) { + let transactionHash; + let outputIndex; + + const lockedOutBuffer = Buffer.from(lockedOutPoint, 'base64'); + + try { + ({ transactionHash, outputIndex } = parseTransactionOutPointBuffer(lockedOutBuffer)); + } catch (e) { + if (e instanceof WrongOutPointError) { + throw new InvalidIdentityOutPointError(e.message); + } else { + throw e; + } + } + + const rawTransaction = await dataProvider.fetchTransaction(transactionHash); + + if (!rawTransaction) { + throw new IdentityLockTransactionNotFoundError(transactionHash); + } + + const transaction = new Transaction(rawTransaction); + + if (!transaction.outputs[outputIndex]) { + throw new InvalidIdentityOutPointError(`Output with index ${outputIndex} not found`); + } + + return transaction.outputs[outputIndex]; + } + + return getLockedTransactionOutput; +} + +module.exports = getLockedTransactionOutputFactory; diff --git a/lib/stateTransition/validation/validateStateTransitionFeeFactory.js b/lib/stateTransition/validation/validateStateTransitionFeeFactory.js index c78acc1af..dc23d3c29 100644 --- a/lib/stateTransition/validation/validateStateTransitionFeeFactory.js +++ b/lib/stateTransition/validation/validateStateTransitionFeeFactory.js @@ -1,21 +1,23 @@ const ValidationResult = require('../../validation/ValidationResult'); -const stateTransitionTypes = require('../../stateTransition/stateTransitionTypes'); + const InvalidStateTransitionTypeError = require('../../errors/InvalidStateTransitionTypeError'); const BalanceIsNotEnoughError = require('../../errors/BalanceIsNotEnoughError'); -const { convertSatoshiToCredits } = require('../../identity/creditsConverter'); +const ConsensusError = require('../../../lib/errors/ConsensusError'); -const PRICE_PER_BYTE = 1; +const stateTransitionTypes = require('../../stateTransition/stateTransitionTypes'); +const { convertSatoshiToCredits } = require('../../identity/creditsConverter'); +const calculateStateTransitionFee = require('../calculateStateTransitionFee'); /** * Validate state transition fee * * @param {DataProvider} dataProvider - * @param {validateLockTransaction} validateLockTransaction + * @param {getLockedTransactionOutput} getLockedTransactionOutput * @return {validateStateTransitionFee} */ function validateStateTransitionFeeFactory( dataProvider, - validateLockTransaction, + getLockedTransactionOutput, ) { /** * @typedef validateStateTransitionFee @@ -28,24 +30,27 @@ function validateStateTransitionFeeFactory( async function validateStateTransitionFee(stateTransition) { const result = new ValidationResult(); - const serializedStateTransition = stateTransition.serialize({ skipSignature: true }); - const byteSize = Buffer.byteLength(serializedStateTransition); - const feeSize = byteSize * PRICE_PER_BYTE; + const feeSize = calculateStateTransitionFee(stateTransition); let balance; switch (stateTransition.getType()) { case stateTransitionTypes.IDENTITY_CREATE: { - const validateLockTransactionResult = await validateLockTransaction(stateTransition); - - if (!validateLockTransactionResult.isValid()) { - result.merge(validateLockTransactionResult); + let output; + try { + output = await getLockedTransactionOutput(stateTransition.getLockedOutPoint()); + } catch (e) { + if (e instanceof ConsensusError) { + result.addError(e); + } else { + throw e; + } + } + if (!result.isValid()) { return result; } - const output = validateLockTransactionResult.getData(); - balance = convertSatoshiToCredits(output.satoshis); break; diff --git a/test/integration/identity/IdentityFacade.spec.js b/test/integration/identity/IdentityFacade.spec.js index cfa35979a..a3ee6cd3a 100644 --- a/test/integration/identity/IdentityFacade.spec.js +++ b/test/integration/identity/IdentityFacade.spec.js @@ -7,12 +7,22 @@ const ValidationResult = require('../../../lib/validation/ValidationResult'); const getIdentityFixture = require('../../../lib/test/fixtures/getIdentityFixture'); const getIdentityCreateSTFixture = require('../../../lib/test/fixtures/getIdentityCreateSTFixture'); +const createDataProviderMock = require('../../../lib/test/mocks/createDataProviderMock'); + describe('IdentityFacade', () => { let dpp; let identity; + let dataProviderMock; + + beforeEach(function beforeEach() { + const rawTransaction = '030000000137feb5676d0851337ea3c9a992496aab7a0b3eee60aeeb9774000b7f4bababa5000000006b483045022100d91557de37645c641b948c6cd03b4ae3791a63a650db3e2fee1dcf5185d1b10402200e8bd410bf516ca61715867666d31e44495428ce5c1090bf2294a829ebcfa4ef0121025c3cc7fbfc52f710c941497fd01876c189171ea227458f501afcb38a297d65b4ffffffff021027000000000000166a14152073ca2300a86b510fa2f123d3ea7da3af68dcf77cb0090a0000001976a914152073ca2300a86b510fa2f123d3ea7da3af68dc88ac00000000'; - beforeEach(() => { - dpp = new DashPlatformProtocol(); + dataProviderMock = createDataProviderMock(this.sinonSandbox); + dataProviderMock.fetchTransaction.resolves(rawTransaction); + + dpp = new DashPlatformProtocol({ + dataProvider: dataProviderMock, + }); identity = getIdentityFixture(); }); @@ -62,9 +72,9 @@ describe('IdentityFacade', () => { }); describe('#applyStateTransition', () => { - it('should apply identity create transition', () => { + it('should apply identity create transition', async () => { const createStateTransition = getIdentityCreateSTFixture(); - const result = dpp.identity.applyStateTransition(createStateTransition, null); + const result = await dpp.identity.applyStateTransition(createStateTransition, null); expect(result).to.be.an.instanceOf(Identity); }); diff --git a/test/unit/identity/Identity.spec.js b/test/unit/identity/Identity.spec.js index e7b9e8228..1a4f6a121 100644 --- a/test/unit/identity/Identity.spec.js +++ b/test/unit/identity/Identity.spec.js @@ -133,4 +133,11 @@ describe('Identity', () => { expect(identity.getBalance()).to.equal(42); }); }); + + describe('#setBalance', () => { + it('should set identity balance', () => { + identity.setBalance(42); + expect(identity.balance).to.equal(42); + }); + }); }); diff --git a/test/unit/identity/stateTransitions/applyIdentityStateTransition.spec.js b/test/unit/identity/stateTransitions/applyIdentityStateTransition.spec.js deleted file mode 100644 index ccc2d4ffc..000000000 --- a/test/unit/identity/stateTransitions/applyIdentityStateTransition.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -const Identity = require('../../../../lib/identity/Identity'); - -const applyIdentityStateTransition = require('../../../../lib/identity/stateTransitions/applyIdentityStateTransition'); - -const getIdentityCreateSTFixture = require('../../../../lib/test/fixtures/getIdentityCreateSTFixture'); - -const IdentityAlreadyExistsError = require('../../../../lib/errors/IdentityAlreadyExistsError'); -const WrongStateTransitionTypeError = require('../../../../lib/identity/errors/WrongStateTransitionTypeError'); - -describe('applyIdentityStateTransition', () => { - describe('Identity Create', () => { - let createStateTransition; - - beforeEach(() => { - createStateTransition = getIdentityCreateSTFixture(); - }); - - it('should throw an error if identity is already present', () => { - const identity = new Identity(); - - try { - applyIdentityStateTransition(createStateTransition, identity); - - expect.fail('error was not thrown'); - } catch (e) { - expect(e).to.be.an.instanceOf(IdentityAlreadyExistsError); - expect(e.getStateTransition()).to.equal(createStateTransition); - } - }); - - it('should set proper data from state transition', () => { - const identity = applyIdentityStateTransition(createStateTransition, null); - - expect(identity.getId()).to.equal(createStateTransition.getIdentityId()); - expect(identity.getPublicKeys()).to.deep.equal(createStateTransition.getPublicKeys()); - }); - }); - - it('should throw an error if state transition is of wrong type', function it() { - const createStateTransition = getIdentityCreateSTFixture(); - this.sinonSandbox.stub(createStateTransition, 'getType').returns(42); - - try { - applyIdentityStateTransition(createStateTransition, null); - - expect.fail('error was not thrown'); - } catch (e) { - expect(e).to.be.an.instanceOf(WrongStateTransitionTypeError); - expect(e.getStateTransition()).to.equal(createStateTransition); - } - }); -}); diff --git a/test/unit/identity/stateTransitions/applyIdentityStateTransitionFactory.spec.js b/test/unit/identity/stateTransitions/applyIdentityStateTransitionFactory.spec.js new file mode 100644 index 000000000..b27d1e4df --- /dev/null +++ b/test/unit/identity/stateTransitions/applyIdentityStateTransitionFactory.spec.js @@ -0,0 +1,73 @@ +const Identity = require('../../../../lib/identity/Identity'); + +const applyIdentityStateTransitionFactory = require('../../../../lib/identity/stateTransitions/applyIdentityStateTransitionFactory'); + +const getIdentityCreateSTFixture = require('../../../../lib/test/fixtures/getIdentityCreateSTFixture'); + +const IdentityAlreadyExistsError = require('../../../../lib/errors/IdentityAlreadyExistsError'); +const WrongStateTransitionTypeError = require('../../../../lib/identity/errors/WrongStateTransitionTypeError'); + +const { convertSatoshiToCredits } = require('../../../../lib/identity/creditsConverter'); +const calculateStateTransitionFee = require('../../../../lib/stateTransition/calculateStateTransitionFee'); + +describe('applyIdentityStateTransitionFactory', () => { + let createStateTransition; + let applyIdentityStateTransition; + let getLockedTransactionOutputMock; + let output; + + beforeEach(function beforeEach() { + output = { + satoshi: 10000, + }; + + getLockedTransactionOutputMock = this.sinonSandbox.stub().resolves(output); + + createStateTransition = getIdentityCreateSTFixture(); + applyIdentityStateTransition = applyIdentityStateTransitionFactory( + getLockedTransactionOutputMock, + ); + }); + + describe('Identity Create', () => { + it('should throw an error if identity is already present', async () => { + const identity = new Identity(); + + try { + await applyIdentityStateTransition(createStateTransition, identity); + + expect.fail('error was not thrown'); + } catch (e) { + expect(e).to.be.an.instanceOf(IdentityAlreadyExistsError); + expect(e.getStateTransition()).to.equal(createStateTransition); + } + }); + + it('should set proper data from state transition', async () => { + const identity = await applyIdentityStateTransition(createStateTransition, null); + + const balance = convertSatoshiToCredits(output.satoshi) + - calculateStateTransitionFee(createStateTransition); + + expect(getLockedTransactionOutputMock).to.be.calledOnceWithExactly( + createStateTransition.getLockedOutPoint(), + ); + expect(identity.getId()).to.equal(createStateTransition.getIdentityId()); + expect(identity.getPublicKeys()).to.deep.equal(createStateTransition.getPublicKeys()); + expect(identity.getBalance()).to.equal(balance); + }); + }); + + it('should throw an error if state transition is of wrong type', async function it() { + this.sinonSandbox.stub(createStateTransition, 'getType').returns(42); + + try { + await applyIdentityStateTransition(createStateTransition, null); + + expect.fail('error was not thrown'); + } catch (e) { + expect(e).to.be.an.instanceOf(WrongStateTransitionTypeError); + expect(e.getStateTransition()).to.equal(createStateTransition); + } + }); +}); diff --git a/test/unit/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.spec.js b/test/unit/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.spec.js index e56c2ab13..a1348f06a 100644 --- a/test/unit/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.spec.js +++ b/test/unit/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory.spec.js @@ -1,41 +1,21 @@ -const { Transaction } = require('@dashevo/dashcore-lib'); - -const WrongOutPointError = require('@dashevo/dashcore-lib/lib/errors/WrongOutPointError'); - const validateLockTransactionFactory = require('../../../../../lib/identity/stateTransitions/identityCreateTransition/validateLockTransactionFactory'); const IdentityCreateTransition = require('../../../../../lib/identity/stateTransitions/identityCreateTransition/IdentityCreateTransition'); const stateTransitionTypes = require('../../../../../lib/stateTransition/stateTransitionTypes'); -const { expectValidationError } = require( - '../../../../../lib/test/expect/expectError', -); - -const createDataProviderMock = require('../../../../../lib/test/mocks/createDataProviderMock'); - -const InvalidIdentityOutPointError = require( - '../../../../../lib/errors/InvalidIdentityOutPointError', -); - -const InvalidIdentityLockTransactionError = require('../../../../../lib/errors/InvalidIdentityLockTransactionError'); - -const IdentityLockTransactionNotFoundError = require( - '../../../../../lib/errors/IdentityLockTransactionNotFoundError', -); - +const InvalidIdentityLockTransactionOutputError = require('../../../../../lib/errors/InvalidIdentityLockTransactionOutputError'); const InvalidStateTransitionSignatureError = require( '../../../../../lib/errors/InvalidStateTransitionSignatureError', ); +const { expectValidationError } = require( + '../../../../../lib/test/expect/expectError', +); describe('validateLockTransactionFactory', () => { - let transactionHash; let validateLockTransaction; - let dataProviderMock; let stateTransition; - let parseOutPointBufferMock; - let lockedOutPointBuffer; - let outputIndex; let privateKey; - let rawTransaction; + let getLockedTransactionOutputMock; + let output; beforeEach(function beforeEach() { privateKey = 'af432c476f65211f45f48f1d42c9c0b497e56696aa1736b40544ef1a496af837'; @@ -55,24 +35,21 @@ describe('validateLockTransactionFactory', () => { }); stateTransition.signByPrivateKey(privateKey); - lockedOutPointBuffer = Buffer.from(stateTransition.getLockedOutPoint(), 'base64'); - - rawTransaction = '030000000137feb5676d0851337ea3c9a992496aab7a0b3eee60aeeb9774000b7f4bababa5000000006b483045022100d91557de37645c641b948c6cd03b4ae3791a63a650db3e2fee1dcf5185d1b10402200e8bd410bf516ca61715867666d31e44495428ce5c1090bf2294a829ebcfa4ef0121025c3cc7fbfc52f710c941497fd01876c189171ea227458f501afcb38a297d65b4ffffffff021027000000000000166a14152073ca2300a86b510fa2f123d3ea7da3af68dcf77cb0090a0000001976a914152073ca2300a86b510fa2f123d3ea7da3af68dc88ac00000000'; + const script = { + isDataOut: this.sinonSandbox.stub() + .returns(true), + getData: this.sinonSandbox.stub() + .returns(Buffer.from('152073ca2300a86b510fa2f123d3ea7da3af68dc', 'hex')), + }; - dataProviderMock = createDataProviderMock(this.sinonSandbox); - dataProviderMock.fetchTransaction.resolves(rawTransaction); + output = { + script, + }; - transactionHash = 'hash'; - outputIndex = 0; - - parseOutPointBufferMock = this.sinonSandbox.stub().returns({ - transactionHash, - outputIndex, - }); + getLockedTransactionOutputMock = this.sinonSandbox.stub().resolves(output); validateLockTransaction = validateLockTransactionFactory( - dataProviderMock, - parseOutPointBufferMock, + getLockedTransactionOutputMock, ); }); @@ -81,122 +58,48 @@ describe('validateLockTransactionFactory', () => { expect(result.isValid()).to.be.true(); - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); - const output = new Transaction(rawTransaction).outputs[outputIndex]; - - // Dirty hack - // after we use "script" getter inside of validateLockTransaction, - // there will be a _script property, and the objects will be different - // so we need to call this getter here, to make the objects equal - - // eslint-disable-next-line no-unused-vars - const { script } = output; - - expect(result.getData()).to.deep.equal(output); - }); - - it('should return invalid result if state transition has wrong out point', async () => { - const wrongOutPointError = new WrongOutPointError('Outpoint is wrong'); - - parseOutPointBufferMock.throws(wrongOutPointError); - - const result = await validateLockTransaction(stateTransition); - - expectValidationError(result, InvalidIdentityOutPointError); - - const [error] = result.getErrors(); - - expect(error.message).to.equal(`Invalid Identity out point: ${wrongOutPointError.message}`); - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.not.called(transactionHash); - }); - - it('should return invalid result if lock transaction is not found', async () => { - dataProviderMock.fetchTransaction.resolves(null); - - const result = await validateLockTransaction(stateTransition); - - expectValidationError(result, IdentityLockTransactionNotFoundError); - - const [error] = result.getErrors(); - expect(error.getTransactionHash()).to.deep.equal(transactionHash); - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); - }); - - it('should return InvalidIdentityLockTransaction error if transaction has no output with given index', async () => { - outputIndex = 10; - - parseOutPointBufferMock.returns({ - transactionHash, - outputIndex, - }); - - const result = await validateLockTransaction(stateTransition); - - expectValidationError(result, InvalidIdentityOutPointError); - - const [error] = result.getErrors(); - - expect(error.message).to.equal(`Invalid Identity out point: Output with index ${outputIndex} not found`); - - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); + expect(getLockedTransactionOutputMock).to.be.calledOnceWithExactly( + stateTransition.getLockedOutPoint(), + ); }); - it('should return InvalidIdentityLockTransaction error if transaction output is not a valid OP_RETURN output', async () => { - outputIndex = 1; // fixture output # 1 is not an OP_RETURN output - - parseOutPointBufferMock.returns({ - transactionHash, - outputIndex, - }); + it('should check transaction output is a valid OP_RETURN output', async () => { + output.script.isDataOut.returns(false); const result = await validateLockTransaction(stateTransition); - expectValidationError(result, InvalidIdentityLockTransactionError); + expectValidationError(result, InvalidIdentityLockTransactionOutputError); const [error] = result.getErrors(); - expect(error.message).to.equal('Invalid identity lock transaction: Output is not a valid standard OP_RETURN output'); - - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); + expect(error.message).to.equal('Invalid identity lock transaction output: Output is not a valid standard OP_RETURN output'); + expect(error.getOutput()).to.deep.equal(output); }); - it('should return InvalidIdentityLockTransaction error if transaction output script data has size < 20', async () => { - rawTransaction = '0300000001ab9eafdc4318fb78f3c1d1dc6bf6e37339170810be031d0cb46cbdce6e155457000000006b483045022100832effb10710fd69399ee0d2a545eac3050e8b49f269a28fb77701411a4af90a02201f70042b2f86d3538e7b26e5995fa2fd5966ce6f5ec540e12d6e3dd5883855cb0121027a68d5e8adb9cd765166b8d7b143de26643617d5683a313960efe7a0267703d7ffffffff021027000000000000156a1300000000000000000000000000000000000000f77cb0090a0000001976a9140f25d6ad33b341e04ee91b693038e5e59d080c2688ac00000000'; - - dataProviderMock.fetchTransaction.resolves(rawTransaction); + it('should return invalid result if transaction output script data has size < 20', async () => { + output.script.getData.returns(Buffer.from('1'.repeat(19))); const result = await validateLockTransaction(stateTransition); - expectValidationError(result, InvalidIdentityLockTransactionError); + expectValidationError(result, InvalidIdentityLockTransactionOutputError); const [error] = result.getErrors(); - expect(error.message).to.equal('Invalid identity lock transaction: Output has invalid public key hash'); - - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); + expect(error.message).to.equal('Invalid identity lock transaction output: Output has invalid public key hash'); + expect(error.getOutput()).to.deep.equal(output); }); - it('should return InvalidIdentityLockTransaction error if transaction output script data has size > 20', async () => { - rawTransaction = '0300000001aa556096e53cced1a46b5fbcb5a250f4c6e85d45d844605a780b9ab03f9ad8f4000000006b483045022100d7e5bf5a77fa4dc10d0b0a90e8aba7646a24214df6d3195d86212ee7177b8d0402201bea80d1464ec70ae4484df3c81c5002d725941a91ea93db3c438b672d68f331012102a22559eb15862d37124dc205d62a6c9de4dd837aee6c0902c5e7589f723f9e88ffffffff021027000000000000176a15000000000000000000000000000000000000000000544a3ba40b0000001976a9140fdd198858cc7c4e1afdba4c83c6348173a9bd3f88ac00000000'; - - dataProviderMock.fetchTransaction.resolves(rawTransaction); + it('should return invalid result if transaction output script data has size > 20', async () => { + output.script.getData.returns(Buffer.from('1'.repeat(21))); const result = await validateLockTransaction(stateTransition); - expectValidationError(result, InvalidIdentityLockTransactionError); + expectValidationError(result, InvalidIdentityLockTransactionOutputError); const [error] = result.getErrors(); - expect(error.message).to.equal('Invalid identity lock transaction: Output has invalid public key hash'); - - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); + expect(error.message).to.equal('Invalid identity lock transaction output: Output has invalid public key hash'); + expect(error.getOutput()).to.deep.equal(output); }); it('should return invalid result if state transition has wrong signature', async () => { @@ -209,7 +112,5 @@ describe('validateLockTransactionFactory', () => { const [error] = result.getErrors(); expect(error.getRawStateTransition()).to.deep.equal(stateTransition); - expect(parseOutPointBufferMock).to.be.calledOnceWithExactly(lockedOutPointBuffer); - expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); }); }); diff --git a/test/unit/stateTransition/getLockedTransactionOutputFactory.spec.js b/test/unit/stateTransition/getLockedTransactionOutputFactory.spec.js new file mode 100644 index 000000000..82864a2ca --- /dev/null +++ b/test/unit/stateTransition/getLockedTransactionOutputFactory.spec.js @@ -0,0 +1,107 @@ +const { Transaction } = require('@dashevo/dashcore-lib'); +const WrongOutPointError = require('@dashevo/dashcore-lib/lib/errors/WrongOutPointError'); + +const getLockedTransactionOutputFactory = require('../../../lib/stateTransition/getLockedTransactionOutputFactory'); + +const createDataProviderMock = require('../../../lib/test/mocks/createDataProviderMock'); + +const IdentityLockTransactionNotFoundError = require( + '../../../lib/errors/IdentityLockTransactionNotFoundError', +); +const InvalidIdentityOutPointError = require( + '../../../lib/errors/InvalidIdentityOutPointError', +); + +describe('getLockedTransactionOutputFactory', () => { + let rawTransaction; + let transactionHash; + let outputIndex; + let dataProviderMock; + let parseTransactionOutPointBufferMock; + let getLockedTransactionOutput; + let lockedOutPoint; + + beforeEach(function beforeEach() { + rawTransaction = '030000000137feb5676d0851337ea3c9a992496aab7a0b3eee60aeeb9774000b7f4bababa5000000006b483045022100d91557de37645c641b948c6cd03b4ae3791a63a650db3e2fee1dcf5185d1b10402200e8bd410bf516ca61715867666d31e44495428ce5c1090bf2294a829ebcfa4ef0121025c3cc7fbfc52f710c941497fd01876c189171ea227458f501afcb38a297d65b4ffffffff021027000000000000166a14152073ca2300a86b510fa2f123d3ea7da3af68dcf77cb0090a0000001976a914152073ca2300a86b510fa2f123d3ea7da3af68dc88ac00000000'; + + dataProviderMock = createDataProviderMock(this.sinonSandbox); + dataProviderMock.fetchTransaction.resolves(rawTransaction); + + lockedOutPoint = 'azW1UgBiB0CmdphN6of4DbT91t0Xv3/c3YUV4CnoV/kAAAAA'; + + transactionHash = 'hash'; + outputIndex = 0; + + parseTransactionOutPointBufferMock = this.sinonSandbox.stub().returns({ + transactionHash, + outputIndex, + }); + + getLockedTransactionOutput = getLockedTransactionOutputFactory( + dataProviderMock, + parseTransactionOutPointBufferMock, + ); + }); + + it('should return lock transaction output', async () => { + const transaction = new Transaction(rawTransaction); + + const result = await getLockedTransactionOutput(lockedOutPoint); + + expect(result).to.deep.equal(transaction.outputs[outputIndex]); + expect(parseTransactionOutPointBufferMock).to.be.calledOnceWithExactly(Buffer.from(lockedOutPoint, 'base64')); + expect(dataProviderMock.fetchTransaction).to.be.calledOnceWithExactly(transactionHash); + }); + + it('should throw InvalidIdentityOutPointError if state transition has wrong out point', async () => { + const wrongOutPointError = new WrongOutPointError('Outpoint is wrong'); + + parseTransactionOutPointBufferMock.throws(wrongOutPointError); + + try { + await getLockedTransactionOutput(lockedOutPoint); + + expect.fail('should throw InvalidIdentityOutPointError'); + } catch (e) { + expect(e).to.be.an.instanceof(InvalidIdentityOutPointError); + expect(e.message).to.equal(`Invalid Identity out point: ${wrongOutPointError.message}`); + expect(parseTransactionOutPointBufferMock).to.be.calledOnceWithExactly(Buffer.from(lockedOutPoint, 'base64')); + expect(dataProviderMock.fetchTransaction).to.be.not.called(); + } + }); + + it('should throw IdentityLockTransactionNotFoundError if lock transaction is not found', async () => { + dataProviderMock.fetchTransaction.resolves(null); + + try { + await getLockedTransactionOutput(lockedOutPoint); + + expect.fail('should throw InvalidIdentityOutPointError'); + } catch (e) { + expect(e).to.be.an.instanceof(IdentityLockTransactionNotFoundError); + expect(e.getTransactionHash()).to.deep.equal(transactionHash); + expect(parseTransactionOutPointBufferMock).to.be.calledOnceWithExactly(Buffer.from(lockedOutPoint, 'base64')); + expect(dataProviderMock.fetchTransaction).to.calledOnceWithExactly(transactionHash); + } + }); + + it('should throw InvalidIdentityOutPointError if transaction has no output with given index', async () => { + outputIndex = 10; + + parseTransactionOutPointBufferMock.returns({ + transactionHash, + outputIndex, + }); + + try { + await getLockedTransactionOutput(lockedOutPoint); + + expect.fail('should throw InvalidIdentityOutPointError'); + } catch (e) { + expect(e).to.be.an.instanceof(InvalidIdentityOutPointError); + expect(e.message).to.equal(`Invalid Identity out point: Output with index ${outputIndex} not found`); + expect(parseTransactionOutPointBufferMock).to.be.calledOnceWithExactly(Buffer.from(lockedOutPoint, 'base64')); + expect(dataProviderMock.fetchTransaction).to.calledOnceWithExactly(transactionHash); + } + }); +}); diff --git a/test/unit/stateTransition/validation/validateStateTransitionFeeFactory.spec.js b/test/unit/stateTransition/validation/validateStateTransitionFeeFactory.spec.js index 449e54968..aca1cdeea 100644 --- a/test/unit/stateTransition/validation/validateStateTransitionFeeFactory.spec.js +++ b/test/unit/stateTransition/validation/validateStateTransitionFeeFactory.spec.js @@ -16,7 +16,6 @@ const { expectValidationError } = require('../../../../lib/test/expect/expectErr const IdentityBalanceIsNotEnoughError = require('../../../../lib/errors/BalanceIsNotEnoughError'); const InvalidStateTransitionTypeError = require('../../../../lib/errors/InvalidStateTransitionTypeError'); -const ValidationResult = require('../../../../lib/validation/ValidationResult'); const { RATIO } = require('../../../../lib/identity/creditsConverter'); describe('validateStateTransitionFeeFactory', () => { @@ -27,7 +26,7 @@ describe('validateStateTransitionFeeFactory', () => { let dataContract; let documents; let identityCreateST; - let validateLockTransactionMock; + let getLockedTransactionOutputMock; let output; beforeEach(function beforeEach() { @@ -39,16 +38,13 @@ describe('validateStateTransitionFeeFactory', () => { satoshis: Math.ceil(stSize / RATIO), }; - const validateLockTransactionResult = new ValidationResult(); - validateLockTransactionResult.setData(output); - - validateLockTransactionMock = this.sinonSandbox.stub().resolves(validateLockTransactionResult); + getLockedTransactionOutputMock = this.sinonSandbox.stub().resolves(output); identity = getIdentityFixture(); dataProviderMock = createDataProviderMock(this.sinonSandbox); dataProviderMock.fetchIdentity.resolves(identity); validateStateTransitionFee = validateStateTransitionFeeFactory( dataProviderMock, - validateLockTransactionMock, + getLockedTransactionOutputMock, ); dataContract = getDataContractFixture(); documents = getDocumentsFixture(); @@ -100,7 +96,9 @@ describe('validateStateTransitionFeeFactory', () => { const result = await validateStateTransitionFee(identityCreateST); expect(result.isValid()).to.be.true(); - expect(validateLockTransactionMock).to.be.calledOnceWithExactly(identityCreateST); + expect(getLockedTransactionOutputMock).to.be.calledOnceWithExactly( + identityCreateST.getLockedOutPoint(), + ); }); it('should throw InvalidStateTransitionTypeError on invalid State Transition', async function it() {