diff --git a/package.json b/package.json index 1f38e7a3f..ec9ca8d7f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "babel-polyfill": "=6.23.0", "bitcore-mnemonic": "LiskHQ/bitcore-mnemonic#v1.2.5", "browserify-bignum": "=1.3.0-2", - "bytebuffer": "=5.0.1", "ed2curve": "=0.2.1", "js-nacl": "LiskHQ/js-nacl#6dc1417", "popsicle": "=9.1.0" diff --git a/src/crypto/convert.js b/src/crypto/convert.js index d273ddfb5..c0394517b 100644 --- a/src/crypto/convert.js +++ b/src/crypto/convert.js @@ -15,7 +15,7 @@ import bignum from 'browserify-bignum'; import ed2curve from 'ed2curve'; import { getSha256Hash } from './hash'; -import { getTransactionBytes } from '../transactions/transactionBytes'; +import getTransactionBytes from '../transactions/transactionBytes'; /** * @method bufferToHex diff --git a/src/crypto/hash.js b/src/crypto/hash.js index e0d80537f..4e7406be3 100644 --- a/src/crypto/hash.js +++ b/src/crypto/hash.js @@ -12,7 +12,7 @@ * Removal or modification of this copyright notice is prohibited. * */ -import { getTransactionBytes } from './../transactions/transactionBytes'; +import getTransactionBytes from './../transactions/transactionBytes'; /** * @method getSha256Hash diff --git a/src/crypto/sign.js b/src/crypto/sign.js index b2d2e025b..0b15e97fa 100644 --- a/src/crypto/sign.js +++ b/src/crypto/sign.js @@ -13,7 +13,7 @@ * */ import crypto from 'crypto'; -import { getTransactionBytes } from '../transactions/transactionBytes'; +import getTransactionBytes from '../transactions/transactionBytes'; import { hexToBuffer, bufferToHex, diff --git a/src/transactions/transactionBytes.js b/src/transactions/transactionBytes.js index 09ef8627f..f39e7d4f1 100644 --- a/src/transactions/transactionBytes.js +++ b/src/transactions/transactionBytes.js @@ -12,323 +12,250 @@ * Removal or modification of this copyright notice is prohibited. * */ -/* eslint-disable no-plusplus */ -import ByteBuffer from 'bytebuffer'; import bignum from 'browserify-bignum'; +import { isValidValue } from './utils'; + +export const BYTESIZES = { + TYPE: 1, + TIMESTAMP: 4, + MULTISIGNATURE_PUBLICKEY: 32, + RECIPIENT_ID: 8, + AMOUNT: 8, + SIGNATURE_TRANSACTION: 64, + SECOND_SIGNATURE_TRANSACTION: 64, + DATA: 64, +}; /** - * @method getEmptyBytesForTransaction - * @param transaction Object - * @return {object} + * @method checkRequiredFields + * @param {Array} requiredFields + * @param {Object} data + * @throws + * @return {Boolean} */ -function getEmptyBytesForTransaction(transaction) { - /** - * @method isSendTransaction - * @return {object} - */ - - function isSendTransaction() { - return { - assetBytes: null, - assetSize: 0, - }; - } - - /** - * @method isSignatureTransaction - * @return {object} - */ - - function isSignatureTransaction() { - const bb = new ByteBuffer(32, true); - const publicKey = transaction.asset.signature.publicKey; - const publicKeyBuffer = Buffer.from(publicKey, 'hex'); - - for (let i = 0; i < publicKeyBuffer.length; i++) { - bb.writeByte(publicKeyBuffer[i]); - } - - bb.flip(); - const signatureBytes = new Uint8Array(bb.toArrayBuffer()); - - return { - assetBytes: signatureBytes, - assetSize: 32, - }; - } - - /** - * @method isDelegateTransaction - * @return {object} - */ - - function isDelegateTransaction() { - return { - assetBytes: Buffer.from(transaction.asset.delegate.username), - assetSize: Buffer.from(transaction.asset.delegate.username).length, - }; - } - - /** - * @method isVoteTransaction - * @return {object} - */ - - function isVoteTransaction() { - const voteTransactionBytes = (Buffer.from(transaction.asset.votes.join('')) || null); - - return { - assetBytes: voteTransactionBytes, - assetSize: (voteTransactionBytes.length || 0), - }; - } - - /** - * @method isMultisignatureTransaction - * @return {object} - */ - - function isMultisignatureTransaction() { - const MINSIGNATURES = 1; - const LIFETIME = 1; - const keysgroupBuffer = Buffer.from(transaction.asset.multisignature.keysgroup.join(''), 'utf8'); - - const bb = new ByteBuffer(MINSIGNATURES + LIFETIME + keysgroupBuffer.length, true); - bb.writeByte(transaction.asset.multisignature.min); - bb.writeByte(transaction.asset.multisignature.lifetime); - for (let i = 0; i < keysgroupBuffer.length; i++) { - bb.writeByte(keysgroupBuffer[i]); - } - bb.flip(); - - bb.toBuffer(); - const multiSigBuffer = new Uint8Array(bb.toArrayBuffer()); - - return { - assetBytes: multiSigBuffer, - assetSize: multiSigBuffer.length, - }; - } - - /** - * @method isDappTransaction - * @return {object} - */ - - function isDappTransaction() { - const dapp = transaction.asset.dapp; - let buf = Buffer.from(dapp.name); - - if (dapp.description) { - const descriptionBuf = Buffer.from(dapp.description); - buf = Buffer.concat([buf, descriptionBuf]); - } - - if (dapp.tags) { - const tagsBuf = Buffer.from(dapp.tags); - buf = Buffer.concat([buf, tagsBuf]); +export function checkRequiredFields(requiredFields, data) { + const dataFields = Object.keys(data); + requiredFields.forEach((parameter) => { + if (!dataFields.includes(parameter.toString()) || !isValidValue(data[parameter])) { + throw new Error(`${parameter} is a required parameter.`); } + }); + return true; +} - if (dapp.link) { - buf = Buffer.concat([buf, Buffer.from(dapp.link)]); - } +/** + * @method getAssetDataForSendTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - if (dapp.icon) { - buf = Buffer.concat([buf, Buffer.from(dapp.icon)]); - } +export function getAssetDataForSendTransaction({ data }) { + return data + ? Buffer.from(data, 'utf8') + : Buffer.alloc(0); +} - const bb = new ByteBuffer(4 + 4, true); - bb.writeInt(dapp.type); - bb.writeInt(dapp.category); - bb.flip(); +/** + * @method getAssetDataForSignatureTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - buf = Buffer.concat([buf, bb.toBuffer()]); +export function getAssetDataForSignatureTransaction({ signature }) { + checkRequiredFields(['publicKey'], signature); + const { publicKey } = signature; + return Buffer.from(publicKey, 'hex'); +} - return { - assetBytes: buf, - assetSize: buf.length, - }; - } +/** + * @method getAssetDataForDelegateTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - /** - * @method isDappInTransferTransaction - * @return {object} - */ +export function getAssetDataForDelegateTransaction({ delegate }) { + checkRequiredFields(['username'], delegate); + const { username } = delegate; + return Buffer.from(username, 'utf8'); +} - function isDappInTransferTransaction() { - const buf = Buffer.from(transaction.asset.inTransfer.dappId); +/** + * @method getAssetDataForVotesTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - return { - assetBytes: buf, - assetSize: buf.length, - }; +export function getAssetDataForVotesTransaction({ votes }) { + if (!Array.isArray(votes)) { + throw new Error('votes parameter must be an Array.'); } + return Buffer.from(votes.join(''), 'utf8'); +} - /** - * @method isDappOutTransferTransaction - * @return {object} - */ - - function isDappOutTransferTransaction() { - const dappBuf = Buffer.from(transaction.asset.outTransfer.dappId); - const transactionBuf = Buffer.from(transaction.asset.outTransfer.transactionId); - const buf = Buffer.concat([dappBuf, transactionBuf]); - - return { - assetBytes: buf, - assetSize: buf.length, - }; - } +/** + * @method getAssetDataForMultisignatureTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - /** - * `transactionType` describes the available transaction types. - * - * @property transactionType - * @type object - */ - - const transactionType = { - 0: isSendTransaction, - 1: isSignatureTransaction, - 2: isDelegateTransaction, - 3: isVoteTransaction, - 4: isMultisignatureTransaction, - 5: isDappTransaction, - 6: isDappInTransferTransaction, - 7: isDappOutTransferTransaction, - }; +export function getAssetDataForMultisignatureTransaction({ multisignature }) { + checkRequiredFields(['min', 'lifetime', 'keysgroup'], multisignature); + const { min, lifetime, keysgroup } = multisignature; + const minBuffer = Buffer.alloc(1, min); + const lifetimeBuffer = Buffer.alloc(1, lifetime); + const keysgroupBuffer = Buffer.from(keysgroup.join(''), 'utf8'); - return transactionType[transaction.type](); + return Buffer.concat([minBuffer, lifetimeBuffer, keysgroupBuffer]); } /** - * @method createTransactionBuffer - * @param transaction Object - * @return {buffer} + * @method getAssetDataForDappTransaction + * @param {Object} transactionAsset + * @return {Buffer} */ -function createTransactionBuffer(transaction) { - function assignHexToTransactionBytes(partTransactionBuffer, hexValue) { - const hexBuffer = Buffer.from(hexValue, 'hex'); - for (let i = 0; i < hexBuffer.length; i++) { - partTransactionBuffer.writeByte(hexBuffer[i]); - } - return partTransactionBuffer; - } - - /** - * @method createEmptyTransactionBuffer - * @param assetSize number - * @return {buffer} - */ - - function createEmptyTransactionBuffer(assetSize) { - const typeSizes = { - TRANSACTION_TYPE: 1, - TIMESTAMP: 4, - MULTISIGNATURE_PUBLICKEY: 32, - RECIPIENT_ID: 8, - AMOUNT: 8, - SIGNATURE_TRANSACTION: 64, - SECOND_SIGNATURE_TRANSACTION: 64, - DATA: 64, - }; - - const totalBytes = Object.values(typeSizes) - .reduce((sum, typeSize) => sum + typeSize, 0); - - return new ByteBuffer(totalBytes + assetSize, true); - } - - /** - * @method assignTransactionBuffer - * @param transactionBuffer buffer - * @param assetSize number - * @param assetBytes number - * @return {buffer} - */ +export function getAssetDataForDappTransaction({ dapp }) { + checkRequiredFields(['name', 'link', 'type', 'category'], dapp); + const { name, description, tags, link, icon, type, category } = dapp; + const nameBuffer = Buffer.from(name, 'utf8'); + const linkBuffer = Buffer.from(link, 'utf8'); + const typeBuffer = Buffer.alloc(4, type); + const categoryBuffer = Buffer.alloc(4, category); + + const descriptionBuffer = description ? Buffer.from(description, 'utf8') : Buffer.alloc(0); + const tagsBuffer = tags ? Buffer.from(tags, 'utf8') : Buffer.alloc(0); + const iconBuffer = icon ? Buffer.from(icon, 'utf8') : Buffer.alloc(0); + + return Buffer.concat([ + nameBuffer, + descriptionBuffer, + tagsBuffer, + linkBuffer, + iconBuffer, + typeBuffer, + categoryBuffer, + ]); +} - function assignTransactionBuffer(transactionBuffer, assetSize, assetBytes) { - transactionBuffer.writeInt8(transaction.type); - transactionBuffer.writeInt(transaction.timestamp); +/** + * @method getAssetDataForDappInTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - assignHexToTransactionBytes(transactionBuffer, transaction.senderPublicKey); +export function getAssetDataForDappInTransaction({ inTransfer }) { + checkRequiredFields(['dappId'], inTransfer); + const { dappId } = inTransfer; + return Buffer.from(dappId, 'utf8'); +} - if (transaction.requesterPublicKey) { - assignHexToTransactionBytes(transactionBuffer, transaction.requesterPublicKey); - } +/** + * @method getAssetDataForDappOutTransaction + * @param {Object} transactionAsset + * @return {Buffer} + */ - if (transaction.recipientId) { - let recipient = transaction.recipientId.slice(0, -1); - recipient = bignum(recipient).toBuffer({ size: 8 }); - - for (let i = 0; i < 8; i++) { - transactionBuffer.writeByte(recipient[i] || 0); - } - } else { - for (let i = 0; i < 8; i++) { - transactionBuffer.writeByte(0); - } - } - transactionBuffer.writeLong(transaction.amount); +export function getAssetDataForDappOutTransaction({ outTransfer }) { + checkRequiredFields(['dappId', 'transactionId'], outTransfer); + const { dappId, transactionId } = outTransfer; + const outAppIdBuffer = Buffer.from(dappId, 'utf8'); + const outTransactionIdBuffer = Buffer.from(transactionId, 'utf8'); - if (transaction.asset.data) { - const dataBuffer = Buffer.from(transaction.asset.data); - for (let i = 0; i < dataBuffer.length; i++) { - transactionBuffer.writeByte(dataBuffer[i]); - } - } + return Buffer.concat([outAppIdBuffer, outTransactionIdBuffer]); +} - if (assetSize > 0) { - for (let i = 0; i < assetSize; i++) { - transactionBuffer.writeByte(assetBytes[i]); - } - } +/** + * @method getAssetBytes + * @param {Object} transaction + * @return {Buffer} + */ - if (transaction.signature) { - assignHexToTransactionBytes(transactionBuffer, transaction.signature); - } +export function getAssetBytes(transaction) { + const assetDataGetters = { + 0: getAssetDataForSendTransaction, + 1: getAssetDataForSignatureTransaction, + 2: getAssetDataForDelegateTransaction, + 3: getAssetDataForVotesTransaction, + 4: getAssetDataForMultisignatureTransaction, + 5: getAssetDataForDappTransaction, + 6: getAssetDataForDappInTransaction, + 7: getAssetDataForDappOutTransaction, + }; - if (transaction.signSignature) { - assignHexToTransactionBytes(transactionBuffer, transaction.signSignature); - } + return assetDataGetters[transaction.type](transaction.asset); +} - transactionBuffer.flip(); - const arrayBuffer = new Uint8Array(transactionBuffer.toArrayBuffer()); - const buffer = []; +const REQUIRED_TRANSACTION_PARAMETERS = [ + 'type', + 'timestamp', + 'senderPublicKey', + 'amount', +]; - for (let i = 0; i < arrayBuffer.length; i++) { - buffer[i] = arrayBuffer[i]; - } +/** + * @method checkTransaction + * @throws + */ - return Buffer.from(buffer); +export function checkTransaction(transaction) { + checkRequiredFields(REQUIRED_TRANSACTION_PARAMETERS, transaction); + const { asset: { data } } = transaction; + if (data && data.length > BYTESIZES.DATA) { + throw new Error(`Transaction asset data exceeds size of ${BYTESIZES.DATA}.`); } - - // Get Transaction Size and Bytes - const transactionAssetSizeBuffer = getEmptyBytesForTransaction(transaction); - const assetSize = transactionAssetSizeBuffer.assetSize; - const assetBytes = transactionAssetSizeBuffer.assetBytes; - - const emptyTransactionBuffer = createEmptyTransactionBuffer(assetSize); - const assignedTransactionBuffer = assignTransactionBuffer( - emptyTransactionBuffer, assetSize, assetBytes, - ); - - return assignedTransactionBuffer; + return true; } /** - * @method getBytes - * @param transaction Object - * - * @return {buffer} - */ - -function getTransactionBytes(transaction) { - return createTransactionBuffer(transaction); +* A utility method to get transaction bytes +* +* @method TransactionBytes +* @param {Object} transaction +* +*/ + +export default function getTransactionBytes(transaction) { + checkTransaction(transaction); + const transactionType = Buffer.alloc(BYTESIZES.TYPE, transaction.type); + const transactionTimestamp = Buffer.alloc(BYTESIZES.TIMESTAMP); + transactionTimestamp.writeIntLE(transaction.timestamp, 0, BYTESIZES.TIMESTAMP); + + const transactionSenderPublicKey = Buffer.from(transaction.senderPublicKey, 'hex'); + const transactionRequesterPublicKey = transaction.requesterPublicKey + ? Buffer.from(transaction.requesterPublicKey, 'hex') + : Buffer.alloc(0); + + const transactionRecipientID = transaction.recipientId + ? Buffer.from( + bignum( + transaction.recipientId.slice(0, -1), + ).toBuffer({ size: BYTESIZES.RECIPIENT_ID }), + ) + : Buffer.alloc(BYTESIZES.RECIPIENT_ID); + + const transactionAmount = Buffer.alloc(BYTESIZES.AMOUNT); + transactionAmount.writeInt32LE(transaction.amount, 0, BYTESIZES.AMOUNT); + + const transactionAssetData = getAssetBytes(transaction); + + const transactionSignature = transaction.signature + ? Buffer.from(transaction.signature, 'hex') + : Buffer.alloc(0); + + const transactionSecondSignature = transaction.signSignature + ? Buffer.from(transaction.signSignature, 'hex') + : Buffer.alloc(0); + + return Buffer.concat([ + transactionType, + transactionTimestamp, + transactionSenderPublicKey, + transactionRequesterPublicKey, + transactionRecipientID, + transactionAmount, + transactionAssetData, + transactionSignature, + transactionSecondSignature, + ]); } - -module.exports = { - getTransactionBytes, -}; diff --git a/src/transactions/utils.js b/src/transactions/utils.js index 2ef3aa25d..1bb2c8bef 100644 --- a/src/transactions/utils.js +++ b/src/transactions/utils.js @@ -35,6 +35,9 @@ const prepareTransaction = (transaction, secret, secondSecret) => { return transactionWithId; }; +const isValidValue = value => ![undefined, false, NaN].includes(value); + module.exports = { prepareTransaction, + isValidValue, }; diff --git a/test/transactions/transactionBytes.js b/test/transactions/transactionBytes.js index 542b0d39e..e13d71bae 100644 --- a/test/transactions/transactionBytes.js +++ b/test/transactions/transactionBytes.js @@ -12,53 +12,567 @@ * Removal or modification of this copyright notice is prohibited. * */ -import { getTransactionBytes } from '../../src/transactions/transactionBytes'; +import getTransactionBytes, { + getAssetDataForSendTransaction, + getAssetDataForSignatureTransaction, + getAssetDataForDelegateTransaction, + getAssetDataForVotesTransaction, + getAssetDataForMultisignatureTransaction, + getAssetDataForDappTransaction, + getAssetDataForDappInTransaction, + getAssetDataForDappOutTransaction, + checkTransaction, + checkRequiredFields, +} from '../../src/transactions/transactionBytes'; + +const fixedPoint = 10 ** 8; +const defaultRecipient = '58191285901858109L'; +const defaultSenderPublicKey = '5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09'; +const defaultSenderId = '18160565574430594874L'; +const defaultSenderSecondPublicKey = '0401c8ac9f29ded9e1e4d5b6b43051cb25b22f27c7b7b35092161e851946f82f'; +const defaultAmount = 1000; +const defaultNoAmount = 0; +const defaultTimestamp = 141738; +const defaultTransactionId = '13987348420913138422'; +const defaultSignature = '618a54975212ead93df8c881655c625544bce8ed7ccdfe6f08a42eecfb1adebd051307be5014bb051617baf7815d50f62129e70918190361e5d4dd4796541b0a'; +const defaultSecondSignature = 'b00c4ad1988bca245d74435660a278bfe6bf2f5efa8bda96d927fabf8b4f6fcfdcb2953f6abacaa119d6880987a55dea0e6354bc8366052b45fa23145522020f'; +const defaultAppId = '1234213'; +const defaultDelegateUsername = 'MyDelegateUsername'; describe('#getTransactionBytes', () => { - let bytes = null; + describe('send transaction, type 0', () => { + let defaultTransaction; + + beforeEach(() => { + defaultTransaction = { + type: 0, + fee: 0.1 * fixedPoint, + amount: defaultAmount, + recipientId: defaultRecipient, + timestamp: defaultTimestamp, + asset: {}, + senderPublicKey: defaultSenderPublicKey, + senderId: defaultSenderId, + signature: defaultSignature, + id: defaultTransactionId, + }; + }); + + it('should return Buffer of type 0 (send LSK) transaction', () => { + const expectedBuffer = Buffer.from('AKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU96AMAAAAAAABhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(defaultTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + + it('should return Buffer of type 0 (send LSK) with data', () => { + defaultTransaction.asset.data = 'Hello Lisk! Some data in here!...'; + const expectedBuffer = Buffer.from('AKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU96AMAAAAAAABIZWxsbyBMaXNrISBTb21lIGRhdGEgaW4gaGVyZSEuLi5hilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(defaultTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + + it('should throw on type 0 with too much data', () => { + const maxDataLength = 64; + defaultTransaction.asset.data = new Array(maxDataLength + 1).fill('1').join(''); + (getTransactionBytes.bind(null, defaultTransaction)).should.throw('Transaction asset data exceeds size of 64.'); + }); + + it('should return Buffer of transaction with second signature', () => { + defaultTransaction.signSignature = defaultSecondSignature; + const expectedBuffer = Buffer.from('AKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU96AMAAAAAAABhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsKsAxK0ZiLyiRddENWYKJ4v+a/L176i9qW2Sf6v4tPb8/cspU/arrKoRnWiAmHpV3qDmNUvINmBStF+iMUVSICDw==', 'base64'); + const transactionBytes = getTransactionBytes(defaultTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + + it('should return Buffer from multisignature type 0 (send LSK) transaction', () => { + const multiSignatureTransaction = { + type: 0, + amount: 1000, + fee: 1 * fixedPoint, + recipientId: defaultRecipient, + senderPublicKey: defaultSenderPublicKey, + requesterPublicKey: '5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09', + timestamp: defaultTimestamp, + asset: {}, + signatures: [], + signature: defaultSignature, + id: defaultTransactionId, + }; + const expectedBuffer = Buffer.from('AKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCV0DaoWM6J+ERJF2LrieK/vVCkoKDaZY5LJiiyWxF64JAM68qo00FT3oAwAAAAAAAGGKVJdSEurZPfjIgWVcYlVEvOjtfM3+bwikLuz7Gt69BRMHvlAUuwUWF7r3gV1Q9iEp5wkYGQNh5dTdR5ZUGwo=', 'base64'); + const transactionBytes = getTransactionBytes(multiSignatureTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); - it('should be ok', () => { - (getTransactionBytes).should.be.ok(); + it('should return Buffer of type 0 (send LSK) with additional properties', () => { + defaultTransaction.skip = false; + const expectedBuffer = Buffer.from('AKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU96AMAAAAAAABhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(defaultTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + + it('should throw on missing required parameters', () => { + const requiredProperties = ['type', 'timestamp', 'senderPublicKey', 'amount']; + + requiredProperties.forEach((parameter) => { + const defaultTransactionClone = Object.assign({}, defaultTransaction); + delete defaultTransactionClone[parameter]; + (getTransactionBytes.bind(null, defaultTransactionClone)).should.throw(`${parameter} is a required parameter.`); + }); + }); + + it('should throw on required parameters as undefined', () => { + const requiredProperties = ['type', 'timestamp', 'senderPublicKey', 'amount']; + + requiredProperties.forEach((parameter) => { + const defaultTransactionClone = Object.assign({}, defaultTransaction); + defaultTransactionClone[parameter] = undefined; + (getTransactionBytes.bind(null, defaultTransactionClone)).should.throw(`${parameter} is a required parameter.`); + }); + }); }); - it('should be a function', () => { - (getTransactionBytes).should.be.type('function'); + describe('signature transaction, type 1', () => { + const signatureTransaction = { + type: 1, + amount: defaultNoAmount, + fee: 5 * fixedPoint, + recipientId: null, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { signature: { publicKey: defaultSenderSecondPublicKey } }, + signature: defaultSignature, + id: defaultTransactionId, + }; + + it('should return Buffer of type 1 (register second signature) transaction', () => { + const expectedBuffer = Buffer.from('AaopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQAAAAAAAAAAAAAAAAAAAAAEAcisnyne2eHk1ba0MFHLJbIvJ8e3s1CSFh6FGUb4L2GKVJdSEurZPfjIgWVcYlVEvOjtfM3+bwikLuz7Gt69BRMHvlAUuwUWF7r3gV1Q9iEp5wkYGQNh5dTdR5ZUGwo=', 'base64'); + const transactionBytes = getTransactionBytes(signatureTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); }); - it('should return Buffer of simply transaction and buffer most be 117 length', () => { - const transaction = { - type: 0, - amount: 1000, - recipientId: '58191285901858109L', - timestamp: 141738, - asset: {}, - senderPublicKey: '5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09', - signature: '618a54975212ead93df8c881655c625544bce8ed7ccdfe6f08a42eecfb1adebd051307be5014bb051617baf7815d50f62129e70918190361e5d4dd4796541b0a', - id: '13987348420913138422', + describe('delegate registration transaction, type 2', () => { + const delegateRegistrationTransaction = { + type: 2, + amount: defaultNoAmount, + fee: 25 * fixedPoint, + recipientId: null, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { delegate: { username: defaultDelegateUsername } }, + signature: defaultSignature, + id: defaultTransactionId, }; - bytes = getTransactionBytes(transaction); - (bytes).should.be.ok(); - (bytes).should.be.type('object'); - (bytes.length).should.be.equal(117); - }); - - it('should return Buffer of transaction with second signature and buffer most be 181 length', () => { - const transaction = { - type: 0, - amount: 1000, - recipientId: '58191285901858109L', - timestamp: 141738, - asset: {}, - senderPublicKey: '5d036a858ce89f844491762eb89e2bfbd50a4a0a0da658e4b2628b25b117ae09', - signature: '618a54975212ead93df8c881655c625544bce8ed7ccdfe6f08a42eecfb1adebd051307be5014bb051617baf7815d50f62129e70918190361e5d4dd4796541b0a', - signSignature: '618a54975212ead93df8c881655c625544bce8ed7ccdfe6f08a42eecfb1adebd051307be5014bb051617baf7815d50f62129e70918190361e5d4dd4796541b0a', - id: '13987348420913138422', + it('should return Buffer of type 2 (register delegate) transaction', () => { + const expectedBuffer = Buffer.from('AqopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQAAAAAAAAAAAAAAAAAAAABNeURlbGVnYXRlVXNlcm5hbWVhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(delegateRegistrationTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); + + describe('vote transaction, type 3', () => { + const voteTransaction = { + type: 3, + amount: 0, + fee: 1 * fixedPoint, + recipientId: defaultRecipient, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { + votes: [ + `+${defaultSenderPublicKey}`, + `+${defaultSenderSecondPublicKey}`, + ], + }, + signature: defaultSignature, + id: defaultTransactionId, }; - bytes = getTransactionBytes(transaction); - (bytes).should.be.ok(); - (bytes).should.be.type('object'); - (bytes.length).should.be.equal(181); + it('should return Buffer of type 3 (vote) transaction', () => { + const expectedBuffer = Buffer.from('A6opAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU9AAAAAAAAAAArNWQwMzZhODU4Y2U4OWY4NDQ0OTE3NjJlYjg5ZTJiZmJkNTBhNGEwYTBkYTY1OGU0YjI2MjhiMjViMTE3YWUwOSswNDAxYzhhYzlmMjlkZWQ5ZTFlNGQ1YjZiNDMwNTFjYjI1YjIyZjI3YzdiN2IzNTA5MjE2MWU4NTE5NDZmODJmYYpUl1IS6tk9+MiBZVxiVUS86O18zf5vCKQu7Psa3r0FEwe+UBS7BRYXuveBXVD2ISnnCRgZA2Hl1N1HllQbCg==', 'base64'); + const transactionBytes = getTransactionBytes(voteTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); + + describe('multisignature transaction, type 4', () => { + const createMultiSignatureTransaction = { + type: 4, + amount: 0, + fee: 15 * fixedPoint, + recipientId: null, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { + multisignature: + { + min: 2, + lifetime: 5, + keysgroup: [ + `+${defaultSenderPublicKey}`, + `+${defaultSenderSecondPublicKey}`, + ], + }, + }, + signature: defaultSignature, + id: defaultTransactionId, + }; + + it('should return Buffer from type 4 (register multisignature) transaction', () => { + const expectedBuffer = Buffer.from('BKopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQAAAAAAAAAAAAAAAAAAAAACBSs1ZDAzNmE4NThjZTg5Zjg0NDQ5MTc2MmViODllMmJmYmQ1MGE0YTBhMGRhNjU4ZTRiMjYyOGIyNWIxMTdhZTA5KzA0MDFjOGFjOWYyOWRlZDllMWU0ZDViNmI0MzA1MWNiMjViMjJmMjdjN2I3YjM1MDkyMTYxZTg1MTk0NmY4MmZhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(createMultiSignatureTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); + + describe('dapp transaction, type 5', () => { + const dappTransaction = { + type: 5, + amount: 0, + fee: 25 * fixedPoint, + recipientId: null, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { + dapp: { + category: 0, + name: 'Lisk Guestbook', + description: 'The official Lisk guestbook', + tags: 'guestbook message sidechain', + type: 0, + link: 'https://github.com/MaxKK/guestbookDapp/archive/master.zip', + icon: 'https://raw.githubusercontent.com/MaxKK/guestbookDapp/master/icon.png', + }, + }, + signature: defaultSignature, + id: defaultTransactionId, + }; + + it('should return Buffer of type 5 (register dapp) transaction', () => { + const expectedBuffer = Buffer.from('BaopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQAAAAAAAAAAAAAAAAAAAABMaXNrIEd1ZXN0Ym9va1RoZSBvZmZpY2lhbCBMaXNrIGd1ZXN0Ym9va2d1ZXN0Ym9vayBtZXNzYWdlIHNpZGVjaGFpbmh0dHBzOi8vZ2l0aHViLmNvbS9NYXhLSy9ndWVzdGJvb2tEYXBwL2FyY2hpdmUvbWFzdGVyLnppcGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9NYXhLSy9ndWVzdGJvb2tEYXBwL21hc3Rlci9pY29uLnBuZwAAAAAAAAAAYYpUl1IS6tk9+MiBZVxiVUS86O18zf5vCKQu7Psa3r0FEwe+UBS7BRYXuveBXVD2ISnnCRgZA2Hl1N1HllQbCg==', 'base64'); + const transactionBytes = getTransactionBytes(dappTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); + + describe('inTransfer transaction, type 6', () => { + const inTransferTransction = { + type: 6, + amount: defaultAmount, + fee: 1 * fixedPoint, + recipientId: null, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { inTransfer: { dappId: defaultAppId } }, + signature: defaultSignature, + id: defaultTransactionId, + }; + it('should return Buffer of type 6 (dapp inTransfer) transaction', () => { + const expectedBuffer = Buffer.from('BqopAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQAAAAAAAAAA6AMAAAAAAAAxMjM0MjEzYYpUl1IS6tk9+MiBZVxiVUS86O18zf5vCKQu7Psa3r0FEwe+UBS7BRYXuveBXVD2ISnnCRgZA2Hl1N1HllQbCg==', 'base64'); + const transactionBytes = getTransactionBytes(inTransferTransction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); + + describe('outTransfer transaction, type 7', () => { + const outTransferTransaction = { + type: 7, + amount: defaultAmount, + fee: 1 * fixedPoint, + recipientId: defaultRecipient, + senderPublicKey: defaultSenderPublicKey, + timestamp: defaultTimestamp, + asset: { outTransfer: { dappId: defaultAppId, transactionId: defaultTransactionId } }, + signature: defaultSignature, + id: defaultTransactionId, + }; + it('should return Buffer of type 7 (dapp outTransfer) transaction', () => { + const expectedBuffer = Buffer.from('B6opAgBdA2qFjOifhESRdi64niv71QpKCg2mWOSyYoslsReuCQDOvKqNNBU96AMAAAAAAAAxMjM0MjEzMTM5ODczNDg0MjA5MTMxMzg0MjJhilSXUhLq2T34yIFlXGJVRLzo7XzN/m8IpC7s+xrevQUTB75QFLsFFhe694FdUPYhKecJGBkDYeXU3UeWVBsK', 'base64'); + const transactionBytes = getTransactionBytes(outTransferTransaction); + + (transactionBytes).should.be.eql(expectedBuffer); + }); + }); +}); + +describe('getTransactionBytes functions', () => { + describe('#checkRequiredFields', () => { + const arrayToCheck = ['OneValue', 'SecondValue', 'ThirdValue', 1]; + it('should accept array and object to check for required fields', () => { + const objectParameter = { + OneValue: '1', + SecondValue: '2', + ThirdValue: '3', + 1: 10, + }; + + (checkRequiredFields(arrayToCheck, objectParameter)).should.be.true(); + }); + + it('should throw on missing value', () => { + const objectParameter = { + OneValue: '1', + SecondValue: '2', + 1: 10, + }; + + (checkRequiredFields.bind(null, arrayToCheck, objectParameter)).should.throw('ThirdValue is a required parameter.'); + }); + }); + + describe('#getAssetDataForSendTransaction', () => { + const defaultEmptyBuffer = Buffer.alloc(0); + it('should return Buffer for data asset', () => { + const expectedBuffer = Buffer.from('my data input', 'utf8'); + const assetDataBuffer = getAssetDataForSendTransaction({ + data: 'my data input', + }); + + (assetDataBuffer).should.be.eql(expectedBuffer); + }); + + it('should return empty Buffer for no asset data', () => { + const assetDataBuffer = getAssetDataForSendTransaction({}); + (assetDataBuffer).should.be.eql(defaultEmptyBuffer); + }); + }); + + describe('#getAssetDataForSignatureTransaction', () => { + it('should return Buffer for signature asset', () => { + const expectedBuffer = Buffer.from(defaultSenderPublicKey, 'hex'); + const assetSignaturesPublicKeyBuffer = getAssetDataForSignatureTransaction({ + signature: { + publicKey: defaultSenderPublicKey, + }, + }); + + (assetSignaturesPublicKeyBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw on missing publicKey in the signature asset', () => { + (getAssetDataForSignatureTransaction.bind(null, { signature: {} })).should.throw( + 'publicKey is a required parameter.', + ); + }); + }); + + describe('#getAssetDataForDelegateTransaction', () => { + it('should return Buffer for delegate asset', () => { + const expectedBuffer = Buffer.from(defaultDelegateUsername, 'utf8'); + const assetDelegateUsernameBuffer = getAssetDataForDelegateTransaction({ + delegate: { + username: defaultDelegateUsername, + }, + }); + + (assetDelegateUsernameBuffer).should.be.eql(expectedBuffer); + }); + it('should throw on missing username in the delegate asset', () => { + (getAssetDataForDelegateTransaction.bind(null, { delegate: {} })).should.throw( + 'username is a required parameter.', + ); + }); + }); + + describe('#getAssetDataForVotesTransaction', () => { + it('should return Buffer for votes asset', () => { + const votesAsset = { + votes: [ + `+${defaultSenderPublicKey}`, + `+${defaultSenderSecondPublicKey}`, + ], + }; + const expectedBuffer = Buffer.from(`+${defaultSenderPublicKey}+${defaultSenderSecondPublicKey}`, 'utf8'); + const assetVoteBuffer = getAssetDataForVotesTransaction(votesAsset); + + (assetVoteBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw on missing votes in the vote asset', () => { + (getAssetDataForVotesTransaction.bind(null, { votes: {} })).should.throw( + 'votes parameter must be an Array.', + ); + }); + }); + + describe('#getAssetDataForMultisignatureTransaction', () => { + const min = 2; + const lifetime = 5; + const keysgroup = ['+123456789', '-987654321']; + let multisignatureAsset; + beforeEach(() => { + multisignatureAsset = { + multisignature: { + min, + lifetime, + keysgroup, + }, + }; + }); + it('should return Buffer for multisignature asset', () => { + const minBuffer = Buffer.alloc(1, min); + const lifetimeBuffer = Buffer.alloc(1, lifetime); + const keysgroupBuffer = Buffer.from('+123456789-987654321', 'utf8'); + + const expectedBuffer = Buffer.concat([minBuffer, lifetimeBuffer, keysgroupBuffer]); + const multisignatureBuffer = getAssetDataForMultisignatureTransaction(multisignatureAsset); + + (multisignatureBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw on missing required parameters', () => { + const requiredProperties = ['min', 'lifetime', 'keysgroup']; + + requiredProperties.forEach((parameter) => { + const multisigAsset = Object.assign({}, multisignatureAsset.multisignature); + delete multisigAsset[parameter]; + (getAssetDataForMultisignatureTransaction.bind(null, { multisignature: multisigAsset })).should.throw(`${parameter} is a required parameter.`); + }); + }); + }); + + describe('#getAssetDataForDappTransaction', () => { + const defaultCategory = 0; + const defaultDappName = 'Lisk Guestbook'; + const defaultDescription = 'The official Lisk guestbook'; + const defaultTags = 'guestbook message sidechain'; + const defaultType = 0; + const defaultLink = 'https://github.com/MaxKK/guestbookDapp/archive/master.zip'; + const defaultIcon = 'https://raw.githubusercontent.com/MaxKK/guestbookDapp/master/icon.png'; + const dappNameBuffer = Buffer.from('TGlzayBHdWVzdGJvb2s=', 'base64'); + const dappDescriptionBuffer = Buffer.from('VGhlIG9mZmljaWFsIExpc2sgZ3Vlc3Rib29r', 'base64'); + const dappTagsBuffer = Buffer.from('Z3Vlc3Rib29rIG1lc3NhZ2Ugc2lkZWNoYWlu', 'base64'); + const dappLinkBuffer = Buffer.from('aHR0cHM6Ly9naXRodWIuY29tL01heEtLL2d1ZXN0Ym9va0RhcHAvYXJjaGl2ZS9tYXN0ZXIuemlw', 'base64'); + const dappIconBuffer = Buffer.from('aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL01heEtLL2d1ZXN0Ym9va0RhcHAvbWFzdGVyL2ljb24ucG5n', 'base64'); + const dappTypeBuffer = Buffer.alloc(4, defaultType); + const dappCategoryBuffer = Buffer.alloc(4, defaultCategory); + it('should return Buffer for create dapp asset', () => { + const dappAsset = { + dapp: { + category: defaultCategory, + name: defaultDappName, + description: defaultDescription, + tags: defaultTags, + type: defaultType, + link: defaultLink, + icon: defaultIcon, + }, + }; + + const expectedBuffer = Buffer.concat([ + dappNameBuffer, + dappDescriptionBuffer, + dappTagsBuffer, + dappLinkBuffer, + dappIconBuffer, + dappTypeBuffer, + dappCategoryBuffer, + ]); + const dappBuffer = getAssetDataForDappTransaction(dappAsset); + + (dappBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw for create dapp asset without required fields', () => { + const dapp = { + category: defaultCategory, + name: defaultDappName, + description: defaultDescription, + tags: defaultTags, + type: defaultType, + link: defaultLink, + icon: defaultIcon, + }; + const requiredProperties = ['name', 'link', 'type', 'category']; + + requiredProperties.forEach((parameter) => { + const dappClone = Object.assign({}, dapp); + delete dappClone[parameter]; + (getAssetDataForDappTransaction.bind(null, { dapp: dappClone })).should.throw(`${parameter} is a required parameter.`); + }); + }); + }); + + describe('#getAssetDataForDappInTransaction', () => { + it('should return Buffer for dappIn asset', () => { + const dappInAsset = { + inTransfer: { + dappId: defaultAppId, + }, + }; + const expectedBuffer = Buffer.from(defaultAppId, 'utf8'); + const dappInTransferBuffer = getAssetDataForDappInTransaction(dappInAsset); + + (dappInTransferBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw on missing votes in the vote asset', () => { + (getAssetDataForDappInTransaction.bind(null, { inTransfer: {} })).should.throw( + 'dappId is a required parameter.', + ); + }); + }); + + describe('#getAssetDataForDappOutTransaction', () => { + it('should return Buffer for dappOut asset', () => { + const dappOutAsset = { + outTransfer: { + dappId: defaultAppId, + transactionId: defaultTransactionId, + }, + }; + const dappIdBuffer = Buffer.from(defaultAppId, 'utf8'); + const transactionIdBuffer = Buffer.from(defaultTransactionId); + const expectedBuffer = Buffer.concat([dappIdBuffer, transactionIdBuffer]); + const dappOutTransferBuffer = getAssetDataForDappOutTransaction(dappOutAsset); + + (dappOutTransferBuffer).should.be.eql(expectedBuffer); + }); + + it('should throw on missing votes in the vote asset', () => { + (getAssetDataForDappOutTransaction.bind(null, { outTransfer: {} })).should.throw( + 'dappId is a required parameter.', + ); + }); + }); + + describe('#checkTransaction', () => { + const maxDataLength = 64; + let defaultTransaction; + beforeEach(() => { + defaultTransaction = { + type: 0, + fee: 0.1 * fixedPoint, + amount: defaultAmount, + recipientId: defaultRecipient, + timestamp: defaultTimestamp, + asset: {}, + senderPublicKey: defaultSenderPublicKey, + senderId: defaultSenderId, + signature: defaultSignature, + id: defaultTransactionId, + }; + }); + it('should throw on too many data in send asset', () => { + defaultTransaction.asset.data = new Array(maxDataLength + 1).fill('1').join(''); + (checkTransaction.bind(null, defaultTransaction)).should.throw('Transaction asset data exceeds size of 64.'); + }); + + it('should return true on asset data exactly at max data length', () => { + defaultTransaction.asset.data = new Array(maxDataLength).fill('1').join(''); + (checkTransaction(defaultTransaction)).should.be.true(); + }); }); }); diff --git a/test/transactions/utils.js b/test/transactions/utils.js index f01e47d52..9ef01b481 100644 --- a/test/transactions/utils.js +++ b/test/transactions/utils.js @@ -81,4 +81,21 @@ describe('transactions utils module', () => { }); }); }); + + describe('isInvalidValue', () => { + it('should return false on invalid values', () => { + const allInvalidValues = [NaN, false, undefined]; + allInvalidValues.forEach((value) => { + const invalid = utils.isValidValue(value); + (invalid).should.be.false(); + }); + }); + it('should return true on valid values', () => { + const exampleValidValues = ['123', 123, { 1: 2, 3: 4 }, [1, 2, 3]]; + exampleValidValues.forEach((value) => { + const valid = utils.isValidValue(value); + (valid).should.be.true(); + }); + }); + }); });