From aff8179f13ff5a630a65dc1a78cf5704ead81325 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 11 Jun 2019 19:41:47 +0100 Subject: [PATCH] Added #93 Merged encoders from Lib into SDK --- e2e/infrastructure/TransactionHttp.spec.ts | 3 +- index.ts | 1 + src/core/format/Base32.ts | 52 +++ src/core/format/Convert.ts | 175 +++++++++ src/core/format/IdGenerator.ts | 53 +++ src/core/format/RawAddress.ts | 130 +++++++ src/core/format/RawArray.ts | 84 +++++ src/core/format/RawUInt64.ts | 116 ++++++ src/core/format/Utilities.ts | 181 +++++++++ src/core/format/index.ts | 21 ++ src/infrastructure/NamespaceHttp.ts | 3 +- .../transaction/CreateTransactionFromDTO.ts | 5 +- .../CreateTransactionFromPayload.ts | 2 +- src/model/UInt64.ts | 2 +- src/model/account/Account.ts | 3 +- src/model/account/Address.ts | 2 +- src/model/account/PublicAccount.ts | 3 +- src/model/mosaic/MosaicId.ts | 5 +- src/model/mosaic/MosaicNonce.ts | 3 +- src/model/mosaic/NetworkHarvestMosaic.ts | 3 - src/model/namespace/AddressAlias.ts | 1 - src/model/namespace/Alias.ts | 4 - src/model/namespace/EmptyAlias.ts | 2 - src/model/namespace/MosaicAlias.ts | 1 - src/model/namespace/NamespaceId.ts | 3 +- src/model/transaction/AliasTransaction.ts | 5 - src/model/transaction/HashType.ts | 2 +- .../RegisterNamespaceTransaction.ts | 3 +- .../transaction/SecretLockTransaction.ts | 3 +- .../transaction/SecretProofTransaction.ts | 3 +- src/model/transaction/TransferTransaction.ts | 3 +- src/model/wallet/SimpleWallet.ts | 3 +- test/core/format/Base32.spec.ts | 188 ++++++++++ test/core/format/Convert.spec.ts | 352 ++++++++++++++++++ test/core/format/IdGenerator.spec.ts | 251 +++++++++++++ test/core/format/RawAddress.spec.ts | 216 +++++++++++ test/core/format/RawArray.spec.ts | 223 +++++++++++ test/core/format/RawUInt64.spec.ts | 319 ++++++++++++++++ test/core/format/Utilities.spec.ts | 146 ++++++++ test/core/utils/TransactionMapping.spec.ts | 2 +- .../SerializeTransactionToJSON.spec.ts | 2 +- .../transaction/SecretLockTransaction.spec.ts | 2 +- .../SecretProofTransaction.spec.ts | 2 +- 43 files changed, 2541 insertions(+), 42 deletions(-) create mode 100644 src/core/format/Base32.ts create mode 100644 src/core/format/Convert.ts create mode 100644 src/core/format/IdGenerator.ts create mode 100644 src/core/format/RawAddress.ts create mode 100644 src/core/format/RawArray.ts create mode 100644 src/core/format/RawUInt64.ts create mode 100644 src/core/format/Utilities.ts create mode 100644 src/core/format/index.ts create mode 100644 test/core/format/Base32.spec.ts create mode 100644 test/core/format/Convert.spec.ts create mode 100644 test/core/format/IdGenerator.spec.ts create mode 100644 test/core/format/RawAddress.spec.ts create mode 100644 test/core/format/RawArray.spec.ts create mode 100644 test/core/format/RawUInt64.spec.ts create mode 100644 test/core/format/Utilities.spec.ts diff --git a/e2e/infrastructure/TransactionHttp.spec.ts b/e2e/infrastructure/TransactionHttp.spec.ts index c94cca7fdc..f27931a440 100644 --- a/e2e/infrastructure/TransactionHttp.spec.ts +++ b/e2e/infrastructure/TransactionHttp.spec.ts @@ -17,7 +17,8 @@ import {assert, expect} from 'chai'; import * as CryptoJS from 'crypto-js'; import {ChronoUnit} from 'js-joda'; import {keccak_256, sha3_256} from 'js-sha3'; -import {convert, nacl_catapult} from 'nem2-library'; +import {nacl_catapult} from 'nem2-library'; +import { Convert as convert } from '../../src/core/format'; import {AccountHttp} from '../../src/infrastructure/AccountHttp'; import { NamespaceHttp } from '../../src/infrastructure/infrastructure'; import {Listener} from '../../src/infrastructure/Listener'; diff --git a/index.ts b/index.ts index 7c61994968..1146495f42 100644 --- a/index.ts +++ b/index.ts @@ -18,3 +18,4 @@ export * from './src/infrastructure/infrastructure'; export * from './src/model/model'; export * from './src/service/service'; export * from './src/core/utils/utility'; +export * from './src/core/format'; diff --git a/src/core/format/Base32.ts b/src/core/format/Base32.ts new file mode 100644 index 0000000000..ac5314f457 --- /dev/null +++ b/src/core/format/Base32.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as utilities from './Utilities'; + +export class Base32 { + /** + * Base32 encodes a binary buffer. + * @param {Uint8Array} data The binary data to encode. + * @returns {string} The base32 encoded string corresponding to the input data. + */ + public static Base32Encode = (data: Uint8Array): string => { + if (0 !== data.length % utilities.Decoded_Block_Size) { + throw Error(`decoded size must be multiple of ${utilities.Decoded_Block_Size}`); + } + const output = new Array(data.length / utilities.Decoded_Block_Size * utilities.Encoded_Block_Size); + for (let i = 0; i < data.length / utilities.Decoded_Block_Size; ++i) { + utilities.encodeBlock(data, i * utilities.Decoded_Block_Size, output, i * utilities.Encoded_Block_Size); + } + return output.join(''); + } + + /** + * Base32 decodes a base32 encoded string. + * @param {string} encoded The base32 encoded string to decode. + * @returns {Uint8Array} The binary data corresponding to the input string. + */ + public static Base32Decode = (encoded: string): Uint8Array => { + if (0 !== encoded.length % utilities.Encoded_Block_Size) { + throw Error(`encoded size must be multiple of ${utilities.Encoded_Block_Size}`); + } + + const output = new Uint8Array(encoded.length / utilities.Encoded_Block_Size * utilities.Decoded_Block_Size); + for (let i = 0; i < encoded.length / utilities.Encoded_Block_Size; ++i) { + utilities.decodeBlock(encoded, i * utilities.Encoded_Block_Size, output, i * utilities.Decoded_Block_Size); + } + return output; + } +} diff --git a/src/core/format/Convert.ts b/src/core/format/Convert.ts new file mode 100644 index 0000000000..f6d12097b2 --- /dev/null +++ b/src/core/format/Convert.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as utilities from './Utilities'; + +export class Convert { + + /** + * Decodes two hex characters into a byte. + * @param {string} char1 The first hex digit. + * @param {string} char2 The second hex digit. + * @returns {number} The decoded byte. + */ + public static toByte = (char1: string, char2: string): number => { + const byte = utilities.tryParseByte(char1, char2); + if (undefined === byte) { + throw Error(`unrecognized hex char`); + } + return byte; + } + + /** + * Determines whether or not a string is a hex string. + * @param {string} input The string to test. + * @returns {boolean} true if the input is a hex string, false otherwise. + */ + public static isHexString = (input: string): boolean => { + if (0 !== input.length % 2) { + return false; + } + for (let i = 0; i < input.length; i += 2) { + if (undefined === utilities.tryParseByte(input[i], input[i + 1])) { + return false; + } + } + return true; + } + + /** + * Converts a hex string to a uint8 array. + * @param {string} input A hex encoded string. + * @returns {Uint8Array} A uint8 array corresponding to the input. + */ + public static hexToUint8 = (input: string): Uint8Array => { + if (0 !== input.length % 2) { + throw Error(`hex string has unexpected size '${input.length}'`); + } + const output = new Uint8Array(input.length / 2); + for (let i = 0; i < input.length; i += 2) { + output[i / 2] = Convert.toByte(input[i], input[i + 1]); + } + return output; + } + + /** + * Reversed convertion hex string to a uint8 array. + * @param {string} input A hex encoded string. + * @returns {Uint8Array} A uint8 array corresponding to the input. + */ + public static hexToUint8Reverse = (input: string): Uint8Array => { + if (0 !== input.length % 2) { + throw Error(`hex string has unexpected size '${input.length}'`); + } + const output = new Uint8Array(input.length / 2); + for (let i = 0; i < input.length; i += 2) { + output[output.length - 1 - (i / 2)] = Convert.toByte(input[i], input[i + 1]); + } + return output; + } + + /** + * Converts a uint8 array to a hex string. + * @param {Uint8Array} input A uint8 array. + * @returns {string} A hex encoded string corresponding to the input. + */ + public static uint8ToHex = (input) => { + let s = ''; + for (const byte of input) { + s += utilities.Nibble_To_Char_Map[byte >> 4]; + s += utilities.Nibble_To_Char_Map[byte & 0x0F]; + } + + return s; + } + + /** + * Converts a uint8 array to a uint32 array. + * @param {Uint8Array} input A uint8 array. + * @returns {Uint32Array} A uint32 array created from the input. + */ + public static uint8ToUint32 = (input) => new Uint32Array(input.buffer); + + /** + * Converts a uint32 array to a uint8 array. + * @param {Uint32Array} input A uint32 array. + * @returns {Uint8Array} A uint8 array created from the input. + */ + public static uint32ToUint8 = (input: Uint32Array): Uint8Array => new Uint8Array(input.buffer); + + /** Converts an unsigned byte to a signed byte with the same binary representation. + * @param {number} input An unsigned byte. + * @returns {number} A signed byte with the same binary representation as the input. + * + */ + public static uint8ToInt8 = (input: number): number => { + if (0xFF < input) { + throw Error(`input '${input}' is out of range`); + } + return input << 24 >> 24; + } + + /** Converts a signed byte to an unsigned byte with the same binary representation. + * @param {number} input A signed byte. + * @returns {number} An unsigned byte with the same binary representation as the input. + */ + public static int8ToUint8 = (input: number): number => { + if (127 < input || -128 > input) { + throw Error(`input '${input}' is out of range`); + } + return input & 0xFF; + } + + /** + * Converts a raw javascript string into a string of single byte characters using utf8 encoding. + * This makes it easier to perform other encoding operations on the string. + * @param {string} input - A raw string + * @return {string} - UTF-8 string + */ + public static rstr2utf8 = (input: string): string => { + let output = ''; + + for (let n = 0; n < input.length; n++) { + const c = input.charCodeAt(n); + + if (128 > c) { + output += String.fromCharCode(c); + } else if ((127 < c) && (2048 > c)) { + output += String.fromCharCode((c >> 6) | 192); + output += String.fromCharCode((c & 63) | 128); + } else { + output += String.fromCharCode((c >> 12) | 224); + output += String.fromCharCode(((c >> 6) & 63) | 128); + output += String.fromCharCode((c & 63) | 128); + } + } + + return output; + } + + /** + * Convert UTF-8 to hex + * @param {string} input - An UTF-8 string + * @return {string} + */ + public static utf8ToHex = (input: string): string => { + const rawString = Convert.rstr2utf8(input); + let result = ''; + for (let i = 0; i < rawString.length; i++) { + result += rawString.charCodeAt(i).toString(16); + } + return result; + } +} diff --git a/src/core/format/IdGenerator.ts b/src/core/format/IdGenerator.ts new file mode 100644 index 0000000000..e0ab0967b3 --- /dev/null +++ b/src/core/format/IdGenerator.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {sha3_256} from 'js-sha3'; +import * as utilities from './Utilities'; + +export class IdGenerator { + /** + * Generates a mosaic id given a nonce and a public id. + * @param {object} nonce The mosaic nonce. + * @param {object} ownerPublicId The public id. + * @returns {module:coders/uint64~uint64} The mosaic id. + */ + public static generateMosaicId = (nonce, ownerPublicId) => { + const hash = sha3_256.create(); + hash.update(nonce); + hash.update(ownerPublicId); + const result = new Uint32Array(hash.arrayBuffer()); + return [result[0], result[1] & 0x7FFFFFFF]; + } + + /** + * Parses a unified namespace name into a path. + * @param {string} name The unified namespace name. + * @returns {array} The namespace path. + */ + public static generateNamespacePath = (name: string) => { + if (0 >= name.length) { + utilities.throwInvalidFqn('having zero length', name); + } + let namespaceId = utilities.idGeneratorConst.namespace_base_id; + const path = []; + const start = utilities.split(name, (substringStart, size) => { + namespaceId = utilities.generateNamespaceId(namespaceId, utilities.extractPartName(name, substringStart, size)); + utilities.append(path, namespaceId, name); + }); + namespaceId = utilities.generateNamespaceId(namespaceId, utilities.extractPartName(name, start, name.length - start)); + utilities.append(path, namespaceId, name); + return path; + } +} diff --git a/src/core/format/RawAddress.ts b/src/core/format/RawAddress.ts new file mode 100644 index 0000000000..c512e3a4f4 --- /dev/null +++ b/src/core/format/RawAddress.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { sha3_256 } from 'js-sha3'; +import RIPEMD160 = require('ripemd160'); +import { Base32 } from './Base32'; +import { Convert } from './Convert'; +import { RawArray } from './RawArray'; + +export class RawAddress { + static readonly constants = { + sizes: { + ripemd160: 20, + addressDecoded: 25, + addressEncoded: 40, + key: 32, + checksum: 4, + }, + }; + /** + * Converts an encoded address string to a decoded address. + * @param {string} encoded The encoded address string. + * @returns {Uint8Array} The decoded address corresponding to the input. + */ + public static stringToAddress = (encoded: string): Uint8Array => { + if (RawAddress.constants.sizes.addressEncoded !== encoded.length) { + throw Error(`${encoded} does not represent a valid encoded address`); + } + + return Base32.Base32Decode(encoded); + } + + /** + * Format a namespaceId *alias* into a valid recipient field value. + * @param {Uint8Array} namespaceId The namespaceId + * @returns {Uint8Array} The padded notation of the alias + */ + public static aliasToRecipient = (namespaceId: Uint8Array): Uint8Array => { + // 0x91 | namespaceId on 8 bytes | 16 bytes 0-pad = 25 bytes + const padded = new Uint8Array(1 + 8 + 16); + padded.set([0x91], 0); + padded.set(namespaceId.reverse(), 1); + padded.set(Convert.hexToUint8('00'.repeat(16)), 9); + return padded; + } + + /** + * Converts a decoded address to an encoded address string. + * @param {Uint8Array} decoded The decoded address. + * @returns {string} The encoded address string corresponding to the input. + */ + public static addressToString = (decoded: Uint8Array): string => { + if (RawAddress.constants.sizes.addressDecoded !== decoded.length) { + throw Error(`${Convert.uint8ToHex(decoded)} does not represent a valid decoded address`); + } + + return Base32.Base32Encode(decoded); + } + + /** + * Converts a public key to a decoded address for a specific network. + * @param {Uint8Array} publicKey The public key. + * @param {number} networkIdentifier The network identifier. + * @returns {Uint8Array} The decoded address corresponding to the inputs. + */ + public static publicKeyToAddress = (publicKey: Uint8Array, networkIdentifier: number): Uint8Array => { + // step 1: sha3 hash of the public key + const publicKeyHash = (sha3_256 as any).arrayBuffer(publicKey); + + // step 2: ripemd160 hash of (1) + const ripemdHash = new RIPEMD160().update(new Buffer(publicKeyHash)).digest(); + + // step 3: add network identifier byte in front of (2) + const decodedAddress = new Uint8Array(RawAddress.constants.sizes.addressDecoded); + decodedAddress[0] = networkIdentifier; + RawArray.copy(decodedAddress, ripemdHash, RawAddress.constants.sizes.ripemd160, 1); + + // step 4: concatenate (3) and the checksum of (3) + const hash = (sha3_256 as any).arrayBuffer(decodedAddress.subarray(0, RawAddress.constants.sizes.ripemd160 + 1)); + RawArray.copy(decodedAddress, RawArray.uint8View(hash), + RawAddress.constants.sizes.checksum, RawAddress.constants.sizes.ripemd160 + 1); + + return decodedAddress; + } + + /** + * Determines the validity of a decoded address. + * @param {Uint8Array} decoded The decoded address. + * @returns {boolean} true if the decoded address is valid, false otherwise. + */ + public static isValidAddress = (decoded: Uint8Array): boolean => { + const hash = sha3_256.create(); + const checksumBegin = RawAddress.constants.sizes.addressDecoded - RawAddress.constants.sizes.checksum; + hash.update(decoded.subarray(0, checksumBegin)); + const checksum = new Uint8Array(RawAddress.constants.sizes.checksum); + RawArray.copy(checksum, RawArray.uint8View(hash.arrayBuffer()), RawAddress.constants.sizes.checksum); + return RawArray.deepEqual(checksum, decoded.subarray(checksumBegin)); + } + + /** + * Determines the validity of an encoded address string. + * @param {string} encoded The encoded address string. + * @returns {boolean} true if the encoded address string is valid, false otherwise. + */ + public static isValidEncodedAddress = (encoded: string): boolean => { + if (RawAddress.constants.sizes.addressEncoded !== encoded.length) { + return false; + } + + try { + const decoded = RawAddress.stringToAddress(encoded); + return RawAddress.isValidAddress(decoded); + } catch (err) { + return false; + } + } +} diff --git a/src/core/format/RawArray.ts b/src/core/format/RawArray.ts new file mode 100644 index 0000000000..d1c0bdad81 --- /dev/null +++ b/src/core/format/RawArray.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class RawArray { + /** + * Creates a Uint8Array view on top of input. + * @param {ArrayBuffer|Uint8Array} input The input array. + * @returns {Uint8Array} A Uint8Array view on top of input. + */ + public static uint8View = (input) => { + if (ArrayBuffer === input.constructor) { + return new Uint8Array(input); + } else if (Uint8Array === input.constructor) { + return input; + } + + throw Error('unsupported type passed to uint8View'); + } + + /** + * Copies elements from a source array to a destination array. + * @param {Array} dest The destination array. + * @param {Array} src The source array. + * @param {number} [numElementsToCopy=undefined] The number of elements to copy. + * @param {number} [destOffset=0] The first index of the destination to write. + * @param {number} [srcOffset=0] The first index of the source to read. + */ + public static copy = (dest, src, numElementsToCopy?, destOffset = 0, srcOffset = 0) => { + const length = undefined === numElementsToCopy ? dest.length : numElementsToCopy; + for (let i = 0; i < length; ++i) { + dest[destOffset + i] = src[srcOffset + i]; + } + } + + /** + * Determines whether or not an array is zero-filled. + * @param {Array} array The array to check. + * @returns {boolean} true if the array is zero-filled, false otherwise. + */ + public static isZeroFilled = (array) => array.every(value => 0 === value); + + /** + * Deeply checks the equality of two arrays. + * @param {Array} lhs First array to compare. + * @param {Array} rhs Second array to compare. + * @param {number} [numElementsToCompare=undefined] The number of elements to compare. + * @returns {boolean} true if all compared elements are equal, false otherwise. + */ + public static deepEqual = (lhs, rhs, numElementsToCompare?) => { + let length = numElementsToCompare; + if (undefined === length) { + if (lhs.length !== rhs.length) { + return false; + } + + length = lhs.length; + } + + if (length > lhs.length || length > rhs.length) { + return false; + } + + for (let i = 0; i < length; ++i) { + if (lhs[i] !== rhs[i]) { + return false; + } + } + + return true; + } +} diff --git a/src/core/format/RawUInt64.ts b/src/core/format/RawUInt64.ts new file mode 100644 index 0000000000..81eaf3bf1e --- /dev/null +++ b/src/core/format/RawUInt64.ts @@ -0,0 +1,116 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Convert } from './Convert'; + +export class RawUInt64 { + static readonly readUint32At = (bytes, i) => (bytes[i] + (bytes[i + 1] << 8) + (bytes[i + 2] << 16) + (bytes[i + 3] << 24)) >>> 0; + + /** + * An exact uint64 representation composed of two 32bit values. + * @typedef {Array} uint64 + * @property {number} 0 The low 32bit value. + * @property {number} 1 The high 32bit value. + */ + /** + * Tries to compact a uint64 into a simple numeric. + * @param {module:coders/uint64~uint64} uint64 A uint64 value. + * @returns {number|module:coders/uint64~uint64} + * A numeric if the uint64 is no greater than Number.MAX_SAFE_INTEGER or the original uint64 value otherwise. + */ + public static compact = (uint64) => { + const low = uint64[0]; + const high = uint64[1]; + + // don't compact if the value is >= 2^53 + if (0x00200000 <= high) { + return uint64; + } + + // multiply because javascript bit operations operate on 32bit values + return (high * 0x100000000) + low; + } + + /** + * Converts a numeric unsigned integer into a uint64. + * @param {number} number The unsigned integer. + * @returns {module:coders/uint64~uint64} The uint64 representation of the input. + */ + public static fromUint = (number) => { + const value = [(number & 0xFFFFFFFF) >>> 0, (number / 0x100000000) >>> 0]; + return value; + } + + /** + * Converts a (64bit) uint8 array into a uint64. + * @param {Uint8Array} uint8Array A uint8 array. + * @returns {module:coders/uint64~uint64} The uint64 representation of the input. + */ + public static fromBytes = (uint8Array) => { + if (8 !== uint8Array.length) { + throw Error(`byte array has unexpected size '${uint8Array.length}'`); + } + return [RawUInt64.readUint32At(uint8Array, 0), RawUInt64.readUint32At(uint8Array, 4)]; + } + + /** + * Converts a (32bit) uint8 array into a uint64. + * @param {Uint8Array} uint8Array A uint8 array. + * @returns {module:coders/uint64~uint64} The uint64 representation of the input. + */ + public static fromBytes32 = (uint8Array) => { + if (4 !== uint8Array.length) { + throw Error(`byte array has unexpected size '${uint8Array.length}'`); + } + return [RawUInt64.readUint32At(uint8Array, 0), 0]; + } + + /** + * Parses a hex string into a uint64. + * @param {string} input A hex encoded string. + * @returns {module:coders/uint64~uint64} The uint64 representation of the input. + */ + public static fromHex = (input) => { + if (16 !== input.length) { + throw Error(`hex string has unexpected size '${input.length}'`); + } + let hexString = input; + if (16 > hexString.length) { + hexString = '0'.repeat(16 - hexString.length) + hexString; + } + const uint8Array = Convert.hexToUint8(hexString); + const view = new DataView(uint8Array.buffer); + return [view.getUint32(4), view.getUint32(0)]; + } + + /** + * Converts a uint64 into a hex string. + * @param {module:coders/uint64~uint64} uint64 A uint64 value. + * @returns {string} A hex encoded string representing the uint64. + */ + public static toHex = (uint64) => { + const uint32Array = new Uint32Array(uint64); + const uint8Array = Convert.uint32ToUint8(uint32Array).reverse(); + return Convert.uint8ToHex(uint8Array); + } + + /** + * Returns true if a uint64 is zero. + * @param {module:coders/uint64~uint64} uint64 A uint64 value. + * @returns {boolean} true if the value is zero. + */ + public static isZero = (uint64) => 0 === uint64[0] && 0 === uint64[1]; +} diff --git a/src/core/format/Utilities.ts b/src/core/format/Utilities.ts new file mode 100644 index 0000000000..fe0fc1b401 --- /dev/null +++ b/src/core/format/Utilities.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {sha3_256} from 'js-sha3'; + +export const createBuilder = () => { + const map = {}; + return { + map, + /** + * Adds a range mapping to the map. + * @param {string} start The start character. + * @param {string} end The end character. + * @param {number} base The value corresponding to the start character. + * @memberof module:utils/charMapping~CharacterMapBuilder + * @instance + */ + addRange: (start, end, base) => { + const startCode = start.charCodeAt(0); + const endCode = end.charCodeAt(0); + + for (let code = startCode; code <= endCode; ++code) { + map[String.fromCharCode(code)] = code - startCode + base; + } + }, + }; +}; + +const Char_To_Nibble_Map = () => { + const builder = createBuilder(); + builder.addRange('0', '9', 0); + builder.addRange('a', 'f', 10); + builder.addRange('A', 'F', 10); + return builder.map; +}; + +const Char_To_Digit_Map = () => { + const builder = createBuilder(); + builder.addRange('0', '9', 0); + return builder.map; +}; + +export const Nibble_To_Char_Map = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; +export const Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; +export const Decoded_Block_Size = 5; +export const Encoded_Block_Size = 8; +export const tryParseByte = (char1, char2) => { + const charMap = Char_To_Nibble_Map(); + const nibble1 = charMap[char1]; + const nibble2 = charMap[char2]; + return undefined === nibble1 || undefined === nibble2 ? + undefined : + (nibble1 << 4) | nibble2; +}; + +/** + * Tries to parse a string representing an unsigned integer. + * @param {string} str The string to parse. + * @returns {number} The number represented by the input or undefined. + */ +export const tryParseUint = (str) => { + if ('0' === str) { + return 0; + } + let value = 0; + for (const char of str) { + const charMap = Char_To_Digit_Map(); + const digit = charMap[char]; + if (undefined === digit || (0 === value && 0 === digit)) { + return undefined; + } + + value *= 10; + value += digit; + + if (value > Number.MAX_SAFE_INTEGER) { + return undefined; + } + } + return value; +}; + +export const idGeneratorConst = { + namespace_base_id: [0, 0], + namespace_max_depth: 3, + name_pattern: /^[a-z0-9][a-z0-9-_]*$/, +}; + +export const throwInvalidFqn = (reason, name) => { + throw Error(`fully qualified id is invalid due to ${reason} (${name})`); +}; + +export const extractPartName = (name, start, size) => { + if (0 === size) { + this.throwInvalidFqn('empty part', name); + } + const partName = name.substr(start, size); + if (!idGeneratorConst.name_pattern.test(partName)) { + this.throwInvalidFqn(`invalid part name [${partName}]`, name); + } + return partName; +}; + +export const append = (path, id, name) => { + if (idGeneratorConst.namespace_max_depth === path.length) { + this.throwInvalidFqn('too many parts', name); + } + path.push(id); +}; + +export const split = (name, processor) => { + let start = 0; + for (let index = 0; index < name.length; ++index) { + if ('.' === name[index]) { + processor(start, index - start); + start = index + 1; + } + } + return start; +}; + +export const generateNamespaceId = (parentId, name) => { + const hash = sha3_256.create(); + hash.update(Uint32Array.from(parentId).buffer); + hash.update(name); + const result = new Uint32Array(hash.arrayBuffer()); + // right zero-filling required to keep unsigned number representation + return [result[0], (result[1] | 0x80000000) >>> 0]; +}; + +export const encodeBlock = (input, inputOffset, output, outputOffset) => { + output[outputOffset + 0] = Alphabet[input[inputOffset + 0] >> 3]; + output[outputOffset + 1] = Alphabet[((input[inputOffset + 0] & 0x07) << 2) | (input[inputOffset + 1] >> 6)]; + output[outputOffset + 2] = Alphabet[(input[inputOffset + 1] & 0x3E) >> 1]; + output[outputOffset + 3] = Alphabet[((input[inputOffset + 1] & 0x01) << 4) | (input[inputOffset + 2] >> 4)]; + output[outputOffset + 4] = Alphabet[((input[inputOffset + 2] & 0x0F) << 1) | (input[inputOffset + 3] >> 7)]; + output[outputOffset + 5] = Alphabet[(input[inputOffset + 3] & 0x7F) >> 2]; + output[outputOffset + 6] = Alphabet[((input[inputOffset + 3] & 0x03) << 3) | (input[inputOffset + 4] >> 5)]; + output[outputOffset + 7] = Alphabet[input[inputOffset + 4] & 0x1F]; +}; + +export const Char_To_Decoded_Char_Map = () => { + const builder = this.createBuilder(); + builder.addRange('A', 'Z', 0); + builder.addRange('2', '7', 26); + return builder.map; +}; + +export const decodeChar = (c) => { + const charMap = Char_To_Decoded_Char_Map(); + const decodedChar = charMap[c]; + if (undefined !== decodedChar) { + return decodedChar; + } + throw Error(`illegal base32 character ${c}`); +}; + +export const decodeBlock = (input, inputOffset, output, outputOffset) => { + const bytes = new Uint8Array(this.Encoded_Block_Size); + for (let i = 0; i < this.Encoded_Block_Size; ++i) { + bytes[i] = decodeChar(input[inputOffset + i]); + } + + output[outputOffset + 0] = (bytes[0] << 3) | (bytes[1] >> 2); + output[outputOffset + 1] = ((bytes[1] & 0x03) << 6) | (bytes[2] << 1) | (bytes[3] >> 4); + output[outputOffset + 2] = ((bytes[3] & 0x0F) << 4) | (bytes[4] >> 1); + output[outputOffset + 3] = ((bytes[4] & 0x01) << 7) | (bytes[5] << 2) | (bytes[6] >> 3); + output[outputOffset + 4] = ((bytes[6] & 0x07) << 5) | bytes[7]; +}; diff --git a/src/core/format/index.ts b/src/core/format/index.ts new file mode 100644 index 0000000000..27fc8ea316 --- /dev/null +++ b/src/core/format/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './RawAddress'; +export * from './RawArray'; +export * from './Convert'; +export * from './IdGenerator'; +export * from './RawUInt64'; diff --git a/src/infrastructure/NamespaceHttp.ts b/src/infrastructure/NamespaceHttp.ts index 72db046c81..e741b61b70 100644 --- a/src/infrastructure/NamespaceHttp.ts +++ b/src/infrastructure/NamespaceHttp.ts @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {address as AddressLibrary, convert, NamespaceRoutesApi} from 'nem2-library'; +import {NamespaceRoutesApi} from 'nem2-library'; import {from as observableFrom, Observable} from 'rxjs'; import {map, mergeMap} from 'rxjs/operators'; +import {Convert as convert, RawAddress as AddressLibrary} from '../core/format'; import {Address} from '../model/account/Address'; import {PublicAccount} from '../model/account/PublicAccount'; import {MosaicId} from '../model/mosaic/MosaicId'; diff --git a/src/infrastructure/transaction/CreateTransactionFromDTO.ts b/src/infrastructure/transaction/CreateTransactionFromDTO.ts index df00cf8223..05ce4c157c 100644 --- a/src/infrastructure/transaction/CreateTransactionFromDTO.ts +++ b/src/infrastructure/transaction/CreateTransactionFromDTO.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {convert} from 'nem2-library'; -import {uint64 as UInt64Library} from 'nem2-library'; +import {Convert as convert} from '../../core/format'; +import {RawUInt64 as UInt64Library} from '../../core/format'; import {Address} from '../../model/account/Address'; import {PublicAccount} from '../../model/account/PublicAccount'; import {NetworkType} from '../../model/blockchain/NetworkType'; @@ -52,6 +52,7 @@ import {TransactionInfo} from '../../model/transaction/TransactionInfo'; import {TransactionType} from '../../model/transaction/TransactionType'; import {TransferTransaction} from '../../model/transaction/TransferTransaction'; import {UInt64} from '../../model/UInt64'; +import { RawUInt64 } from '../../core/format/RawUInt64'; /** * @internal diff --git a/src/infrastructure/transaction/CreateTransactionFromPayload.ts b/src/infrastructure/transaction/CreateTransactionFromPayload.ts index 52acb8144d..c042e778e7 100644 --- a/src/infrastructure/transaction/CreateTransactionFromPayload.ts +++ b/src/infrastructure/transaction/CreateTransactionFromPayload.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { convert } from 'nem2-library'; +import { Convert as convert } from '../../core/format'; import {decode} from 'utf8'; import { Address } from '../../model/account/Address'; import { PublicAccount } from '../../model/account/PublicAccount'; diff --git a/src/model/UInt64.ts b/src/model/UInt64.ts index 978eec992e..61048f0170 100644 --- a/src/model/UInt64.ts +++ b/src/model/UInt64.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {uint64} from 'nem2-library'; +import { RawUInt64 as uint64 } from '../core/format'; /** * UInt64 data model diff --git a/src/model/account/Account.ts b/src/model/account/Account.ts index 728dfc12a7..e94f7ec420 100644 --- a/src/model/account/Account.ts +++ b/src/model/account/Account.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import {address as AddressLibrary, convert, KeyPair, nacl_catapult} from 'nem2-library'; +import {KeyPair, nacl_catapult} from 'nem2-library'; +import {Convert as convert, RawAddress as AddressLibrary} from '../../core/format'; import {NetworkType} from '../blockchain/NetworkType'; import {AggregateTransaction} from '../transaction/AggregateTransaction'; import {CosignatureSignedTransaction} from '../transaction/CosignatureSignedTransaction'; diff --git a/src/model/account/Address.ts b/src/model/account/Address.ts index db5ebeeac2..b3d7dbbc40 100644 --- a/src/model/account/Address.ts +++ b/src/model/account/Address.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {address as AddressLibrary, convert} from 'nem2-library'; +import { Convert as convert, RawAddress as AddressLibrary} from '../../core/format'; import {NetworkType} from '../blockchain/NetworkType'; /** diff --git a/src/model/account/PublicAccount.ts b/src/model/account/PublicAccount.ts index 8bc2967bcd..06b169e58e 100644 --- a/src/model/account/PublicAccount.ts +++ b/src/model/account/PublicAccount.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { convert, KeyPair } from 'nem2-library'; +import { KeyPair } from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import { NetworkType } from '../blockchain/NetworkType'; import { Address } from './Address'; diff --git a/src/model/mosaic/MosaicId.ts b/src/model/mosaic/MosaicId.ts index 42c0de97ee..3dc4db31bc 100644 --- a/src/model/mosaic/MosaicId.ts +++ b/src/model/mosaic/MosaicId.ts @@ -14,12 +14,9 @@ * limitations under the License. */ import { - convert, mosaicId as MosaicIdentifierGenerator, - nacl_catapult, - uint64 as uint64_t, } from 'nem2-library'; - +import { Convert as convert, RawUInt64 as uint64_t } from '../../core/format'; import {PublicAccount} from '../account/PublicAccount'; import {Id} from '../Id'; import {MosaicNonce} from '../mosaic/MosaicNonce'; diff --git a/src/model/mosaic/MosaicNonce.ts b/src/model/mosaic/MosaicNonce.ts index 319bf2a0eb..00985bd81a 100644 --- a/src/model/mosaic/MosaicNonce.ts +++ b/src/model/mosaic/MosaicNonce.ts @@ -14,10 +14,9 @@ * limitations under the License. */ import { - convert, nacl_catapult, } from 'nem2-library'; - +import { Convert as convert} from '../../core/format'; /** * The mosaic nonce structure * diff --git a/src/model/mosaic/NetworkHarvestMosaic.ts b/src/model/mosaic/NetworkHarvestMosaic.ts index d8f4359fb2..f9df23fb98 100644 --- a/src/model/mosaic/NetworkHarvestMosaic.ts +++ b/src/model/mosaic/NetworkHarvestMosaic.ts @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import {convert, mosaicId as MosaicIdGenerator} from 'nem2-library'; -import {PublicAccount} from '../account/PublicAccount'; import {NamespaceId} from '../namespace/NamespaceId'; import {UInt64} from '../UInt64'; import {Mosaic} from './Mosaic'; diff --git a/src/model/namespace/AddressAlias.ts b/src/model/namespace/AddressAlias.ts index 5e5c091513..de1aade68c 100644 --- a/src/model/namespace/AddressAlias.ts +++ b/src/model/namespace/AddressAlias.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {namespaceId as NamespaceIdGenerator} from 'nem2-library'; import {Address} from '../account/Address'; import {Alias} from './Alias'; diff --git a/src/model/namespace/Alias.ts b/src/model/namespace/Alias.ts index 5f2e56b179..b05262610a 100644 --- a/src/model/namespace/Alias.ts +++ b/src/model/namespace/Alias.ts @@ -13,12 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {namespaceId as NamespaceIdGenerator} from 'nem2-library'; import {Address} from '../account/Address'; import {MosaicId} from '../mosaic/MosaicId'; -import {AddressAlias} from './AddressAlias'; -import {AliasType} from './AliasType'; -import {MosaicAlias} from './MosaicAlias'; /** * The alias structure defines an interface for Aliases diff --git a/src/model/namespace/EmptyAlias.ts b/src/model/namespace/EmptyAlias.ts index ace9a03254..35e625f63e 100644 --- a/src/model/namespace/EmptyAlias.ts +++ b/src/model/namespace/EmptyAlias.ts @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {namespaceId as NamespaceIdGenerator} from 'nem2-library'; -import {Address} from '../account/Address'; import {Alias} from './Alias'; /** diff --git a/src/model/namespace/MosaicAlias.ts b/src/model/namespace/MosaicAlias.ts index fa8048ad04..c49640016c 100644 --- a/src/model/namespace/MosaicAlias.ts +++ b/src/model/namespace/MosaicAlias.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {namespaceId as NamespaceIdGenerator} from 'nem2-library'; import {MosaicId} from '../mosaic/MosaicId'; import {Alias} from './Alias'; diff --git a/src/model/namespace/NamespaceId.ts b/src/model/namespace/NamespaceId.ts index 4b8f956cba..40f5316d34 100644 --- a/src/model/namespace/NamespaceId.ts +++ b/src/model/namespace/NamespaceId.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {convert, namespaceId as NamespaceIdGenerator} from 'nem2-library'; +import {namespaceId as NamespaceIdGenerator} from 'nem2-library'; +import {Convert as convert} from '../../core/format'; import {Id} from '../Id'; /** diff --git a/src/model/transaction/AliasTransaction.ts b/src/model/transaction/AliasTransaction.ts index 579f9d4969..2333bd04be 100644 --- a/src/model/transaction/AliasTransaction.ts +++ b/src/model/transaction/AliasTransaction.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { MosaicSupplyChangeTransaction as MosaicSupplyChangeTransactionLibrary, VerifiableTransaction } from 'nem2-library'; import { Address } from '../account/Address'; -import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { AliasActionType } from '../namespace/AliasActionType'; @@ -26,9 +24,6 @@ import { AddressAliasTransaction } from './AddressAliasTransaction'; import { Deadline } from './Deadline'; import { MosaicAliasTransaction } from './MosaicAliasTransaction'; import { Transaction } from './Transaction'; -import { TransactionInfo } from './TransactionInfo'; -import { TransactionType } from './TransactionType'; -import { TransactionVersion } from './TransactionVersion'; export abstract class AliasTransaction extends Transaction { diff --git a/src/model/transaction/HashType.ts b/src/model/transaction/HashType.ts index df42501603..6219e7cb05 100644 --- a/src/model/transaction/HashType.ts +++ b/src/model/transaction/HashType.ts @@ -21,7 +21,7 @@ * 2: Op_Hash_160 (first with SHA-256 and then with RIPEMD-160 (BTC compatibility)) * 3: Op_Hash_256: input is hashed twice with SHA-256 (BTC compatibility) */ -import {convert} from 'nem2-library'; +import {Convert as convert} from '../../core/format'; export enum HashType { Op_Sha3_256 = 0, diff --git a/src/model/transaction/RegisterNamespaceTransaction.ts b/src/model/transaction/RegisterNamespaceTransaction.ts index 5cbabf32e3..09e64be488 100644 --- a/src/model/transaction/RegisterNamespaceTransaction.ts +++ b/src/model/transaction/RegisterNamespaceTransaction.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { convert, NamespaceCreationTransaction as RegisterNamespaceTransactionLibrary, subnamespaceNamespaceId, subnamespaceParentId, namespaceId, VerifiableTransaction } from 'nem2-library'; +import { NamespaceCreationTransaction as RegisterNamespaceTransactionLibrary, subnamespaceNamespaceId, subnamespaceParentId, namespaceId, VerifiableTransaction } from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { NamespaceId } from '../namespace/NamespaceId'; diff --git a/src/model/transaction/SecretLockTransaction.ts b/src/model/transaction/SecretLockTransaction.ts index 2d66cd3612..d27418ebb0 100644 --- a/src/model/transaction/SecretLockTransaction.ts +++ b/src/model/transaction/SecretLockTransaction.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { convert, SecretLockTransaction as SecretLockTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { SecretLockTransaction as SecretLockTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; diff --git a/src/model/transaction/SecretProofTransaction.ts b/src/model/transaction/SecretProofTransaction.ts index 644e7fe023..d05635f2d1 100644 --- a/src/model/transaction/SecretProofTransaction.ts +++ b/src/model/transaction/SecretProofTransaction.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { convert, SecretProofTransaction as SecretProofTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { SecretProofTransaction as SecretProofTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; diff --git a/src/model/transaction/TransferTransaction.ts b/src/model/transaction/TransferTransaction.ts index 5994c63f81..ddf7b7fca2 100644 --- a/src/model/transaction/TransferTransaction.ts +++ b/src/model/transaction/TransferTransaction.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { convert, TransferTransaction as TransferTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { TransferTransaction as TransferTransactionLibrary, VerifiableTransaction } from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; diff --git a/src/model/wallet/SimpleWallet.ts b/src/model/wallet/SimpleWallet.ts index 3a130134f4..2797c6a3cb 100644 --- a/src/model/wallet/SimpleWallet.ts +++ b/src/model/wallet/SimpleWallet.ts @@ -15,7 +15,8 @@ */ import {LocalDateTime} from 'js-joda'; -import {convert, crypto, KeyPair, nacl_catapult} from 'nem2-library'; +import {crypto, KeyPair, nacl_catapult} from 'nem2-library'; +import { Convert as convert} from '../../core/format'; import {Account} from '../account/Account'; import {Address} from '../account/Address'; import {NetworkType} from '../blockchain/NetworkType'; diff --git a/test/core/format/Base32.spec.ts b/test/core/format/Base32.spec.ts new file mode 100644 index 0000000000..ff414f02f9 --- /dev/null +++ b/test/core/format/Base32.spec.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import {Convert as convert} from '../../../src/core/format'; +import {Base32 as base32} from '../../../src/core/format/Base32'; + +describe('base32', () => { + const Test_Vectors = [{ + decoded: '68BA9E8D1AA4502E1F73DA19784B5D7DA16CA1E4AF895FAC12', + encoded: 'NC5J5DI2URIC4H3T3IMXQS25PWQWZIPEV6EV7LAS' + }, + { + decoded: '684C2605E5B366BB94BC30755EC9F50D74E80FC9283D20E283', + encoded: 'NBGCMBPFWNTLXFF4GB2V5SPVBV2OQD6JFA6SBYUD' + }, + { + decoded: '68D7B09A14BEA7CE060E71C0FA9AC9B4226DE167013DE10B3D', + encoded: 'NDL3BGQUX2T44BQOOHAPVGWJWQRG3YLHAE66CCZ5' + }, + { + decoded: '686C44C024F1089669F53C45AC6D62CC17A0D9CBA67A6205E6', + encoded: 'NBWEJQBE6EEJM2PVHRC2Y3LCZQL2BWOLUZ5GEBPG' + }, + { + decoded: '98A0FE84BBFC5EEE7CADC2B12F790DAA4A7A9505096E674FAB', + encoded: 'TCQP5BF37RPO47FNYKYS66INVJFHVFIFBFXGOT5L' + } + ]; + + describe('encode', () => { + it('can convert empty input', () => { + // Act: + const encoded = base32.Base32Encode(new Uint8Array([])); + + // Assert: + expect(encoded).to.equal(''); + }); + + it('can convert test vectors', () => { + // Arrange: + for (const sample of Test_Vectors) { + const input = convert.hexToUint8(sample.decoded); + + // Act: + const encoded = base32.Base32Encode(input); + + // Assert: + expect(encoded, `input ${sample.decoded}`).to.equal(sample.encoded); + } + }); + + it('accepts all byte values', () => { + // Arrange: + const data: any = []; + for (let i = 0; 260 > i; ++i) { + data.push(i & 0xFF); + } + + // Act: + const encoded = base32.Base32Encode(data); + + // Assert: + const expected = + 'AAAQEAYEAUDAOCAJBIFQYDIOB4IBCEQTCQKRMFYY' + + 'DENBWHA5DYPSAIJCEMSCKJRHFAUSUKZMFUXC6MBR' + + 'GIZTINJWG44DSOR3HQ6T4P2AIFBEGRCFIZDUQSKK' + + 'JNGE2TSPKBIVEU2UKVLFOWCZLJNVYXK6L5QGCYTD' + + 'MRSWMZ3INFVGW3DNNZXXA4LSON2HK5TXPB4XU634' + + 'PV7H7AEBQKBYJBMGQ6EITCULRSGY5D4QSGJJHFEV' + + 'S2LZRGM2TOOJ3HU7UCQ2FI5EUWTKPKFJVKV2ZLNO' + + 'V6YLDMVTWS23NN5YXG5LXPF5X274BQOCYPCMLRWH' + + 'ZDE4VS6MZXHM7UGR2LJ5JVOW27MNTWW33TO55X7A' + + '4HROHZHF43T6R2PK5PWO33XP6DY7F47U6X3PP6HZ' + + '7L57Z7P674AACAQD'; + expect(encoded).to.equal(expected); + }); + + it('throws if input size is not a multiple of block size', () => { + // Arrange: + for (let i = 2; 10 > i; i += 2) { + const input = new Uint8Array(i); + + // Act + Assert: + expect(() => { + base32.Base32Encode(input); + }, `input at ${i}`).to.throw('decoded size must be multiple of 5'); + } + }); + }); + + describe('decode', () => { + it('can convert empty input', () => { + // Act: + const decoded = base32.Base32Decode(''); + + // Assert: + expect(convert.uint8ToHex(decoded)).to.equal(''); + }); + + it('can convert test vectors', () => { + // Arrange: + for (const sample of Test_Vectors) { + // Act: + const decoded = base32.Base32Decode(sample.encoded); + + // Assert: + expect(convert.uint8ToHex(decoded), `input ${sample.encoded}`).to.equal(sample.decoded); + } + }); + + it('accepts all valid characters', () => { + // Act: + const decoded = base32.Base32Decode('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); + + // Assert: + expect(convert.uint8ToHex(decoded)).to.equal('00443214C74254B635CF84653A56D7C675BE77DF'); + }); + + it('throws if input size is not a multiple of block size', () => { + // Arrange: + for (let i = 1; 8 > i; ++i) { + const input = 'A'.repeat(i); + + // Act + Assert: + expect(() => { + base32.Base32Decode(input); + }, `input at ${i}`).to.throw('encoded size must be multiple of 8'); + } + }); + + it('throws if input contains an invalid character', () => { + // Arrange: + const illegalInputs = [ + 'NC5J5DI2URIC4H3T3IMXQS21PWQWZIPEV6EV7LAS', // contains char '1' + 'NBGCMBPFWNTLXFF4GB2V5SPV!V2OQD6JFA6SBYUD', // contains char '!' + 'NDL3BGQUX2T44BQOOHAPVGWJWQRG3YLHAE)6CCZ5' // contains char ')' + ]; + + // Act + Assert: + for (const input of illegalInputs) { + expect(() => { + base32.Base32Decode(input); + }, `input ${input}`).to.throw('illegal base32 character'); + } + }); + }); + + describe('roundtrip', () => { + it('decode -> encode', () => { + // Arrange: inputs + const inputs = ['BDS73DQ5NC33MKYI3K6GXLJ53C2HJ35A', '46FNYP7T4DD3SWAO6C4NX62FJI5CBA26']; + for (const input of inputs) { + // Act: + const decoded = base32.Base32Decode(input); + const result = base32.Base32Encode(decoded); + + // Assert: + expect(result, `input ${input}`).to.equal(input); + } + }); + + it('encode -> decode', () => { + // Arrange: inputs + const inputs = ['8A4E7DF5B61CC0F97ED572A95F6ACA', '2D96E4ABB65F0AD3C29FEA48C132CE']; + for (const input of inputs) { + // Act: + const encoded = base32.Base32Encode(convert.hexToUint8(input)); + const result = base32.Base32Decode(encoded); + + // Assert: + expect(convert.uint8ToHex(result), `input ${input}`).to.equal(input); + } + }); + }); +}); diff --git a/test/core/format/Convert.spec.ts b/test/core/format/Convert.spec.ts new file mode 100644 index 0000000000..d40aa5b8b0 --- /dev/null +++ b/test/core/format/Convert.spec.ts @@ -0,0 +1,352 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import {Convert as convert} from '../../../src/core/format'; + +describe('convert', () => { + describe('toByte', () => { + it('can convert all valid hex char combinations to byte', () => { + // Arrange: + const charToValueMappings: any = []; + for (let code = '0'.charCodeAt(0); code <= '9'.charCodeAt(0); ++code) { + charToValueMappings.push([String.fromCharCode(code), code - '0'.charCodeAt(0)]); + } + for (let code = 'a'.charCodeAt(0); code <= 'f'.charCodeAt(0); ++code) { + charToValueMappings.push([String.fromCharCode(code), code - 'a'.charCodeAt(0) + 10]); + } + for (let code = 'A'.charCodeAt(0); code <= 'F'.charCodeAt(0); ++code) { + charToValueMappings.push([String.fromCharCode(code), code - 'A'.charCodeAt(0) + 10]); + } + + // Act: + let numTests = 0; + charToValueMappings.forEach((pair1) => { + charToValueMappings.forEach((pair2) => { + // Act: + const byte = convert.toByte(pair1[0], pair2[0]); + + // Assert: + const expected = (pair1[1] * 16) + pair2[1]; + expect(byte, `input: ${pair1[0]}${pair2[0]}`).to.equal(expected); + ++numTests; + }); + }); + + // Sanity: + expect(numTests).to.equal(22 * 22); + }); + + it('cannot convert invalid hex chars to byte', () => { + // Arrange: + const pairs = [ + ['G', '6'], + ['7', 'g'], + ['*', '8'], + ['9', '!'], + ]; + + // Act: + pairs.forEach((pair) => { + // Assert: + const message = `input: ${pair[0]}${pair[0]}`; + expect(() => { + convert.toByte(pair[0], pair[1]); + }, message).to.throw('unrecognized hex char'); + }); + }); + }); + + describe('isHexString', () => { + it('returns true for valid hex strings', () => { + // Arrange: + const inputs = [ + '', + '026ee415fc15', + 'abcdef0123456789ABCDEF', + ]; + + // Act: + for (const input of inputs) { + const isHexString = convert.isHexString(input); + + // Assert: + expect(isHexString, `input ${input}`).to.equal(true); + } + }); + + it('returns false for invalid hex strings', () => { + // Arrange: + const inputs = [ + 'abcdef012345G789ABCDEF', // invalid ('G') char + 'abcdef0123456789ABCDE', // invalid (odd) length + ]; + + // Act: + for (const input of inputs) { + const isHexString = convert.isHexString(input); + + // Assert: + expect(isHexString, `input ${input}`).to.equal(false); + } + }); + }); + + describe('hexToUint8', () => { + it('can parse empty hex string into array', () => { + // Act: + const actual = convert.hexToUint8(''); + + // Assert: + const expected = Uint8Array.of(); + expect(actual).to.deep.equal(expected); + }); + + it('can parse valid hex string into array', () => { + // Act: + const actual = convert.hexToUint8('026ee415fc15'); + + // Assert: + const expected = Uint8Array.of(0x02, 0x6E, 0xE4, 0x15, 0xFC, 0x15); + expect(actual).to.deep.equal(expected); + }); + + it('can parse valid hex string containing all valid hex characters into array', () => { + // Act: + const actual = convert.hexToUint8('abcdef0123456789ABCDEF'); + + // Assert: + const expected = Uint8Array.of(0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF); + expect(actual).to.deep.equal(expected); + }); + + it('cannot parse hex string with invalid characters into array', () => { + // Assert: + expect(() => { + convert.hexToUint8('abcdef012345G789ABCDEF'); + }).to.throw('unrecognized hex char'); + }); + + it('cannot parse hex string with invalid size into array', () => { + // Assert: + expect(() => { + convert.hexToUint8('abcdef012345G789ABCDE'); + }).to.throw('hex string has unexpected size'); + }); + }); + + describe('uint8ToHex', () => { + it('can format empty array into hex string', () => { + // Act: + const actual = convert.uint8ToHex(Uint8Array.of()); + + // Assert: + expect(actual).to.equal(''); + }); + + it('can format single value array into hex string', () => { + // Act: + const actual = convert.uint8ToHex(Uint8Array.of(0xD2)); + + // Assert: + expect(actual).to.equal('D2'); + }); + + it('can format multi value array into hex string', () => { + // Act: + const actual = convert.uint8ToHex(Uint8Array.of(0x02, 0x6E, 0xE4, 0x15, 0xFC, 0x15)); + + // Assert: + expect(actual).to.equal('026EE415FC15'); + }); + }); + + describe('uint8ToUint32', () => { + it('uint8 array with zero length can be converted to uint32 array', () => { + // Act: + const actual = convert.uint8ToUint32(Uint8Array.of()); + + // Assert: + expect(actual).to.deep.equal(Uint32Array.of()); + }); + + it('uint8 array with length multiple of four can be converted to uint32 array', () => { + // Act: + const actual = convert.uint8ToUint32(Uint8Array.of(0x02, 0x6E, 0x89, 0xAB, 0xCD, 0xEF, 0xE4, 0x15)); + + // Assert: + expect(actual).to.deep.equal(Uint32Array.of(0xAB896E02, 0x15E4EFCD)); + }); + + it('uint8 array with length not multiple of four cannot be converted to uint32 array', () => { + // Assert: + expect(() => { + convert.uint8ToUint32(Uint8Array.of(0x02, 0x6E, 0xE4, 0x15, 0x15)); + }) + .to.throw('byte length of Uint32Array should be a multiple of 4'); + }); + }); + + describe('uint32ToUint8', () => { + it('uint32 array with zero length can be converted to uint8 array', () => { + // Act: + const actual = convert.uint32ToUint8(Uint32Array.of()); + + // Assert: + expect(actual).to.deep.equal(Uint8Array.of()); + }); + + it('uint32 array with nonzero length can be converted to uint8 array', () => { + // Act: + const actual = convert.uint32ToUint8(Uint32Array.of(0xAB896E02, 0x15E4EFCD)); + + // Assert: + expect(actual).to.deep.equal(Uint8Array.of(0x02, 0x6E, 0x89, 0xAB, 0xCD, 0xEF, 0xE4, 0x15)); + }); + }); + + describe('utf8ToHex', () => { + it('utf8 text to hex', () => { + // Act: + const actual = convert.utf8ToHex('test words |@#¢∞¬÷“”≠[]}{–'); + + // Assert: + expect(actual).to.equal('7465737420776f726473207c4023c2a2e2889ec2acc3b7e2809ce2809de289a05b5d7d7be28093'); + }); + + it('utf8 text to hex', () => { + // Act: + const actual = convert.utf8ToHex('先秦兩漢'); + + // Assert: + expect(actual).to.equal('e58588e7a7a6e585a9e6bca2'); + }); + }); + + describe('signed <-> unsigned byte', () => { + const testCases = [{ + signed: -128, + unsigned: 0x80, + description: 'min negative', + }, + { + signed: -127, + unsigned: 0x81, + description: 'min negative plus one', + }, + { + signed: -87, + unsigned: 0xA9, + description: 'negative', + }, + { + signed: -1, + unsigned: 0xFF, + description: 'negative one', + }, + { + signed: 0, + unsigned: 0, + description: 'zero', + }, + { + signed: 1, + unsigned: 0x01, + description: 'positive one', + }, + { + signed: 57, + unsigned: 0x39, + description: 'positive', + }, + { + signed: 126, + unsigned: 0x7E, + description: 'max positive minus one', + }, + { + signed: 127, + unsigned: 0x7F, + description: 'max positive', + }, + ]; + + describe('uint8ToInt8', () => { + const failureTestCases = [{ + input: 256, + description: 'one too large', + }, + { + input: 1000, + description: 'very large', + }, + ]; + + for (const testCase of failureTestCases) { + it(`cannot convert number that is ${testCase.description}`, () => { + // Assert: + expect(() => convert.uint8ToInt8(testCase.input)).to.throw(`input '${testCase.input}' is out of range`); + }); + } + + for (const testCase of testCases) { + it(`can convert ${testCase.description}`, () => { + // Act: + const value = convert.uint8ToInt8(testCase.unsigned); + + // Assert: + expect(value).to.equal(testCase.signed); + }); + } + }); + + describe('int8ToUint8', () => { + const failureTestCases = [{ + input: -1000, + description: 'very small', + }, + { + input: -129, + description: 'one too small', + }, + { + input: 128, + description: 'one too large', + }, + { + input: 1000, + description: 'very large', + }, + ]; + + for (const testCase of failureTestCases) { + it(`cannot convert number that is ${testCase.description}`, () => { + // Assert: + expect(() => convert.int8ToUint8(testCase.input)).to.throw(`input '${testCase.input}' is out of range`); + }); + } + + for (const testCase of testCases) { + it(`can convert ${testCase.description}`, () => { + // Act: + const value = convert.int8ToUint8(testCase.signed); + + // Assert: + expect(value).to.equal(testCase.unsigned); + }); + } + }); + }); +}); diff --git a/test/core/format/IdGenerator.spec.ts b/test/core/format/IdGenerator.spec.ts new file mode 100644 index 0000000000..32921fd12b --- /dev/null +++ b/test/core/format/IdGenerator.spec.ts @@ -0,0 +1,251 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import {sha3_256} from 'js-sha3'; +import { + Convert as convert, + IdGenerator as idGenerator, + RawUInt64 as uint64, +} from '../../../src/core/format'; + +const constants = { + nem_id: [0x375FFA4B, 0x84B3552D], + xem_id: [0xD95FCF29, 0xD525AD41], + namespace_base_id: [0, 0], +}; + +const basicMosaicInfo = { + nonce: [0x78, 0xE3, 0x6F, 0xB7], + publicId: [ + 0x4A, 0xFF, 0x7B, 0x4B, 0xA8, 0xC1, 0xC2, 0x6A, 0x79, 0x17, 0x57, 0x59, 0x93, 0x34, 0x66, 0x27, + 0xCB, 0x6C, 0x80, 0xDE, 0x62, 0xCD, 0x92, 0xF7, 0xF9, 0xAE, 0xDB, 0x70, 0x64, 0xA3, 0xDE, 0x62, + ], + id: [0xC0AFC518, 0x3AD842A8], +}; + +const mosaicTestVector = { + rows: [{ + publicKey: '4AFF7B4BA8C1C26A7917575993346627CB6C80DE62CD92F7F9AEDB7064A3DE62', + nonce: 'B76FE378', + expectedMosaicId: '3AD842A8C0AFC518', + }, + { + publicKey: '3811EDF245F1D30171FF1474B24C4366FECA365A8457AAFA084F3DE4AEA0BA60', + nonce: '21832A2A', + expectedMosaicId: '24C54740A9F3893F', + }, + { + publicKey: '3104D468D20491EC12C988C50CAD9282256052907415359201C46CBD7A0BCD75', + nonce: '2ADBB332', + expectedMosaicId: '43908F2DEEA04245', + }, + { + publicKey: '6648E16513F351E9907B0EA34377E25F579BE640D4698B28E06585A21E94CFE2', + nonce: 'B9175E0F', + expectedMosaicId: '183172772BD29E78', + }, + { + publicKey: '1C05C40D38463FE725CF0584A3A69E3B0D6B780196A88C50624E49B921EE1404', + nonce: 'F6077DDD', + expectedMosaicId: '423DB0B12F787422', + }, + { + publicKey: '37926B3509987093C776C8EA3E7F978E3A78142B5C96B9434C3376177DC65EFD', + nonce: '08190C6D', + expectedMosaicId: '1F07D26B6CD352D5', + }, + { + publicKey: 'FDC6B0D415D90536263431F05C46AC492D0BD9B3CFA1B79D5A35E0F371655C0C', + nonce: '81662AA5', + expectedMosaicId: '74511F54940729CB', + }, + { + publicKey: '2D4EA99965477AEB3BC162C09C24C8DA4DABE408956C2F69642554EA48AAE1B2', + nonce: 'EA16BF58', + expectedMosaicId: '4C55843B6EB4A5BD', + }, + { + publicKey: '68EB2F91E74D005A7C22D6132926AEF9BFD90A3ACA3C7F989E579A93EFF24D51', + nonce: 'E5F87A8B', + expectedMosaicId: '4D89DE2B6967666A', + }, + { + publicKey: '3B082C0074F65D1E205643CDE72C6B0A3D0579C7ACC4D6A7E23A6EC46363B90F', + nonce: '1E6BB49F', + expectedMosaicId: '0A96B3A44615B62F', + }, + { + publicKey: '81245CA233B729FAD1752662EADFD73C5033E3B918CE854E01F6EB51E98CD9F1', + nonce: 'B82965E3', + expectedMosaicId: '1D6D8E655A77C4E6', + }, + { + publicKey: 'D3A2C1BFD5D48239001174BFF62A83A52BC9A535B8CDBDF289203146661D8AC4', + nonce: 'F37FB460', + expectedMosaicId: '268A3CC23ADCDA2D', + }, + { + publicKey: '4C4CA89B7A31C42A7AB963B8AB9D85628BBB94735C999B2BD462001A002DBDF3', + nonce: 'FF6323B0', + expectedMosaicId: '51202B5C51F6A5A9', + }, + { + publicKey: '2F95D9DCD4F18206A54FA95BD138DA1C038CA82546525A8FCC330185DA0647DC', + nonce: '99674492', + expectedMosaicId: '5CE4E38B09F1423D', + }, + { + publicKey: 'A7892491F714B8A7469F763F695BDB0B3BF28D1CC6831D17E91F550A2D48BD12', + nonce: '55141880', + expectedMosaicId: '5EFD001B3350C9CB', + }, + { + publicKey: '68BBDDF5C08F54278DA516F0E4A5CCF795C10E2DE26CAF127FF4357DA7ACF686', + nonce: '11FA5BAF', + expectedMosaicId: '179F0CDD6D2CCA7B', + }, + { + publicKey: '014F6EF90792F814F6830D64017107534F5B718E2DD43C25ACAABBE347DEC81E', + nonce: '6CFBF7B3', + expectedMosaicId: '53095813DEB3D108', + }, + { + publicKey: '95A6344597E0412C51B3559F58F564F9C2DE3101E5CC1DD8B115A93CE7040A71', + nonce: '905EADFE', + expectedMosaicId: '3551C4B12DDF067D', + }, + { + publicKey: '0D7DDFEB652E8B65915EA734420A1233A233119BF1B0D41E1D5118CDD44447EE', + nonce: '61F5B671', + expectedMosaicId: '696E2FB0682D3199', + }, + { + publicKey: 'FFD781A20B01D0C999AABC337B8BAE82D1E7929A9DD77CC1A71E4B99C0749684', + nonce: 'D8542F1A', + expectedMosaicId: '6C55E05D11D19FBD', + }, + ], +}; + +describe('id generator', () => { + function generateNamespaceId(parentId, name) { + const hash = sha3_256.create(); + hash.update(Uint32Array.from(parentId).buffer); + hash.update(name); + const result = new Uint32Array(hash.arrayBuffer()); + // right zero-filling required to keep unsigned number representation + return [result[0], (result[1] | 0x80000000) >>> 0]; + } + + function addBasicTests(generator) { + it('produces different results for different names', () => { + // Assert: + ['bloodyrookie.alice', 'nem.mex', 'bloodyrookie.xem', 'bloody_rookie.xem'].forEach((name) => + expect(generator(name), `nem.xem vs ${name}`).to.not.equal(generator('nem.xem'))); + }); + + it('rejects names with uppercase characters', () => { + // Assert: + ['NEM.xem', 'NEM.XEM', 'nem.XEM', 'nEm.XeM', 'NeM.xEm'].forEach((name) => + expect(() => generator(name), `name ${name}`).to.throw('invalid part name')); + }); + + it('rejects improper qualified names', () => { + // Assert: + ['.', '..', '...', '.a', 'b.', 'a..b', '.a.b', 'b.a.'].forEach((name) => + expect(() => generator(name), `name ${name}`).to.throw('empty part')); + }); + + it('rejects improper part names', () => { + // Assert: + ['alpha.bet@.zeta', 'a!pha.beta.zeta', 'alpha.beta.ze^a'].forEach((name) => + expect(() => generator(name), `name ${name}`).to.throw('invalid part name')); + }); + + it('rejects empty string', () => { + // Assert: + expect(() => generator(''), 'empty string').to.throw('having zero length'); + }); + } + + describe('generate mosaic id', () => { + it('generates correct well known id', () => { + // Assert: + expect(idGenerator.generateMosaicId(basicMosaicInfo.nonce, basicMosaicInfo.publicId)) + .to.deep.equal(basicMosaicInfo.id); + }); + + // @dataProvider mosaicTestVector + it('generates correct mosaicId given nonce and public key', () => { + mosaicTestVector.rows.map((row, i) => { + const pubKey = convert.hexToUint8(row.publicKey); + const nonce = convert.hexToUint8(row.nonce).reverse(); // Little-Endianness! + const mosaicId = idGenerator.generateMosaicId(nonce, pubKey); + const expectedId = uint64.fromHex(row.expectedMosaicId); + + // Assert: + expect(mosaicId) + .to.deep.equal(expectedId); + }); + }); + }); + + describe('generate namespace paths', () => { + it('generates correct well known root path', () => { + // Act: + const path = idGenerator.generateNamespacePath('nem'); + + // Assert: + expect(path.length).to.equal(1); + expect(path[0]).to.deep.equal(constants.nem_id); + }); + + it('generates correct well known child path', () => { + // Act: + const path = idGenerator.generateNamespacePath('nem.xem'); + + // Assert: + expect(path.length).to.equal(2); + expect(path[0]).to.deep.equal(constants.nem_id); + expect(path[1]).to.deep.equal(constants.xem_id); + }); + + it('supports multi level namespaces', () => { + // Arrange: + const expected: any = []; + expected.push(generateNamespaceId(constants.namespace_base_id, 'foo')); + expected.push(generateNamespaceId(expected[0], 'bar')); + expected.push(generateNamespaceId(expected[1], 'baz')); + + // Assert: + expect(idGenerator.generateNamespacePath('foo.bar.baz')).to.deep.equal(expected); + }); + + it('rejects names with too many parts', () => { + // Assert: + ['a.b.c.d', 'a.b.c.d.e'].forEach((name) => + expect(() => idGenerator.generateNamespacePath(name), `name ${name}`).to.throw('too many parts')); + }); + + it('rejects improper qualified names', () => { + // Assert: + ['a:b:c', 'a::b'].forEach((name) => + expect(() => idGenerator.generateNamespacePath(name), `name ${name}`).to.throw('invalid part name')); + }); + + addBasicTests(idGenerator.generateNamespacePath); + }); +}); diff --git a/test/core/format/RawAddress.spec.ts b/test/core/format/RawAddress.spec.ts new file mode 100644 index 0000000000..c6580fb656 --- /dev/null +++ b/test/core/format/RawAddress.spec.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import { + Convert as convert, + RawAddress as address, +} from '../../../src/core/format'; + +const Address_Decoded_Size = 25; +const Network_Mijin_Identifier = 0x60; +const Network_Public_Test_Identifier = 0x98; + +describe('address', () => { + describe('stringToAddress', () => { + function assertCannotCreateAddress(encoded, message) { + // Assert: + expect(() => { + address.stringToAddress(encoded); + }).to.throw(message); + } + + it('can create address from valid encoded address', () => { + // Arrange: + const encoded = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFG'; + const expectedHex = '6823BB7C3C089D996585466380EDBDC19D4959184893E38CA6'; + + // Act: + const decoded = address.stringToAddress(encoded); + + // Assert: + expect(address.isValidAddress(decoded)).to.equal(true); + expect(convert.uint8ToHex(decoded)).to.equal(expectedHex); + }); + + it('cannot create address from encoded string with wrong length', () => { + // Assert: + assertCannotCreateAddress( + 'NC5J5DI2URIC4H3T3IMXQS25PWQWZIPEV6EV7LASABCDEFGH', + 'NC5J5DI2URIC4H3T3IMXQS25PWQWZIPEV6EV7LASABCDEFGH does not represent a valid encoded address' + ); + }); + + it('cannot create address from invalid encoded string', () => { + // Assert: + assertCannotCreateAddress('NC5(5DI2URIC4H3T3IMXQS25PWQWZIPEV6EV7LAS', 'illegal base32 character ('); + assertCannotCreateAddress('NC5J1DI2URIC4H3T3IMXQS25PWQWZIPEV6EV7LAS', 'illegal base32 character 1'); + assertCannotCreateAddress('NC5J5?I2URIC4H3T3IMXQS25PWQWZIPEV6EV7LAS', 'illegal base32 character ?'); + }); + }); + + describe('addressToString', () => { + it('can create encoded address from address', () => { + // Arrange: + const decodedHex = '6823BB7C3C089D996585466380EDBDC19D4959184893E38CA6'; + const expected = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFG'; + + // Act: + const encoded = address.addressToString(convert.hexToUint8(decodedHex)); + + // Assert: + expect(encoded).to.equal(expected); + }); + }); + + describe('publicKeyToAddress', () => { + it('can create address from public key for well known network', () => { + // Arrange: + const expectedHex = '6023BB7C3C089D996585466380EDBDC19D49591848B3727714'; + const publicKey = convert.hexToUint8('3485D98EFD7EB07ADAFCFD1A157D89DE2796A95E780813C0258AF3F5F84ED8CB'); + + // Act: + const decoded = address.publicKeyToAddress(publicKey, Network_Mijin_Identifier); + + // Assert: + expect(decoded[0]).to.equal(Network_Mijin_Identifier); + expect(address.isValidAddress(decoded)).to.equal(true); + expect(convert.uint8ToHex(decoded)).to.equal(expectedHex); + }); + + it('can create address from public key for custom network', () => { + // Arrange: + const expectedHex = '9823BB7C3C089D996585466380EDBDC19D495918484BF7E997'; + const publicKey = convert.hexToUint8('3485D98EFD7EB07ADAFCFD1A157D89DE2796A95E780813C0258AF3F5F84ED8CB'); + + // Act: + const decoded = address.publicKeyToAddress(publicKey, Network_Public_Test_Identifier); + + // Assert: + expect(decoded[0]).to.equal(Network_Public_Test_Identifier); + expect(address.isValidAddress(decoded)).to.equal(true); + expect(convert.uint8ToHex(decoded)).to.equal(expectedHex); + }); + + it('address calculation is deterministic', () => { + // Arrange: + const publicKey = convert.hexToUint8('3485D98EFD7EB07ADAFCFD1A157D89DE2796A95E780813C0258AF3F5F84ED8CB'); + + // Act: + const decoded1 = address.publicKeyToAddress(publicKey, Network_Mijin_Identifier); + const decoded2 = address.publicKeyToAddress(publicKey, Network_Mijin_Identifier); + + // Assert: + expect(address.isValidAddress(decoded1)).to.equal(true); + expect(decoded1).to.deep.equal(decoded2); + }); + + it('different public keys result in different addresses', () => { + // Arrange: + const publicKey1 = convert.hexToUint8('1464953393CE96A08ABA6184601FD08864E910696B060FF7064474726E666CA8'); + const publicKey2 = convert.hexToUint8('b4f12e7c9f6946091e2cb8b6d3a12b50d17ccbbf646386ea27ce2946a7423dcf'); + + // Act: + const decoded1 = address.publicKeyToAddress(publicKey1, Network_Mijin_Identifier); + const decoded2 = address.publicKeyToAddress(publicKey2, Network_Mijin_Identifier); + + // Assert: + expect(address.isValidAddress(decoded1)).to.equal(true); + expect(address.isValidAddress(decoded2)).to.equal(true); + expect(decoded1).to.not.deep.equal(decoded2); + }); + + it('different networks result in different addresses', () => { + // Arrange: + const publicKey = convert.hexToUint8('b4f12e7c9f6946091e2cb8b6d3a12b50d17ccbbf646386ea27ce2946a7423dcf'); + + // Act: + const decoded1 = address.publicKeyToAddress(publicKey, Network_Mijin_Identifier); + const decoded2 = address.publicKeyToAddress(publicKey, Network_Public_Test_Identifier); + + // Assert: + expect(address.isValidAddress(decoded1)).to.equal(true); + expect(address.isValidAddress(decoded2)).to.equal(true); + expect(decoded1).to.not.deep.equal(decoded2); + }); + }); + + describe('isValidAddress', () => { + it('returns true for valid address', () => { + // Arrange: + const validHex = '6823BB7C3C089D996585466380EDBDC19D4959184893E38CA6'; + const decoded = convert.hexToUint8(validHex); + + // Assert: + expect(address.isValidAddress(decoded)).to.equal(true); + }); + + it('returns false for address with invalid checksum', () => { + // Arrange: + const validHex = '6823BB7C3C089D996585466380EDBDC19D4959184893E38CA6'; + const decoded = convert.hexToUint8(validHex); + decoded[Address_Decoded_Size - 1] ^= 0xff; // ruin checksum + + // Assert: + expect(address.isValidAddress(decoded)).to.equal(false); + }); + + it('returns false for address with invalid hash', () => { + // Arrange: + const validHex = '6823BB7C3C089D996585466380EDBDC19D4959184893E38CA6'; + const decoded = convert.hexToUint8(validHex); + decoded[5] ^= 0xff; // ruin ripemd160 hash + + // Assert: + expect(address.isValidAddress(decoded)).to.equal(false); + }); + }); + + describe('isValidEncodedAddress', () => { + it('returns true for valid encoded address', () => { + // Arrange: + const encoded = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFG'; + + // Assert: + expect(address.isValidEncodedAddress(encoded)).to.equal(true); + }); + + it('returns false for invalid encoded address', () => { + // Arrange: changed last char + const encoded = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFH'; + + // Assert: + expect(address.isValidEncodedAddress(encoded)).to.equal(false); + }); + + it('returns false for encoded address with wrong length', () => { + // Arrange: added ABC + const encoded = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFGABC'; + + // Assert: + expect(address.isValidEncodedAddress(encoded)).to.equal(false); + }); + + it('adding leading or trailing white space invalidates encoded address', () => { + // Arrange: + const encoded = 'NAR3W7B4BCOZSZMFIZRYB3N5YGOUSWIYJCJ6HDFG'; + + // Assert: + expect(address.isValidEncodedAddress(` \t ${encoded}`)).to.equal(false); + expect(address.isValidEncodedAddress(`${encoded} \t `)).to.equal(false); + expect(address.isValidEncodedAddress(` \t ${encoded} \t `)).to.equal(false); + }); + }); +}); \ No newline at end of file diff --git a/test/core/format/RawArray.spec.ts b/test/core/format/RawArray.spec.ts new file mode 100644 index 0000000000..a1017c4c05 --- /dev/null +++ b/test/core/format/RawArray.spec.ts @@ -0,0 +1,223 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import { Convert as convert, RawArray as array} from '../../../src/core/format'; + +describe('array', () => { + describe('uint8View', () => { + it('can get uint8 view of array buffer', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + + // Act: + const view = array.uint8View(src.buffer); + + // Assert: + expect(convert.uint8ToHex(view)).to.equal('0A12B5675069'); + }); + + it('can get uint8 view of uint8 typed array', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + + // Act: + const view = array.uint8View(src); + + // Assert: + expect(convert.uint8ToHex(view)).to.equal('0A12B5675069'); + }); + + it('cannot get uint8 view of arbitrary typed array', () => { + // Arrange: + const src = new Uint16Array(10); + + // Act: + expect(() => array.uint8View(src)).to.throw('unsupported type passed to uint8View'); + }); + }); + + describe('copy', () => { + it('can copy full typed array', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + const dest = new Uint8Array(src.length); + + // Act: + array.copy(dest, src); + + // Assert: + expect(convert.uint8ToHex(dest)).to.equal('0A12B5675069'); + }); + + it('can copy partial typed array when dest is same size as src', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + const dest = new Uint8Array(src.length); + + // Act: + array.copy(dest, src, 3); + + // Assert: + expect(convert.uint8ToHex(dest)).to.equal('0A12B5000000'); + }); + + it('can copy partial typed array when dest is smaller than src', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + const dest = new Uint8Array(4); + + // Act: + array.copy(dest, src); + + // Assert: + expect(convert.uint8ToHex(dest)).to.equal('0A12B567'); + }); + + it('can copy partial typed array with custom offsets', () => { + // Arrange: + const src = convert.hexToUint8('0A12B5675069'); + const dest = new Uint8Array(src.length); + + // Act: + array.copy(dest, src, 3, 2, 1); + + // Assert: + expect(convert.uint8ToHex(dest)).to.equal('000012B56700'); + }); + }); + + describe('isZeroFilled', () => { + it('returns true if typed array is zero', () => { + // Act: + const isZero = array.isZeroFilled(new Uint16Array(10)); + + // Assert: + expect(isZero).to.equal(true); + }); + + function assertIsNonZero(length, nonZeroOffset) { + // Arrange: + const src = new Uint16Array(length); + src[nonZeroOffset] = 2; + + // Act + const isZero = array.isZeroFilled(src); + + // Assert: + expect(isZero, `nonzero offset ${nonZeroOffset}`).to.equal(false); + } + + it('returns false if typed array is non zero', () => { + // Assert: + assertIsNonZero(10, 0); + assertIsNonZero(10, 5); + assertIsNonZero(10, 9); + }); + }); + + describe('deepEqual', () => { + it('returns true if typed arrays are equal', () => { + // Arrange: + const lhs = convert.hexToUint8('0A12B5675069'); + const rhs = convert.hexToUint8('0A12B5675069'); + + // Act: + const isEqual = array.deepEqual(lhs, rhs); + + // Assert: + expect(isEqual).to.equal(true); + }); + + it('returns false if typed arrays have different sizes', () => { + // Arrange: + const shorter = convert.hexToUint8('0A12B5675069'); + const longer = convert.hexToUint8('0A12B567506983'); + + // Act: + const isEqual1 = array.deepEqual(shorter, longer); + const isEqual2 = array.deepEqual(longer, shorter); + + // Assert: + expect(isEqual1).to.equal(false); + expect(isEqual2).to.equal(false); + }); + + function assertNotEqual(lhs, unequalOffset) { + // Arrange: + const rhs = new Uint8Array(lhs.length); + array.copy(rhs, lhs); + rhs[unequalOffset] ^= 0xFF; + + // Act + const isEqual = array.deepEqual(lhs, rhs); + + // Assert: + expect(isEqual, `unequal offset ${unequalOffset}`).to.equal(false); + } + + it('returns false if typed arrays are not equal', () => { + // Arrange: + const lhs = convert.hexToUint8('0A12B5675069'); + + // Assert: + assertNotEqual(lhs, 0); + assertNotEqual(lhs, 3); + assertNotEqual(lhs, 5); + }); + + it('returns true if subset of typed arrays are equal', () => { + // Arrange: different at 2 + const lhs = convert.hexToUint8('0A12B5675069'); + const rhs = convert.hexToUint8('0A12C5675069'); + + // Act: + const isEqualSubset = array.deepEqual(lhs, rhs, 2); + const isEqualAll = array.deepEqual(lhs, rhs); + + // Assert: + expect(isEqualSubset).to.equal(true); + expect(isEqualAll).to.equal(false); + }); + + it('returns true if subset of typed arrays of different lengths are equal', () => { + // Arrange: + const shorter = convert.hexToUint8('0A12B5'); + const longer = convert.hexToUint8('0A12B567506983'); + + // Act: + const isEqual1 = array.deepEqual(shorter, longer, 3); + const isEqual2 = array.deepEqual(longer, shorter, 3); + + // Assert: + expect(isEqual1).to.equal(true); + expect(isEqual2).to.equal(true); + }); + + it('returns false if either typed array has fewer elements than requested for comparison', () => { + // Arrange: + const shorter = convert.hexToUint8('0A12B5'); + const longer = convert.hexToUint8('0A12B567506983'); + + // Act: + const isEqual1 = array.deepEqual(shorter, longer, 4); + const isEqual2 = array.deepEqual(longer, shorter, 4); + + // Assert: + expect(isEqual1).to.equal(false); + expect(isEqual2).to.equal(false); + }); + }); +}); \ No newline at end of file diff --git a/test/core/format/RawUInt64.spec.ts b/test/core/format/RawUInt64.spec.ts new file mode 100644 index 0000000000..f7d3c26bdc --- /dev/null +++ b/test/core/format/RawUInt64.spec.ts @@ -0,0 +1,319 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + expect +} from 'chai'; +import { Convert as convert, RawUInt64 as uint64} from '../../../src/core/format'; + +describe('uint64', () => { + describe('compact', () => { + it('can compact 32 bit value', () => { + // Act: + const result = uint64.compact([0x12345678, 0x00000000]); + + // Assert: + expect(result).to.equal(0x12345678); + }); + + it('can compact less than max safe integer', () => { + // Act: + const result = uint64.compact([0x00ABCDEF, 0x000FDFFF]); + + // Assert: + expect(result).to.equal(0xFDFFF00ABCDEF); + }); + + it('can compact max safe integer', () => { + // Sanity: + expect(0x1FFFFFFFFFFFFF).to.equal(Number.MAX_SAFE_INTEGER); + + // Act: + const result = uint64.compact([0xFFFFFFFF, 0x001FFFFF]); + + // Assert: + expect(result).to.equal(Number.MAX_SAFE_INTEGER); + }); + + it('cannot compact min unsafe integer', () => { + // Sanity: + expect(0x0020000000000000 + 1).to.equal(0x0020000000000000); + + // Act: + const result = uint64.compact([0x00000000, 0x00200000]); + + // Assert: + expect(result).to.deep.equal([0x00000000, 0x00200000]); + }); + + it('cannot compact greater than min unsafe integer', () => { + // Act: + const result = uint64.compact([0xF0000000, 0x01000D00]); + + // Assert: + expect(result).to.deep.equal([0xF0000000, 0x01000D00]); + }); + }); + + describe('fromUint', () => { + // const failureTestCases = [ + // { number: 0x0020000000000000, description: 'min unsafe integer' }, + // { number: 0x01000D00F0000000, description: 'greater than min unsafe integer' }, + // { number: -1, description: 'negative' }, + // { number: 1234.56, description: 'floating point' } + // ]; + + // failureTestCases.forEach(testCase => { + // it(`cannot parse number that is ${testCase.description}`, () => { + // // Assert: + // expect(() => uint64.fromUint(testCase.number)).to.throw(`number cannot be converted to uint '${testCase.number}'`); + // }); + // }); + + const successTestCases = [{ + number: 0, + uint64: [0, 0], + description: '0' + }, + { + number: 0xA1B2, + uint64: [0xA1B2, 0], + description: '(0, 8)' + }, + { + number: 0x12345678, + uint64: [0x12345678, 0], + description: '8' + }, + { + number: 0xABCD12345678, + uint64: [0x12345678, 0xABCD], + description: '(8, 16)' + }, + { + number: 0x0014567890ABCDEF, + uint64: [0x90ABCDEF, 0x00145678], + description: '14' + }, + { + number: Number.MAX_SAFE_INTEGER, + uint64: [0xFFFFFFFF, 0x001FFFFF], + description: '14 (max value)' + } + ]; + + successTestCases.forEach(testCase => { + it(`can parse numeric with ${testCase.description} significant digits`, () => { + // Act: + const value = uint64.fromUint(testCase.number); + + // Assert: + expect(value).to.deep.equal(testCase.uint64); + }); + }); + }); + + const hexTestCases = [{ + str: '0000000000000000', + value: [0, 0], + description: '0' + }, + { + str: '000000000000A1B2', + value: [0xA1B2, 0], + description: '(0, 8)' + }, + { + str: '0000000012345678', + value: [0x12345678, 0], + description: '8' + }, + { + str: '0000ABCD12345678', + value: [0x12345678, 0xABCD], + description: '(8, 16)' + }, + { + str: '1234567890ABCDEF', + value: [0x90ABCDEF, 0x12345678], + description: '16' + }, + { + str: 'FFFFFFFFFFFFFFFF', + value: [0xFFFFFFFF, 0xFFFFFFFF], + description: '16 (max value)' + } + ]; + + describe('fromBytes', () => { + hexTestCases.forEach(testCase => { + it(`can parse byte array with ${testCase.description} significant digits`, () => { + // Arrange: prepare little-endian bytes + const bytes = convert.hexToUint8(testCase.str).reverse(); + + // Act: + const value = uint64.fromBytes(bytes); + + // Assert: + expect(value).to.deep.equal(testCase.value); + }); + }); + + it('cannot parse byte array with invalid size into uint64', () => { + // Arrange: + const errorMessage = 'byte array has unexpected size'; + + // Assert: + [0, 3, 4, 5, 7, 9].forEach(size => { + expect(() => { + uint64.fromBytes(new Uint8Array(size)); + }, `size ${size}`).to.throw(errorMessage); + }); + }); + }); + + describe('fromBytes32', () => { + const fromBytes32TestCases = [{ + str: '00000000', + value: [0, 0], + description: '0' + }, + { + str: '0000A1B2', + value: [0xA1B2, 0], + description: '(0, 8)' + }, + { + str: '12345678', + value: [0x12345678, 0], + description: '8' + }, + { + str: 'FFFFFFFF', + value: [0xFFFFFFFF, 0], + description: '8 (max value)' + } + ]; + + fromBytes32TestCases.forEach(testCase => { + it(`can parse byte array with ${testCase.description} significant digits`, () => { + // Arrange: prepare little-endian bytes + const bytes = convert.hexToUint8(testCase.str).reverse(); + + // Act: + const value = uint64.fromBytes32(bytes); + + // Assert: + expect(value).to.deep.equal(testCase.value); + }); + }); + + it('cannot parse byte array with invalid size into uint64', () => { + // Arrange: + const errorMessage = 'byte array has unexpected size'; + + // Assert: + [0, 3, 5, 7, 8, 9].forEach(size => { + expect(() => { + uint64.fromBytes32(new Uint8Array(size)); + }, `size ${size}`).to.throw(errorMessage); + }); + }); + }); + + describe('fromHex', () => { + hexTestCases.forEach(testCase => { + it(`can parse hex string with ${testCase.description} significant digits`, () => { + // Act: + const value = uint64.fromHex(testCase.str); + + // Assert: + expect(value).to.deep.equal(testCase.value); + }); + }); + + it('cannot parse hex string with invalid characters into uint64', () => { + // Assert: + expect(() => { + uint64.fromHex('0000000012345G78'); + }).to.throw('unrecognized hex char'); // contains 'G' + }); + + it('cannot parse hex string with invalid size into uint64', () => { + // Arrange: + const errorMessage = 'hex string has unexpected size'; + + // Assert: + expect(() => { + uint64.fromHex(''); + }).to.throw(errorMessage); // empty string + expect(() => { + uint64.fromHex('1'); + }).to.throw(errorMessage); // odd number of chars + expect(() => { + uint64.fromHex('ABCDEF12'); + }).to.throw(errorMessage); // too short + expect(() => { + uint64.fromHex('1234567890ABCDEF12'); + }).to.throw(errorMessage); // too long + }); + }); + + describe('toHex', () => { + hexTestCases.forEach(testCase => { + it(`can format hex string with ${testCase.description} significant digits`, () => { + // Act: + const str = uint64.toHex(testCase.value); + + // Assert: + expect(str).to.equal(testCase.str); + }); + }); + }); + + describe('isZero', () => { + const zeroTestCases = [{ + description: 'low and high are zero', + value: [0, 0], + isZero: true + }, + { + description: 'low is nonzero and high is zero', + value: [1, 0], + isZero: false + }, + { + description: 'low is zero and high is nonzero', + value: [0, 1], + isZero: false + }, + { + description: 'low and high are nonzero', + value: [74, 12], + isZero: false + } + ]; + + zeroTestCases.forEach(testCase => { + it(`returns ${testCase.isZero} when ${testCase.description}`, () => { + // Act: + const isZero = uint64.isZero(testCase.value); + + // Assert: + expect(isZero).to.equal(testCase.isZero); + }); + }); + }); +}); diff --git a/test/core/format/Utilities.spec.ts b/test/core/format/Utilities.spec.ts new file mode 100644 index 0000000000..26fa600e4a --- /dev/null +++ b/test/core/format/Utilities.spec.ts @@ -0,0 +1,146 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {expect} from 'chai'; +import * as utilities from '../../../src/core/format/Utilities'; + +describe('Char Mapping', () => { + describe('builder', () => { + it('initially has empty map', () => { + // Arrange: + const builder = utilities.createBuilder(); + + // Act: + const map = builder.map; + + // Assert: + expect(map).to.deep.equal({}); + }); + + it('can add single arbitrary range with zero base', () => { + // Arrange: + const builder = utilities.createBuilder(); + + // Act: + builder.addRange('d', 'f', 0); + const map = builder.map; + + // Assert: + expect(map).to.deep.equal({ + d: 0, + e: 1, + f: 2, + }); + }); + + it('can add single arbitrary range with nonzero base', () => { + // Arrange: + const builder = utilities.createBuilder(); + + // Act: + builder.addRange('d', 'f', 17); + const map = builder.map; + + // Assert: + expect(map).to.deep.equal({ + d: 17, + e: 18, + f: 19, + }); + }); + + it('can add multiple arbitrary ranges', () => { + // Arrange: + const builder = utilities.createBuilder(); + + // Act: + builder.addRange('b', 'b', 8); + builder.addRange('d', 'f', 17); + builder.addRange('y', 'z', 0); + const map = builder.map; + + // Assert: + expect(map).to.deep.equal({ + b: 8, + d: 17, + e: 18, + f: 19, + y: 0, + z: 1, + }); + }); + + it('can add multiple arbitrary overlapping ranges', () => { + // Arrange: + const builder = utilities.createBuilder(); + + // Act: + builder.addRange('b', 'b', 18); + builder.addRange('d', 'f', 17); + builder.addRange('y', 'z', 19); + const map = builder.map; + + // Assert: + expect(map).to.deep.equal({ + b: 18, + d: 17, + e: 18, + f: 19, + y: 19, + z: 20, + }); + }); + }); +}); + +describe('Convert', () => { + describe('tryParseUint', () => { + function addTryParseSuccessTest(name, str, expectedValue) { + it(name, () => { + // Act: + const value = utilities.tryParseUint(str); + + // Assert: + expect(value).to.equal(expectedValue); + }); + } + + addTryParseSuccessTest('can parse decimal string', '14952', 14952); + addTryParseSuccessTest('can parse zero decimal string', '0', 0); + addTryParseSuccessTest('can parse decimal string with all digits', '1234567890', 1234567890); + addTryParseSuccessTest('can parse decimal string with zeros', '10002', 10002); + addTryParseSuccessTest('can parse max safe integer decimal string', Number.MAX_SAFE_INTEGER.toString(), 9007199254740991); + + function addTryParseFailureTest(name, str) { + it(name, () => { + // Act: + const value = utilities.tryParseUint(str); + + // Assert: + expect(value).to.equal(undefined); + }); + } + + addTryParseFailureTest('cannot parse decimal string with left padding', ' 14952'); + addTryParseFailureTest('cannot parse decimal string with right padding', '14952 '); + addTryParseFailureTest('cannot parse decimal string too large', '9007199254740992'); + addTryParseFailureTest('cannot parse zeros string', '00'); + addTryParseFailureTest('cannot parse octal string', '0123'); + addTryParseFailureTest('cannot parse hex string', '0x14A52'); + addTryParseFailureTest('cannot parse double string', '14.52'); + addTryParseFailureTest('cannot parse negative decimal string', '-14952'); + addTryParseFailureTest('cannot parse arbitrary string', 'catapult'); + }); +}); diff --git a/test/core/utils/TransactionMapping.spec.ts b/test/core/utils/TransactionMapping.spec.ts index 0b1ca6bca8..c7bc986b01 100644 --- a/test/core/utils/TransactionMapping.spec.ts +++ b/test/core/utils/TransactionMapping.spec.ts @@ -17,7 +17,7 @@ import {deepEqual} from 'assert'; import { expect } from 'chai'; import { sha3_256 } from 'js-sha3'; -import { convert } from 'nem2-library'; +import {Convert as convert} from '../../../src/core/format'; import { TransactionMapping } from '../../../src/core/utils/TransactionMapping'; import { Account } from '../../../src/model/account/Account'; import { Address } from '../../../src/model/account/Address'; diff --git a/test/infrastructure/SerializeTransactionToJSON.spec.ts b/test/infrastructure/SerializeTransactionToJSON.spec.ts index 655e644a01..3c2d1bcdf9 100644 --- a/test/infrastructure/SerializeTransactionToJSON.spec.ts +++ b/test/infrastructure/SerializeTransactionToJSON.spec.ts @@ -16,7 +16,7 @@ import { expect } from 'chai'; import { sha3_256 } from 'js-sha3'; -import { convert } from 'nem2-library'; +import {Convert as convert} from '../../src/core/format'; import { Account } from '../../src/model/account/Account'; import { Address } from '../../src/model/account/Address'; import { PropertyModificationType } from '../../src/model/account/PropertyModificationType'; diff --git a/test/model/transaction/SecretLockTransaction.spec.ts b/test/model/transaction/SecretLockTransaction.spec.ts index 9795d7f5ae..26bbd11ede 100644 --- a/test/model/transaction/SecretLockTransaction.spec.ts +++ b/test/model/transaction/SecretLockTransaction.spec.ts @@ -17,7 +17,7 @@ import {deepEqual} from 'assert'; import {expect} from 'chai'; import * as CryptoJS from 'crypto-js'; import {keccak_256, sha3_256} from 'js-sha3'; -import {convert} from 'nem2-library'; +import {Convert as convert} from '../../../src/core/format'; import {Address} from '../../../src/model/account/Address'; import {NetworkType} from '../../../src/model/blockchain/NetworkType'; import {NetworkCurrencyMosaic} from '../../../src/model/mosaic/NetworkCurrencyMosaic'; diff --git a/test/model/transaction/SecretProofTransaction.spec.ts b/test/model/transaction/SecretProofTransaction.spec.ts index 12ccf662a0..6ee0b3aaab 100644 --- a/test/model/transaction/SecretProofTransaction.spec.ts +++ b/test/model/transaction/SecretProofTransaction.spec.ts @@ -16,7 +16,7 @@ import {expect} from 'chai'; import * as CryptoJS from 'crypto-js'; import {keccak_256, sha3_256} from 'js-sha3'; -import {convert} from 'nem2-library'; +import {Convert as convert} from '../../../src/core/format'; import { Account } from '../../../src/model/account/Account'; import {NetworkType} from '../../../src/model/blockchain/NetworkType'; import {Deadline} from '../../../src/model/transaction/Deadline';