diff --git a/src/bee-debug.ts b/src/bee-debug.ts index e4923b53..0b8c0fd0 100644 --- a/src/bee-debug.ts +++ b/src/bee-debug.ts @@ -97,7 +97,10 @@ export class BeeDebug { */ /** - * Get the address of the chequebook contract used + * Get the address of the chequebook contract used. + * + * **Warning:** The address is returned with 0x prefix unlike all other calls. + * https://github.com/ethersphere/bee/issues/1443 */ getChequebookAddress(): Promise { return chequebook.getChequebookAddress(this.url) diff --git a/src/bee.ts b/src/bee.ts index 8a212111..7d71b79e 100644 --- a/src/bee.ts +++ b/src/bee.ts @@ -29,9 +29,8 @@ import { Signer } from './chunk/signer' import { downloadSingleOwnerChunk, uploadSingleOwnerChunkData, SOCReader, SOCWriter } from './chunk/soc' import { Topic, makeTopic, makeTopicFromString } from './feed/topic' import { createFeedManifest } from './modules/feed' -import { bytesToHex } from './utils/hex' import { assertBeeUrl, stripLastSlash } from './utils/url' -import { EthAddress, makeEthAddress } from './utils/eth' +import { EthAddress, makeEthAddress, makeHexEthAddress } from './utils/eth' /** * The Bee class provides a way of interacting with the Bee APIs based on the provided url @@ -382,9 +381,9 @@ export class Bee { assertIsFeedType(type) const canonicalTopic = makeTopic(topic) - const canonicalOwner = makeEthAddress(owner) + const canonicalOwner = makeHexEthAddress(owner) - return createFeedManifest(this.url, bytesToHex(canonicalOwner), bytesToHex(canonicalTopic), { type }) + return createFeedManifest(this.url, canonicalOwner, canonicalTopic, { type }) } /** @@ -402,7 +401,7 @@ export class Bee { assertIsFeedType(type) const canonicalTopic = makeTopic(topic) - const canonicalOwner = makeEthAddress(owner) + const canonicalOwner = makeHexEthAddress(owner) return makeFeedReader(this.url, type, canonicalTopic, canonicalOwner) } diff --git a/src/chunk/signer.ts b/src/chunk/signer.ts index e2c82193..3fc825af 100644 --- a/src/chunk/signer.ts +++ b/src/chunk/signer.ts @@ -2,7 +2,7 @@ import { ec, curve } from 'elliptic' import { BeeError } from '../utils/error' import { Bytes, verifyBytes } from '../utils/bytes' import { keccak256Hash } from './hash' -import { hexToBytes, verifyHex } from '../utils/hex' +import { hexToBytes, makeHexString } from '../utils/hex' import { EthAddress } from '../utils/eth' /** @@ -128,11 +128,10 @@ export function isSigner(signer: unknown): signer is Signer { export function makeSigner(signer: Signer | Uint8Array | string | unknown): Signer { if (typeof signer === 'string') { - const hexKey = verifyHex(signer) - const keyBytes = hexToBytes(hexKey) - const verifiedPrivateKey = verifyBytes(32, keyBytes) + const hexKey = makeHexString(signer, 64) + const keyBytes = hexToBytes<32>(hexKey) // HexString is verified for 64 length => 32 is guaranteed - return makeDefaultSigner(verifiedPrivateKey) + return makeDefaultSigner(keyBytes) } else if (signer instanceof Uint8Array) { const verifiedPrivateKey = verifyBytes(32, signer) diff --git a/src/feed/index.ts b/src/feed/index.ts index a560e55a..b500ccb8 100644 --- a/src/feed/index.ts +++ b/src/feed/index.ts @@ -1,23 +1,33 @@ import { keccak256Hash } from '../chunk/hash' import { serializeBytes } from '../chunk/serialize' -import { Signer } from '../chunk/signer' import { Identifier, uploadSingleOwnerChunkData, verifySingleOwnerChunk } from '../chunk/soc' import { FeedUpdateOptions, fetchFeedUpdate, FetchFeedUpdateResponse } from '../modules/feed' -import { Reference, ReferenceResponse, UploadOptions } from '../types' -import { Bytes, makeBytes, verifyBytes, verifyBytesAtOffset } from '../utils/bytes' +import { + REFERENCE_HEX_LENGTH, + Reference, + ReferenceResponse, + UploadOptions, + ENCRYPTED_REFERENCE_HEX_LENGTH, + ENCRYPTED_REFERENCE_BYTES_LENGTH, + REFERENCE_BYTES_LENGTH, +} from '../types' +import { Bytes, makeBytes, verifyBytesAtOffset } from '../utils/bytes' import { BeeResponseError } from '../utils/error' -import { bytesToHex, HexString, hexToBytes, verifyHex } from '../utils/hex' +import { bytesToHex, HexString, hexToBytes, makeHexString } from '../utils/hex' import { readUint64BigEndian, writeUint64BigEndian } from '../utils/uint64' import * as chunkAPI from '../modules/chunk' -import { Topic } from './topic' -import { FeedType } from './type' -import { EthAddress } from '../utils/eth' +import { EthAddress, HexEthAddress, makeHexEthAddress } from '../utils/eth' + +import type { Signer } from '../chunk/signer' +import type { Topic } from './topic' +import type { FeedType } from './type' const TIMESTAMP_PAYLOAD_OFFSET = 0 const TIMESTAMP_PAYLOAD_SIZE = 8 const REFERENCE_PAYLOAD_OFFSET = TIMESTAMP_PAYLOAD_SIZE const REFERENCE_PAYLOAD_MIN_SIZE = 32 const REFERENCE_PAYLOAD_MAX_SIZE = 64 +const INDEX_HEX_LENGTH = 16 export interface Epoch { time: number @@ -42,7 +52,7 @@ export interface FeedUpdate { */ export interface FeedReader { readonly type: FeedType - readonly owner: EthAddress + readonly owner: HexEthAddress readonly topic: Topic /** * Download the latest feed update @@ -70,7 +80,7 @@ export function isEpoch(epoch: unknown): epoch is Epoch { } function hashFeedIdentifier(topic: Topic, index: IndexBytes): Identifier { - return keccak256Hash(topic, index) + return keccak256Hash(hexToBytes(topic), index) } export function makeSequentialFeedIdentifier(topic: Topic, index: number): Identifier { @@ -80,10 +90,9 @@ export function makeSequentialFeedIdentifier(topic: Topic, index: number): Ident } export function makeFeedIndexBytes(s: string): IndexBytes { - const hex = verifyHex(s) - const bytes = hexToBytes(hex) + const hex = makeHexString(s, INDEX_HEX_LENGTH) - return verifyBytes(8, bytes) + return hexToBytes(hex) } export function makeFeedIdentifier(topic: Topic, index: Index): Identifier { @@ -118,14 +127,14 @@ export function uploadFeedUpdate( export async function findNextIndex( url: string, - owner: HexString, - topic: HexString, + owner: HexEthAddress, + topic: Topic, options?: FeedUpdateOptions, -): Promise { +): Promise> { try { const feedUpdate = await fetchFeedUpdate(url, owner, topic, options) - return feedUpdate.feedIndexNext + return makeHexString(feedUpdate.feedIndexNext, INDEX_HEX_LENGTH) } catch (e) { if (e instanceof BeeResponseError && e.status === 404) { return bytesToHex(makeBytes(8)) @@ -141,9 +150,8 @@ export async function updateFeed( reference: ChunkReference, options?: FeedUploadOptions, ): Promise { - const ownerHex = bytesToHex(signer.address) - const topicHex = bytesToHex(topic) - const nextIndex = await findNextIndex(url, ownerHex, topicHex, options) + const ownerHex = makeHexEthAddress(signer.address) + const nextIndex = await findNextIndex(url, ownerHex, topic, options) return uploadFeedUpdate(url, signer, topic, nextIndex, reference, options) } @@ -182,10 +190,8 @@ export async function downloadFeedUpdate( } } -export function makeFeedReader(url: string, type: FeedType, topic: Topic, owner: EthAddress): FeedReader { - const ownerHex = bytesToHex(owner) - const topicHex = bytesToHex(topic) - const download = (options?: FeedUpdateOptions) => fetchFeedUpdate(url, ownerHex, topicHex, { ...options, type }) +export function makeFeedReader(url: string, type: FeedType, topic: Topic, owner: HexEthAddress): FeedReader { + const download = (options?: FeedUpdateOptions) => fetchFeedUpdate(url, owner, topic, { ...options, type }) return { type, @@ -197,10 +203,21 @@ export function makeFeedReader(url: string, type: FeedType, topic: Topic, owner: function makeChunkReference(reference: ChunkReference | Reference): ChunkReference { if (typeof reference === 'string') { - const hexReference = verifyHex(reference) - const referenceBytes = hexToBytes(hexReference) + try { + // Non-encrypted chunk hex string reference + const hexReference = makeHexString(reference, REFERENCE_HEX_LENGTH) + + return hexToBytes(hexReference) + } catch (e) { + if (!(e instanceof TypeError)) { + throw e + } - return verifyChunkReference(referenceBytes) + // Encrypted chunk hex string reference + const hexReference = makeHexString(reference, ENCRYPTED_REFERENCE_HEX_LENGTH) + + return hexToBytes(hexReference) + } } else if (reference instanceof Uint8Array) { return verifyChunkReference(reference) } @@ -215,7 +232,7 @@ export function makeFeedWriter(url: string, type: FeedType, topic: Topic, signer } return { - ...makeFeedReader(url, type, topic, signer.address), + ...makeFeedReader(url, type, topic, makeHexEthAddress(signer.address)), upload, } } diff --git a/src/feed/topic.ts b/src/feed/topic.ts index a1fa1585..3d1d08f2 100644 --- a/src/feed/topic.ts +++ b/src/feed/topic.ts @@ -1,24 +1,23 @@ import { keccak256Hash } from '../chunk/hash' -import { Bytes, verifyBytes } from '../utils/bytes' -import { hexToBytes, verifyHex } from '../utils/hex' +import { verifyBytes } from '../utils/bytes' +import { HexString, makeHexString, bytesToHex } from '../utils/hex' -export const TOPIC_LENGTH_BYTES = 32 -export const TOPIC_LENGTH_HEX = 2 * TOPIC_LENGTH_BYTES +export const TOPIC_BYTES_LENGTH = 32 +export const TOPIC_HEX_LENGTH = 64 -export type Topic = Bytes<32> +export type Topic = HexString export function makeTopic(topic: Uint8Array | string): Topic { if (typeof topic === 'string') { - const topicHex = verifyHex(topic) - const topicBytes = hexToBytes(topicHex) - - return verifyBytes(TOPIC_LENGTH_BYTES, topicBytes) + return makeHexString(topic, TOPIC_HEX_LENGTH) } else if (topic instanceof Uint8Array) { - return verifyBytes(TOPIC_LENGTH_BYTES, topic) + verifyBytes(TOPIC_BYTES_LENGTH, topic) + + return bytesToHex(topic, TOPIC_HEX_LENGTH) } throw new TypeError('invalid topic') } export function makeTopicFromString(s: string): Topic { - return keccak256Hash(s) + return bytesToHex(keccak256Hash(s), TOPIC_HEX_LENGTH) } diff --git a/src/modules/bytes.ts b/src/modules/bytes.ts index 20293d66..908ce435 100644 --- a/src/modules/bytes.ts +++ b/src/modules/bytes.ts @@ -1,6 +1,6 @@ import type { AxiosRequestConfig } from 'axios' import type { Readable } from 'stream' -import { UploadOptions } from '../types' +import { Reference, UploadOptions } from '../types' import { prepareData } from '../utils/data' import { extractUploadHeaders } from '../utils/headers' import { safeAxios } from '../utils/safeAxios' @@ -14,8 +14,8 @@ const endpoint = '/bytes' * @param data Data to be uploaded * @param options Aditional options like tag, encryption, pinning */ -export async function upload(url: string, data: string | Uint8Array, options?: UploadOptions): Promise { - const response = await safeAxios<{ reference: string }>({ +export async function upload(url: string, data: string | Uint8Array, options?: UploadOptions): Promise { + const response = await safeAxios<{ reference: Reference }>({ ...options?.axiosOptions, method: 'post', url: url + endpoint, @@ -36,7 +36,7 @@ export async function upload(url: string, data: string | Uint8Array, options?: U * @param url Bee URL * @param hash Bee content reference */ -export async function download(url: string, hash: string): Promise { +export async function download(url: string, hash: Reference): Promise { const response = await safeAxios({ responseType: 'arraybuffer', url: `${url}${endpoint}/${hash}`, @@ -54,7 +54,7 @@ export async function download(url: string, hash: string): Promise { */ export async function downloadReadable( url: string, - hash: string, + hash: Reference, axiosOptions?: AxiosRequestConfig, ): Promise { const response = await safeAxios({ diff --git a/src/modules/collection.ts b/src/modules/collection.ts index 8a7656bc..03b33e09 100644 --- a/src/modules/collection.ts +++ b/src/modules/collection.ts @@ -9,6 +9,7 @@ import { safeAxios } from '../utils/safeAxios' import { extractUploadHeaders, readFileHeaders } from '../utils/headers' import { BeeArgumentError } from '../utils/error' import { fileArrayBuffer } from '../utils/file' +import { Reference } from '../types' const dirsEndpoint = '/dirs' const bzzEndpoint = '/bzz' @@ -124,7 +125,7 @@ export async function upload( url: string, data: Collection, options?: CollectionUploadOptions, -): Promise { +): Promise { if (!url || url === '') { throw new BeeArgumentError('url parameter is required and cannot be empty', url) } @@ -135,7 +136,7 @@ export async function upload( const tarData = makeTar(data) - const response = await safeAxios<{ reference: string }>({ + const response = await safeAxios<{ reference: Reference }>({ ...options?.axiosOptions, method: 'post', url: `${url}${dirsEndpoint}`, diff --git a/src/modules/feed.ts b/src/modules/feed.ts index 65c9727c..6c033db5 100644 --- a/src/modules/feed.ts +++ b/src/modules/feed.ts @@ -1,6 +1,8 @@ import { Dictionary, Reference, ReferenceResponse } from '../types' import { safeAxios } from '../utils/safeAxios' import { FeedType } from '../feed/type' +import { HexEthAddress } from '../utils/eth' +import { Topic } from '../feed/topic' const feedEndpoint = '/feeds' @@ -35,8 +37,8 @@ export interface FetchFeedUpdateResponse extends ReferenceResponse, FeedUpdateHe */ export async function createFeedManifest( url: string, - owner: string, - topic: string, + owner: HexEthAddress, + topic: Topic, options?: CreateFeedOptions, ): Promise { const response = await safeAxios({ @@ -70,8 +72,8 @@ function readFeedUpdateHeaders(headers: Dictionary): FeedUpdateHeaders { */ export async function fetchFeedUpdate( url: string, - owner: string, - topic: string, + owner: HexEthAddress, + topic: Topic, options?: FeedUpdateOptions, ): Promise { const response = await safeAxios({ diff --git a/src/modules/file.ts b/src/modules/file.ts index d43c6708..bd419577 100644 --- a/src/modules/file.ts +++ b/src/modules/file.ts @@ -1,6 +1,6 @@ import type { AxiosRequestConfig } from 'axios' import type { Readable } from 'stream' -import { FileData, FileUploadOptions, UploadHeaders } from '../types' +import { FileData, FileUploadOptions, Reference, UploadHeaders } from '../types' import { prepareData } from '../utils/data' import { extractUploadHeaders, readFileHeaders } from '../utils/headers' import { safeAxios } from '../utils/safeAxios' @@ -35,8 +35,8 @@ export async function upload( data: string | Uint8Array | Readable | ArrayBuffer, name?: string, options?: FileUploadOptions, -): Promise { - const response = await safeAxios<{ reference: string }>({ +): Promise { + const response = await safeAxios<{ reference: Reference }>({ ...options?.axiosOptions, method: 'post', url: url + endpoint, diff --git a/src/types/index.ts b/src/types/index.ts index 299d7fd5..893fe0b5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,20 +1,23 @@ import { BeeError } from '../utils/error' import type { AxiosRequestConfig } from 'axios' +import { HexString } from '../utils/hex' export * from './debug' export interface Dictionary { [Key: string]: T } -export type Reference = string +export const REFERENCE_HEX_LENGTH = 64 +export const ENCRYPTED_REFERENCE_HEX_LENGTH = 128 +export const REFERENCE_BYTES_LENGTH = 32 +export const ENCRYPTED_REFERENCE_BYTES_LENGTH = 64 + +export type Reference = HexString | HexString export type PublicKey = string export type Address = string export type AddressPrefix = Address -export const HEX_REFERENCE_LENGTH = 64 -export const ENCRYPTED_HEX_REFERENCE_LENGTH = 2 * HEX_REFERENCE_LENGTH - export interface UploadOptions { pin?: boolean encrypt?: boolean @@ -102,8 +105,8 @@ export interface ReferenceResponse { * * See https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ */ -export type BrandedType = T & { __tag__: N } +export type BrandedType = Type & { __tag__: Name } -export type BrandedString = BrandedType +export type BrandedString = BrandedType -export type FlavoredType = T & { __tag__?: N } +export type FlavoredType = Type & { __tag__?: Name } diff --git a/src/utils/eth.ts b/src/utils/eth.ts index dd78a684..81baf8d8 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -1,22 +1,36 @@ import { keccak256, sha3_256 } from 'js-sha3' import { BrandedString } from '../types' -import { HexString, hexToBytes, intToHex, isHexString, stripHexPrefix, verifyHex } from './hex' +import { HexString, hexToBytes, intToHex, makeHexString, assertHexString } from './hex' import { Bytes, verifyBytes } from './bytes' -export type HexEthAddress = BrandedString<'HexEthAddress'> export type OverlayAddress = BrandedString<'OverlayAddress'> export type EthAddress = Bytes<20> +export type HexEthAddress = HexString<40> +const ETH_ADDR_BYTES_LENGTH = 20 +const ETH_ADDR_HEX_LENGTH = 40 export function makeEthAddress(address: EthAddress | Uint8Array | string): EthAddress { if (typeof address === 'string') { - const hexOwner = verifyHex(address) - const ownerBytes = hexToBytes(hexOwner) + const hexAddr = makeHexString(address, ETH_ADDR_HEX_LENGTH) + const ownerBytes = hexToBytes(hexAddr) - return verifyBytes(20, ownerBytes) + return verifyBytes(ETH_ADDR_BYTES_LENGTH, ownerBytes) } else if (address instanceof Uint8Array) { - return verifyBytes(20, address) + return verifyBytes(ETH_ADDR_BYTES_LENGTH, address) + } + throw new TypeError('Invalid EthAddress') +} + +export function makeHexEthAddress(address: EthAddress | Uint8Array | string): HexEthAddress { + try { + return makeHexString(address, ETH_ADDR_HEX_LENGTH) + } catch (e) { + if (e instanceof TypeError) { + e.message = `Invalid HexEthAddress: ${e.message}` + } + + throw e } - throw new TypeError('invalid owner') } /** @@ -37,23 +51,30 @@ function isEthAddrCaseIns(address: string | HexString | HexEthAddress): address * @param address Ethereum address as hex string */ function isValidChecksummedEthAddress(address: string | HexString | HexEthAddress): address is HexEthAddress { - // Does not meet basic requirements of an address - type string, 40 chars, case insensitive hex numbers - if (typeof address !== 'string' && !/^(0x)?[0-9a-f]{40}$/i.test(address)) return false - - // Check the checksum - const addr = stripHexPrefix(address) - const addressHash = keccak256(addr.toLowerCase()) - for (let i = 0; i < 40; i += 1) { - // the nth letter should be uppercase if the nth digit of casemap is 1 - if ( - (parseInt(addressHash[i], 16) > 7 && addr[i].toUpperCase() !== addr[i]) || - (parseInt(addressHash[i], 16) <= 7 && addr[i].toLowerCase() !== addr[i]) - ) { + try { + // Check for valid case insensitive hex type string, 40 chars + const addr = makeHexString(address, ETH_ADDR_HEX_LENGTH) + + // Check the checksum + const addressHash = keccak256(addr.toLowerCase()) + for (let i = 0; i < 40; i += 1) { + // the nth letter should be uppercase if the nth digit of casemap is 1 + if ( + (parseInt(addressHash[i], 16) > 7 && addr[i].toUpperCase() !== addr[i]) || + (parseInt(addressHash[i], 16) <= 7 && addr[i].toLowerCase() !== addr[i]) + ) { + return false + } + } + + return true + } catch (e) { + if (e instanceof TypeError) { return false } - } - return true + throw e + } } /** @@ -86,7 +107,7 @@ export function toLittleEndian(bigEndian: number | string | HexString, pad = 2): let hexRep - if (isHexString(bigEndian as string)) hexRep = stripHexPrefix(bigEndian as HexString) + if (typeof bigEndian === 'string') hexRep = makeHexString(bigEndian as HexString) else if (typeof bigEndian === 'number') hexRep = intToHex(bigEndian) else throw new TypeError('incorrect input type') @@ -139,7 +160,8 @@ export function ethToSwarmAddress(ethAddress: string | HexString | HexEthAddress assertIsEthAddress(ethAddress) assertIsSwarmNetworkId(networkId) - const hex = verifyHex(`${stripHexPrefix(ethAddress)}${toLittleEndian(networkId, 16)}`) + const hex = `${makeHexString(ethAddress)}${toLittleEndian(networkId, 16)}` + assertHexString(hex) const overlayAddress = sha3_256(hexToBytes(hex)) diff --git a/src/utils/hex.ts b/src/utils/hex.ts index 76f81bc4..b0209cb9 100644 --- a/src/utils/hex.ts +++ b/src/utils/hex.ts @@ -1,90 +1,183 @@ -import { BrandedString } from '../types' +import { Bytes, makeBytes } from './bytes' +import { BrandedType, FlavoredType } from '../types' /** - * Nominal type to represent hex strings + * Nominal type to represent hex strings WITHOUT '0x' prefix. + * For example for 32 bytes hex representation you have to use 64 length. + * TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 */ -export type HexString = BrandedString<'HexString'> +export type HexString = FlavoredType< + string & { + readonly length: Length + }, + 'HexString' +> /** - * Strips the '0x' hex prefix from a string + * Type for HexString with prefix. + * The main hex type used internally should be non-prefixed HexString + * and therefore this type should be used as least as possible. + * Because of that it does not contain the Length property as the variables + * should be validated and converted to HexString ASAP. + */ +export type PrefixedHexString = BrandedType - * @param hex string input +/** + * Creates unprefixed hex string from wide range of data. + * + * TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 + * + * @param input + * @param len of the resulting HexString WITHOUT prefix! */ -export function stripHexPrefix(hex: T): T { - return hex.startsWith('0x') ? (hex.slice(2) as T) : hex +export function makeHexString(input: string | number | Uint8Array, len?: L): HexString { + if (typeof input === 'number') { + return intToHex(input, len) + } + + if (input instanceof Uint8Array) { + return bytesToHex(input, len) + } + + if (typeof input === 'string') { + if (isPrefixedHexString(input)) { + const hex = input.slice(2) as HexString + + if (len && hex.length !== len) { + throw new TypeError(`Length mismatch for valid hex string. Expecting length ${len}: ${hex}`) + } + + return hex + } else { + // We use assertHexString() as there might be more reasons why a string is not valid hex string + // and usage of isHexString() would not give enough information to the user on what is going + // wrong. + assertHexString(input, len) + + return input + } + } + + throw new TypeError('Not HexString compatible type!') } /** * Converts a hex string to Uint8Array * - * @param hex string input + * @param hex string input without 0x prefix! */ -export function hexToBytes(hex: HexString): Uint8Array { - const hexWithoutPrefix = stripHexPrefix(hex) - const bytes = new Uint8Array(hexWithoutPrefix.length / 2) +export function hexToBytes( + hex: HexString, +): Bytes { + assertHexString(hex) + + const bytes = makeBytes(hex.length / 2) for (let i = 0; i < bytes.length; i++) { - const hexByte = hexWithoutPrefix.substr(i * 2, 2) + const hexByte = hex.substr(i * 2, 2) bytes[i] = parseInt(hexByte, 16) } - return bytes + return bytes as Bytes } /** - * Converts array of number or Uint8Array to hex string. + * Converts array of number or Uint8Array to HexString without prefix. * - * Optionally provides '0x' prefix. - * - * @param bytes The input array - * @param withPrefix Provides '0x' prefix when true (default: false) + * @param bytes The input array + * @param len The length of the non prefixed HexString */ -export function bytesToHex(bytes: Uint8Array, withPrefix = false): HexString { - const prefix = withPrefix ? '0x' : '' +export function bytesToHex(bytes: Uint8Array, len?: Length): HexString { const hexByte = (n: number) => n.toString(16).padStart(2, '0') - const hex = Array.from(bytes, hexByte).join('') + const hex = Array.from(bytes, hexByte).join('') as HexString + + // TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 + if (len && hex.length !== len) { + throw new TypeError(`Resulting HexString does not have expected length ${len}: ${hex}`) + } - return `${prefix}${hex}` as HexString + return hex } /** - * Converst integer number to hex string. + * Converts integer number to hex string. * * Optionally provides '0x' prefix or padding * * @param int The positive integer to be converted - * @param withPrefix Provides '0x' prefix when true (default: false) + * @param len The length of the non prefixed HexString */ -export function intToHex(int: number, withPrefix = false): HexString { +export function intToHex(int: number, len?: Length): HexString { if (!Number.isInteger(int)) throw new TypeError('the value provided is not integer') if (int > Number.MAX_SAFE_INTEGER) throw new TypeError('the value provided exceeds safe integer') if (int < 0) throw new TypeError('the value provided is a negative integer') - const prefix = withPrefix ? '0x' : '' - const hex = int.toString(16) + const hex = int.toString(16) as HexString - return `${prefix}${hex}` as HexString + // TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 + if (len && hex.length !== len) { + throw new TypeError(`Resulting HexString does not have expected length ${len}: ${hex}`) + } + + return hex } /** - * Type guard for HexStrings + * Type guard for HexStrings. + * Requires no 0x prefix! + * + * TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 * * @param s string input + * @param len expected length of the HexString */ -export function isHexString(s: string): s is HexString { - return typeof s === 'string' && /^(0x)?[0-9a-f]+$/i.test(s) +export function isHexString(s: unknown, len?: number): s is HexString { + return typeof s === 'string' && /^[0-9a-f]+$/i.test(s) && (!len || s.length === len) +} + +/** + * Type guard for PrefixedHexStrings. + * Does enforce presence of 0x prefix! + * + * @param s string input + */ +export function isPrefixedHexString(s: unknown): s is PrefixedHexString { + return typeof s === 'string' && /^0x[0-9a-f]+$/i.test(s) } /** * Verifies if the provided input is a HexString. * + * TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208 + * * @param s string input + * @param len expected length of the HexString + * @returns HexString or throws error + */ +export function assertHexString( + s: string, + len?: number, +): asserts s is HexString { + if (!isHexString(s, len)) { + if (isPrefixedHexString(s)) { + throw new TypeError(`Not valid non prefixed hex string (has 0x prefix): ${s}`) + } + + // Don't display length error if no length specified in order not to confuse user + const lengthMsg = len ? ` of length ${len}` : '' + throw new TypeError(`Not valid hex string${lengthMsg}: ${s}`) + } +} + +/** + * Verifies if the provided input is a PrefixedHexString. * + * @param s string input + * @param len expected length of the HexString * @returns HexString or throws error */ -export function verifyHex(s: string): HexString | never { - if (isHexString(s)) { - return s +export function assertPrefixedHexString(s: string): asserts s is PrefixedHexString { + if (!isPrefixedHexString(s)) { + throw new TypeError(`Not valid prefixed hex string: ${s}`) } - throw new Error(`verifyHex: not valid hex string: ${s}`) } diff --git a/test/bee-class.spec.ts b/test/bee-class.spec.ts index 63a19147..dbc3679b 100644 --- a/test/bee-class.spec.ts +++ b/test/bee-class.spec.ts @@ -2,7 +2,7 @@ import { Bee, BeeDebug, Collection } from '../src' import { BeeArgumentError } from '../src/utils/error' import { ChunkReference } from '../src/feed' -import { HEX_REFERENCE_LENGTH } from '../src/types' +import { REFERENCE_HEX_LENGTH } from '../src/types' import { makeBytes } from '../src/utils/bytes' import { bytesToHex, HexString } from '../src/utils/hex' import { @@ -126,7 +126,7 @@ describe('Bee class', () => { it('should work with directory with unicode filenames', async () => { const hash = await bee.uploadFilesFromDirectory('./test/data') - expect(hash.length).toEqual(HEX_REFERENCE_LENGTH) + expect(hash.length).toEqual(REFERENCE_HEX_LENGTH) }) }) diff --git a/test/bee.sh b/test/bee.sh index ad60d2a0..639560ea 100755 --- a/test/bee.sh +++ b/test/bee.sh @@ -138,6 +138,7 @@ if [ -z "$QUEEN_CONTAINER_IN_DOCKER" ] || $EPHEMERAL ; then --bootnode=$QUEEN_BOOTNODE \ --swap-enable=false \ --debug-api-enable \ + --verbosity=4 --welcome-message="You have found the queen of the beehive..." \ --cors-allowed-origins="*" \ --payment-tolerance=2147483647 diff --git a/test/chunk/signer.spec.ts b/test/chunk/signer.spec.ts index fe2adb94..059cb936 100644 --- a/test/chunk/signer.spec.ts +++ b/test/chunk/signer.spec.ts @@ -20,7 +20,7 @@ describe('signer', () => { test('recover address from signature', () => { const recoveredAddress = recoverAddress(expectedSignature as Signature, dataToSign) - expect(bytesToHex(recoveredAddress, true)).toEqual(testIdentity.address) + expect(bytesToHex(recoveredAddress)).toEqual(testIdentity.address) }) describe('makeSigner', () => { @@ -28,7 +28,7 @@ describe('signer', () => { const signer = makeSigner(testIdentity.privateKey) const signature = await signer.sign(dataToSign) - expect(bytesToHex(signer.address, true)).toEqual(testIdentity.address) + expect(bytesToHex(signer.address)).toEqual(testIdentity.address) expect(signature).toEqual(expectedSignature) }) @@ -36,7 +36,7 @@ describe('signer', () => { const signer = makeSigner(hexToBytes(testIdentity.privateKey)) const signature = await signer.sign(dataToSign) - expect(bytesToHex(signer.address, true)).toEqual(testIdentity.address) + expect(bytesToHex(signer.address)).toEqual(testIdentity.address) expect(signature).toEqual(expectedSignature) }) diff --git a/test/feed/index.spec.ts b/test/feed/index.spec.ts index 1b2ff006..4608f98c 100644 --- a/test/feed/index.spec.ts +++ b/test/feed/index.spec.ts @@ -1,5 +1,5 @@ import { fetchFeedUpdate } from '../../src/modules/feed' -import { HexString, hexToBytes, stripHexPrefix, verifyHex } from '../../src/utils/hex' +import { HexString, hexToBytes, makeHexString } from '../../src/utils/hex' import { beeUrl, testIdentity } from '../utils' import { ChunkReference, downloadFeedUpdate, findNextIndex, Index, uploadFeedUpdate } from '../../src/feed' import { Bytes, verifyBytes } from '../../src/utils/bytes' @@ -37,22 +37,20 @@ async function tryUploadFeedUpdate(url: string, signer: Signer, topic: Topic, in describe('feed', () => { const url = beeUrl() - const owner = stripHexPrefix(testIdentity.address) + const owner = makeHexString(testIdentity.address, 40) const signer = makeDefaultSigner(hexToBytes(testIdentity.privateKey) as PrivateKey) - const topic = '0000000000000000000000000000000000000000000000000000000000000000' as HexString + const topic = '0000000000000000000000000000000000000000000000000000000000000000' as Topic test('empty feed update', async () => { - const emptyTopic = '1000000000000000000000000000000000000000000000000000000000000000' as HexString + const emptyTopic = '1000000000000000000000000000000000000000000000000000000000000000' as Topic const index = await findNextIndex(url, owner, emptyTopic) expect(index).toEqual('0000000000000000') }, 15000) test('feed update', async () => { - const topicBytes = hexToBytes(topic) as Bytes<32> - const uploadedChunk = await uploadChunk(url, 0) - await tryUploadFeedUpdate(url, signer, topicBytes, 0, uploadedChunk) + await tryUploadFeedUpdate(url, signer, topic, 0, uploadedChunk) const feedUpdate = await fetchFeedUpdate(url, owner, topic) @@ -61,21 +59,20 @@ describe('feed', () => { }, 15000) test('multiple updates and lookup', async () => { - const reference = '0000000000000000000000000000000000000000000000000000000000000000' as HexString - const referenceBytes = verifyBytes(32, hexToBytes(verifyHex(reference))) - const multipleUpdateTopic = '3000000000000000000000000000000000000000000000000000000000000000' as HexString - const topicBytes = verifyBytes(32, hexToBytes(multipleUpdateTopic)) + const reference = makeHexString('0000000000000000000000000000000000000000000000000000000000000000', 64) + const referenceBytes = verifyBytes(32, hexToBytes(reference)) + const multipleUpdateTopic = '3000000000000000000000000000000000000000000000000000000000000000' as Topic const numUpdates = 5 for (let i = 0; i < numUpdates; i++) { const referenceI = new Uint8Array([i, ...referenceBytes.slice(1)]) as Bytes<32> - await tryUploadFeedUpdate(url, signer, topicBytes, i, referenceI) + await tryUploadFeedUpdate(url, signer, multipleUpdateTopic, i, referenceI) } for (let i = 0; i < numUpdates; i++) { const referenceI = new Uint8Array([i, ...referenceBytes.slice(1)]) as Bytes<32> - const feedUpdateResponse = await downloadFeedUpdate(url, signer.address, topicBytes, i) + const feedUpdateResponse = await downloadFeedUpdate(url, signer.address, multipleUpdateTopic, i) expect(feedUpdateResponse.reference).toEqual(referenceI) } }, 15000) diff --git a/test/modules/collection.spec.ts b/test/modules/collection.spec.ts index af0f0059..fb2fe6bb 100644 --- a/test/modules/collection.spec.ts +++ b/test/modules/collection.spec.ts @@ -1,5 +1,5 @@ import * as collection from '../../src/modules/collection' -import { Collection, ENCRYPTED_HEX_REFERENCE_LENGTH } from '../../src/types' +import { Collection, ENCRYPTED_REFERENCE_HEX_LENGTH } from '../../src/types' import { beeUrl } from '../utils' const BEE_URL = beeUrl() @@ -65,7 +65,7 @@ describe('modules/collection', () => { expect(file.name).toEqual(directoryStructure[0].path) expect(file.data).toEqual(directoryStructure[0].data) - expect(hash.length).toEqual(ENCRYPTED_HEX_REFERENCE_LENGTH) + expect(hash.length).toEqual(ENCRYPTED_REFERENCE_HEX_LENGTH) }) it('should upload bigger file', async () => { diff --git a/test/modules/debug/chequebook.spec.ts b/test/modules/debug/chequebook.spec.ts index fa8d5899..9bdc0f97 100644 --- a/test/modules/debug/chequebook.spec.ts +++ b/test/modules/debug/chequebook.spec.ts @@ -5,7 +5,7 @@ import { getLastCheques, withdrawTokens, } from '../../../src/modules/debug/chequebook' -import { isHexString } from '../../../src/utils/hex' +import { isPrefixedHexString } from '../../../src/utils/hex' import { beeDebugUrl, sleep } from '../../utils' if (process.env.BEE_TEST_CHEQUEBOOK) { @@ -13,7 +13,7 @@ if (process.env.BEE_TEST_CHEQUEBOOK) { test('address', async () => { const response = await getChequebookAddress(beeDebugUrl()) - expect(isHexString(response.chequebookaddress)).toBeTruthy() + expect(isPrefixedHexString(response.chequebookaddress)).toBeTruthy() }) test('balance', async () => { @@ -32,7 +32,7 @@ if (process.env.BEE_TEST_CHEQUEBOOK) { expect(typeof withdrawResponse.transactionHash).toBe('string') // TODO avoid sleep in tests - // See https://github.com/ethersphere/bee/issues/1191 + // See https://github.com/ethersphere/bee/issues/1191 await sleep(TRANSACTION_TIMEOUT) const depositResponse = await depositTokens(beeDebugUrl(), 10) diff --git a/test/modules/feed.spec.ts b/test/modules/feed.spec.ts index d9767109..a9ed024e 100644 --- a/test/modules/feed.spec.ts +++ b/test/modules/feed.spec.ts @@ -1,12 +1,13 @@ import { createFeedManifest, fetchFeedUpdate } from '../../src/modules/feed' -import { HexString, hexToBytes, stripHexPrefix } from '../../src/utils/hex' +import { HexString, hexToBytes, makeHexString } from '../../src/utils/hex' import { beeUrl, testIdentity, tryDeleteChunkFromLocalStorage } from '../utils' import { upload as uploadSOC } from '../../src/modules/soc' +import type { Topic } from '../../src/feed/topic' describe('modules/feed', () => { const url = beeUrl() - const owner = stripHexPrefix(testIdentity.address) - const topic = '0000000000000000000000000000000000000000000000000000000000000000' as HexString + const owner = makeHexString(testIdentity.address, 40) + const topic = '0000000000000000000000000000000000000000000000000000000000000000' as Topic test('feed manifest creation', async () => { const reference = '92442c3e08a308aeba8e2d231733ec57011a203354cad24129e7e0c37bac0cbe' @@ -16,14 +17,14 @@ describe('modules/feed', () => { }) test('empty feed update', async () => { - const emptyTopic = '1000000000000000000000000000000000000000000000000000000000000000' + const emptyTopic = '1000000000000000000000000000000000000000000000000000000000000000' as Topic const feedUpdate = fetchFeedUpdate(url, owner, emptyTopic) await expect(feedUpdate).rejects.toThrow('Not Found') }, 15000) test('one feed update', async () => { - const oneUpdateTopic = '2000000000000000000000000000000000000000000000000000000000000000' + const oneUpdateTopic = '2000000000000000000000000000000000000000000000000000000000000000' as Topic const identifier = '7c5c4c857ed4cae434c2c737bad58a93719f9b678647310ffd03a20862246a3b' const signature = 'bba40ea2c87b7801f54f5cca70e06deaed5c366b588e38ce0c42f7f8f16562c3243b43101faa6dbaeaab3244b1a0ceaec92dd117995e19116a372eadbec945b01b' diff --git a/test/utils.ts b/test/utils.ts index fd1d5c06..f725bbea 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -157,7 +157,7 @@ export const testChunkData = new Uint8Array([...testChunkSpan, ...testChunkPaylo export const testChunkHash = 'ca6357a08e317d15ec560fef34e4c45f8f19f01c372aa70f1da72bfa7f1a4338' as HexString export const testIdentity = { - privateKey: '0x634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd' as HexString, - publicKey: '0x03c32bb011339667a487b6c1c35061f15f7edc36aa9a0f8648aba07a4b8bd741b4' as HexString, - address: '0x8d3766440f0d7b949a5e32995d09619a7f86e632' as HexString, + privateKey: '634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd' as HexString, + publicKey: '03c32bb011339667a487b6c1c35061f15f7edc36aa9a0f8648aba07a4b8bd741b4' as HexString, + address: '8d3766440f0d7b949a5e32995d09619a7f86e632' as HexString, } diff --git a/test/utils/hex.spec.ts b/test/utils/hex.spec.ts index 4ffcc415..e35bc044 100644 --- a/test/utils/hex.spec.ts +++ b/test/utils/hex.spec.ts @@ -1,76 +1,72 @@ -import { bytesToHex, HexString, hexToBytes, intToHex, isHexString, stripHexPrefix } from '../../src/utils/hex' +import { bytesToHex, HexString, hexToBytes, intToHex, isHexString, makeHexString } from '../../src/utils/hex' describe('hex', () => { - describe('stripHexPrefix', () => { - test('with prefix', () => { - const input = '0xC0FFEE' - const result = stripHexPrefix(input) + // prettier-ignore + const testBytes = new Uint8Array([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]) + const testHex = '00112233445566778899aabbccddeeff' as HexString - expect(result).toBe('C0FFEE') - }) + describe('makeHexString', () => { + describe('strings', () => { + it('should strip prefix from valid prefixed string', () => { + const input = '0xC0fFEE' + const result = makeHexString(input) + + expect(result).toBe('C0fFEE') + }) - test('without prefix', () => { - const input = 'C0FFEE' - const result = stripHexPrefix(input) + it('should return valid non prefixed string', () => { + const input = 'C0FFEE' + const result = makeHexString(input) - expect(result).toBe('C0FFEE') - }) + expect(result).toBe('C0FFEE') + }) - test('empty string', () => { - const result = stripHexPrefix('') + it('should throw for other non valid strings', () => { + expect(() => makeHexString('')).toThrowError(TypeError) + expect(() => makeHexString('COFFEE')).toThrowError(TypeError) + }) - expect(result).toBe('') + it('should validate length if specified', () => { + expect(makeHexString('C0fFEE', 6)).toBe('C0fFEE') + expect(makeHexString('0xC0fFEE', 6)).toBe('C0fFEE') + expect(() => makeHexString('C0fFEE', 5)).toThrowError(TypeError) + expect(() => makeHexString('0xC0fFEE', 7)).toThrowError(TypeError) + }) }) }) describe('isHexString', () => { - test('with hex string', () => { - const input = 'C0FFEE' - const result = isHexString(input) - - expect(result).toBeTruthy() - }) - test('with not hex string', () => { - const input = 'COFFEE' - const result = isHexString(input) - - expect(result).toBeFalsy() + function testCase(input: unknown, result: boolean): void { + it(`should ${result ? 'accept' : 'reject'} input: ${input}`, () => { + expect(isHexString(input)).toEqual(result) + }) + } + + testCase('C0FFEE', true) + testCase('123C0FFEE', true) + testCase('ZACOFFEE', false) + testCase('', false) + testCase(undefined, false) + testCase(null, false) + testCase(1, false) + testCase({}, false) + testCase([], false) + + it('should validate length if specified', () => { + expect(isHexString('C0FFEE', 6)).toEqual(true) + expect(isHexString('C0FFEE', 7)).toEqual(false) }) - test('empty string', () => { - const result = isHexString('') - expect(result).toBeFalsy() - }) - test('chequebookaddress', () => { - const input = '0x20d7855b548C71b69dA434D46187C336BDcef00F' + it('chequebookaddress', () => { + const input = '20d7855b548C71b69dA434D46187C336BDcef00F' const result = isHexString(input) expect(result).toBeTruthy() }) }) - const testBytes = new Uint8Array([ - 0x00, - 0x11, - 0x22, - 0x33, - 0x44, - 0x55, - 0x66, - 0x77, - 0x88, - 0x99, - 0xaa, - 0xbb, - 0xcc, - 0xdd, - 0xee, - 0xff, - ]) - const testHex = '00112233445566778899aabbccddeeff' as HexString - describe('hexToBytes', () => { - test('converts hex to bytes', () => { + it('converts hex to bytes', () => { const input = testHex const result = hexToBytes(input) @@ -79,7 +75,7 @@ describe('hex', () => { }) describe('bytesToHex', () => { - test('converts bytes to hex', () => { + it('converts bytes to hex', () => { const input = testBytes const result = bytesToHex(input) @@ -90,28 +86,30 @@ describe('hex', () => { describe('intToHex', () => { const testValues = [ { value: 1, result: '1' }, - { value: 1, result: '0x1', prefix: true }, - { value: 15, result: '0xf', prefix: true }, - { value: 16, result: '0x10', prefix: true }, + { value: 1, result: '1', length: 1 }, + { value: 15, length: 2, throws: TypeError }, + { value: 16, result: '10', length: 2 }, { value: 16, result: '10' }, { value: 124, result: '7c' }, { value: 28721856816, result: '6aff4c130' }, { value: Number.MAX_SAFE_INTEGER, result: '1fffffffffffff' }, + { value: Number.MAX_SAFE_INTEGER + 1, throws: TypeError }, + { value: 124.1, throws: TypeError }, + { value: 'a', throws: TypeError }, + { value: '0', throws: TypeError }, + { value: -1, throws: TypeError }, ] - testValues.forEach(({ value, result, prefix }) => { - test(`should conver value ${value} to ${result}`, () => { - expect(intToHex(value, prefix)).toBe(result) - }) - }) - - test('should throw for int value higher than MAX_SAFE_INTEGER', () => { - expect(() => intToHex(Number.MAX_SAFE_INTEGER + 1)).toThrow() - }) - - test('should throw for non-positive or non-int', () => { - const testValues = [124.1, 'a', '0', -1, () => {}, new Function()] // eslint-disable-line @typescript-eslint/no-empty-function - testValues.forEach(value => expect(() => intToHex((value as unknown) as number)).toThrow()) + testValues.forEach(({ value, result, length, throws }) => { + if (throws) { + it(`should throw error for value ${value}`, () => { + expect(() => intToHex(value as number, length)).toThrowError(throws) + }) + } else { + it(`should convert value ${value} to ${result}`, () => { + expect(intToHex(value as number, length)).toBe(result) + }) + } }) }) })