DeFiCh/jellyfish

View on GitHub
packages/jellyfish-buffer/src/Composer.ts

Summary

Maintainability
F
4 days
Test Coverage
import BigNumber from 'bignumber.js'
import { SmartBuffer } from 'smart-buffer'
import { readCompactSize, writeCompactSize } from './CompactSize'
import { readVarInt, writeVarInt } from './VarInt'
import { getBitsFrom } from './BitMask'
import { MAX_INT64, ONE_HUNDRED_MILLION, readBigNumberUInt64, writeBigNumberUInt64 } from './BigNumber'

export interface BufferComposer {
  fromBuffer: (buffer: SmartBuffer) => void
  toBuffer: (buffer: SmartBuffer) => void
}

/**
 * A highly composable buffer, by defining a list of composer, it allows bi-directional buffer to object serialization.
 * In short, you compose from a Buffer to Object or an Object to a Buffer. Little endian by design.
 *
 * It is also deeply recursive by default allow cascading object composing.
 */
export abstract class ComposableBuffer<T> implements BufferComposer {
  protected readonly data: T

  abstract composers (data: T): BufferComposer[]

  /**
   * @param data to create ComposableBuffer holder, nothing is done/composed yet.
   */
  constructor (data: T)

  /**
   * @param {SmartBuffer} buffer to compose into Object
   */
  constructor (buffer: SmartBuffer)

  /**
   * For typescript type checking
   */
  constructor (data: SmartBuffer | T)

  constructor (data: SmartBuffer | T) {
    if (data instanceof SmartBuffer) {
      // @ts-expect-error as data will be mapped by fromBuffer()
      this.data = {}
      this.fromBuffer(data)
    } else {
      this.data = data
    }
  }

  fromBuffer (buffer: SmartBuffer): void {
    for (const mapping of this.composers(this.data)) {
      mapping.fromBuffer(buffer)
    }
  }

  toBuffer (buffer: SmartBuffer): void {
    for (const mapping of this.composers(this.data)) {
      mapping.toBuffer(buffer)
    }
  }

  toHex (): string {
    const buffer: SmartBuffer = new SmartBuffer()
    this.toBuffer(buffer)
    return buffer.toString('hex')
  }

  /**
   * Deeply toObject() mapper.
   * This unwrap the data in ComposableBuffer and convert all ComposableBuffer into their raw object.
   * This make it compatible to convert into JSON with JSON.stringify()
   * @return {Object}
   */
  toObject (): any {
    function toObject (value: any): any {
      if (value instanceof ComposableBuffer) {
        return value.toObject()
      }
      if (Array.isArray(value) && value.length > 0) {
        return value.map(v => toObject(v))
      }
      return value
    }

    const json: any = {}
    for (const [key, value] of Object.entries(this.data)) {
      json[key] = toObject(value)
    }
    return json
  }

  /**
   * @return BufferComposer that does nothing
   */
  static empty (): BufferComposer {
    return {
      fromBuffer (_: SmartBuffer): void {
      },
      toBuffer (_: SmartBuffer): void {
      }
    }
  }

  /**
   * The length of the array is set with CompactSize in the first sequence of 1 - 9 bytes.
   *
   * @param getter to read array of ComposableBuffer Object from to buffer
   * @param setter to set array of ComposableBuffer Object from buffer
   * @param asC map single object into ComposableBuffer Object
   *
   * @see array if length is not given but known
   */
  static compactSizeArray<T> (
    getter: () => T[],
    setter: (data: T[]) => void,
    asC: (data: SmartBuffer | T) => ComposableBuffer<T>
  ): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const length = readCompactSize(buffer)
        const array: T[] = []
        for (let i = 0; i < length; i++) {
          array.push(asC(buffer).data)
        }
        setter(array)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const array = getter()
        writeCompactSize(array.length, buffer)
        array.forEach(data => asC(data).toBuffer(buffer))
      }
    }
  }

  /**
   * The length of the array must be known and given to the composer, use compactSizeArray if length is set as CompactSize.
   *
   * @param getter to read array of ComposableBuffer Object from to buffer
   * @param setter to set array of ComposableBuffer Object from buffer
   * @param asC map single object into ComposableBuffer Object
   * @param getLength of the array
   *
   * @see use compactSizeArray if length is set as CompactSize
   */
  static array<T> (
    getter: () => T[],
    setter: (data: T[]) => void,
    asC: (data: SmartBuffer | T) => ComposableBuffer<T>,
    getLength: () => number
  ): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const array: T[] = []
        for (let i = 0; i < getLength(); i++) {
          array.push(asC(buffer).data)
        }
        setter(array)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const array = getter()
        array.forEach(data => asC(data).toBuffer(buffer))
      }
    }
  }

  /**
   * The length depends on the Composable buffer composer configuration
   *
   * @param getter to read single ComposableBuffer Object from to buffer
   * @param setter to set single ComposableBuffer Object from buffer
   * @param asC map object into ComposableBuffer Object
   */
  static single<T> (
    getter: () => T,
    setter: (data: T) => void,
    asC: (data: SmartBuffer | T) => ComposableBuffer<T>
  ): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(asC(buffer).data)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        asC(getter()).toBuffer(buffer)
      }
    }
  }

  /**
   * HEX String with length specified, encoded into Buffer as the same order of the Hex String.
   * In short this read a hex and push it into the Buffer. It will not re-order the endian.
   *
   * @param length of the bytes to read/set
   * @param getter to read HEX String and write as the same ordered Buffer
   * @param setter to read ordered Buffer and set as the same ordered HEX String
   * @throws Error if length != getter().length in set
   */
  static hex (length: number, getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const buff = Buffer.from(buffer.readBuffer(length))
        setter(buff.toString('hex'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const hex = getter()
        if (hex.length !== length * 2) {
          throw new Error('ComposableBuffer.hex.toBuffer invalid as length != getter().length')
        }
        const buff: Buffer = Buffer.from(hex, 'hex')
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * HEX String with length specified, encoded into Buffer as the same order of the Hex String.
   * In short this read a hex and push it into the Buffer. It will not re-order the endian.
   *
   * When hex is `undefined` it will write `0x00` signifying zero byte length.
   *
   * @param getter to read HEX String. Writes its length then write the HEX string. Defaults to empty string.
   * @param setter to read ordered Buffer and set as the same ordered HEX String
   */
  static compactSizeOptionalHex (getter: () => string | undefined, setter: (data: string | undefined) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const length = readCompactSize(buffer)
        if (length > 0) {
          const buff = Buffer.from(buffer.readBuffer(length))
          setter(buff.toString('hex'))
        }
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const hex = getter()
        if (hex !== undefined) {
          if (hex === '') {
            throw new Error('ComposableBuffer.CompactSizeOptionalHex.toBuffer attempting to write empty buffer')
          }
          const buff: Buffer = Buffer.from(hex, 'hex')
          writeCompactSize(buff.length, buffer)
          buffer.writeBuffer(buff)
        } else {
          writeCompactSize(0x00, buffer)
        }
      }
    }
  }

  /**
   * Same behavior with `hex` when the field is defined
   * `toBuffer` resulted empty SmartBuffer
   *
   * @param length of the bytes to read/set
   * @param getter to read HEX String and write as the same ordered Buffer
   * @param setter to read ordered Buffer and set as the same ordered HEX String
   * @throws Error if getter() is defined && length != getter().length in set
   */
  static optionalHex (length: number, getter: () => string | undefined, setter: (data: string | undefined) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const buff = Buffer.from(buffer.readBuffer(length))
        if (buff.length > 0) {
          setter(buff.toString('hex'))
        }
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const hex = getter()
        if (hex === undefined) {
          return
        }
        if (hex.length !== length * 2) {
          throw new Error('ComposableBuffer.optionalHex.toBuffer invalid as length != getter().length')
        }
        buffer.writeBuffer(Buffer.from(hex, 'hex'))
      }
    }
  }

  /**
   * BE ordered HEX String with length specified, encoded in LE order buffer.
   * Different from BufferComposer.hex, this will reorder the Buffer from LE to BE and BE to LE.
   *
   * @param length of the bytes to read/set
   * @param getter to read BE ordered HEX String and write as LE ordered Buffer
   * @param setter to read LE ordered Buffer and set as BE ordered HEX String
   * @throws Error if length != getter().length in set
   */
  static hexBEBufferLE (length: number, getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const buff = Buffer.from(buffer.readBuffer(length)).reverse()
        setter(buff.toString('hex'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const hex = getter()
        if (hex.length !== length * 2) {
          throw new Error('ComposableBuffer.hexBEBufferLE.toBuffer invalid as length != getter().length')
        }
        const buff: Buffer = Buffer.from(hex, 'hex').reverse()
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * UTF-8 string with length specified, encoded in LE order buffer.
   * String is always BE, as Javascript is uses BE by default.
   *
   * @param length of the bytes to read/set
   * @param getter to read BE ordered String and write as LE ordered Buffer
   * @param setter to read LE ordered Buffer and set as BE ordered String
   * @throws Error if length != getter().length in set
   */
  static utf8LE (length: number, getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const buff = Buffer.from(buffer.readBuffer(length)).reverse()
        setter(buff.toString('utf-8'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const buff: Buffer = Buffer.from(getter(), 'utf-8').reverse()
        if (buff.length !== length) {
          throw new Error('ComposableBuffer.utf8LE.toBuffer invalid as length != getter().length')
        }
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * UTF-8 string with length specified, encoded in BE order buffer.
   * String is always BE, as Javascript is uses BE by default.
   *
   * @param length of the bytes to read/set
   * @param getter to read BE ordered String and write as BE ordered Buffer
   * @param setter to read BE ordered Buffer and set as BE ordered String
   * @throws Error if length != getter().length in set
   */
  static utf8BE (length: number, getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const buff = Buffer.from(buffer.readBuffer(length))
        setter(buff.toString('utf-8'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const buff: Buffer = Buffer.from(getter(), 'utf-8')
        if (buff.length !== length) {
          throw new Error('ComposableBuffer.utf8BE.toBuffer invalid as length != getter().length')
        }
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * CompactSize sized UTF-8 string, encoded in LE order buffer.
   * String is always BE, as Javascript is uses BE by default.
   *
   * @param getter to read BE ordered String and write as LE ordered Buffer
   * @param setter to read LE ordered Buffer and set as BE ordered String
   */
  static compactSizeUtf8LE (getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const length = readCompactSize(buffer)
        const buff = Buffer.from(buffer.readBuffer(length)).reverse()
        setter(buff.toString('utf-8'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const buff: Buffer = Buffer.from(getter(), 'utf-8').reverse()
        writeCompactSize(buff.length, buffer)
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * CompactSize sized UTF-8 string, encoded in BE order buffer.
   * String is always BE, as Javascript is uses BE by default.
   *
   * @param getter to read BE ordered String and write as BE ordered Buffer
   * @param setter to read BE ordered Buffer and set as BE ordered String
   */
  static compactSizeUtf8BE (getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const length = readCompactSize(buffer)
        const buff = Buffer.from(buffer.readBuffer(length))
        setter(buff.toString('utf-8'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const buff: Buffer = Buffer.from(getter(), 'utf-8')
        writeCompactSize(buff.length, buffer)
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * CompactSize sized hex string, encoded into Buffer as the same order of the hex String.
   * In short this read a CompactSize sized hex and push it into the Buffer. It will not re-order the endian.
   *
   * @param getter to read hex String and write as the same ordered Buffer
   * @param setter to read ordered Buffer and set as the same ordered hex String
   */
  static compactSizeHex (getter: () => string, setter: (data: string) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const length = readCompactSize(buffer)
        const buff = Buffer.from(buffer.readBuffer(length))
        setter(buff.toString('hex'))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const buff: Buffer = Buffer.from(getter(), 'hex')
        writeCompactSize(buff.length, buffer)
        buffer.writeBuffer(buff)
      }
    }
  }

  /**
   * Unsigned Int8, 1 byte
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static uInt8 (getter: () => number, setter: (data: number) => any): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readUInt8())
      },
      toBuffer: (buffer: SmartBuffer): void => {
        buffer.writeUInt8(getter())
      }
    }
  }

  /**
   * Unsigned Int16, 2 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static uInt16 (getter: () => number, setter: (data: number) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readUInt16LE())
      },
      toBuffer: (buffer: SmartBuffer): void => {
        buffer.writeUInt16LE(getter())
      }
    }
  }

  /**
   * Signed Int32, 4 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static int32 (getter: () => number, setter: (data: number) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readInt32LE())
      },
      toBuffer: (buffer: SmartBuffer): void => {
        buffer.writeInt32LE(getter())
      }
    }
  }

  /**
   * Unsigned Int32, 4 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static uInt32 (getter: () => number, setter: (data: number) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readUInt32LE())
      },
      toBuffer: (buffer: SmartBuffer): void => {
        buffer.writeUInt32LE(getter())
      }
    }
  }

  /**
   * Unsigned BigNumber, 8 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static bigNumberUInt64 (getter: () => BigNumber, setter: (data: BigNumber) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(readBigNumberUInt64(buffer))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        writeBigNumberUInt64(getter(), buffer)
      }
    }
  }

  /**
   * Unsigned satoshi as BigNumber, 8 bytes
   * BigNumber is multiplied/divided by 100,000,000
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static satoshiAsBigNumber (getter: () => BigNumber, setter: (data: BigNumber) => void): BufferComposer {
    return ComposableBuffer.bigNumberUInt64(() => {
      return getter().multipliedBy(ONE_HUNDRED_MILLION)
    }, v => {
      setter(v.dividedBy(ONE_HUNDRED_MILLION))
    })
  }

  /**
   * CompactSize helper method, 1 - 9 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static compactSize (getter: () => number, setter: (data: number) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(readCompactSize(buffer))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        writeCompactSize(getter(), buffer)
      }
    }
  }

  /**
   * VarInt helper method, 1 - 9 bytes
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static varInt (getter: () => number, setter: (data: number) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(readVarInt(buffer))
      },
      toBuffer: (buffer: SmartBuffer): void => {
        writeVarInt(getter(), buffer)
      }
    }
  }

  /**
   * Imposing mask over bits method, 1 byte
   *
   * @param length of the input array to read/set
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static bitmask1Byte (
    length: number,
    getter: () => boolean[],
    setter: (data: boolean[]) => void
  ): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const num = buffer.readUInt8()
        const array: boolean[] = []
        for (let i = 0; i < length; i += 1) {
          array.unshift(getBitsFrom(num, i))
        }
        setter(array)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const bools = getter().map(bool => bool.toString().toLowerCase() === 'true' ? 1 : 0)
        const num = parseInt(bools.join(''), 2)
        buffer.writeBuffer(Buffer.from([num]))
      }
    }
  }

  /**
   * Unsigned Boolean 1 byte, 1 = true, 0 = false
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static uBool8 (getter: () => boolean, setter: (data: boolean) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readUInt8() === 1)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const v = getter() ? 1 : 0
        buffer.writeBuffer(Buffer.from([v]))
      }
    }
  }

  /**
   * Unsigned Boolean 4 bytes, 1 = true, 0 = false
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static uBool32 (getter: () => boolean, setter: (data: boolean) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        setter(buffer.readInt32BE() === 1)
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const v = getter() ? 1 : 0
        buffer.writeBuffer(Buffer.from([0, 0, 0, v]))
      }
    }
  }

  /**
   * maxPrice as BigNumber, first 8 bytes for integer, last 8 bytes for fraction
   *
   * @param getter to read from to buffer
   * @param setter to set to from buffer
   */
  static maxPriceAsBigNumber (getter: () => BigNumber, setter: (data: BigNumber) => void): BufferComposer {
    return {
      fromBuffer: (buffer: SmartBuffer): void => {
        const integer = readBigNumberUInt64(buffer)
        const fraction = readBigNumberUInt64(buffer)

        // Disabled as it's not enforced
        // NOTE(canonbrother): max price default value is max int64 (9223372036854775807)
        // https://github.com/DeFiCh/ain/blob/aed00d09075094a3a0cedebde5248c006216ba09/src/masternodes/rpc_poolpair.cpp#L115-L123
        // if (fraction.gt(new BigNumber('99999999')) && !fraction.eq(MAX_INT64)) {
        //   throw new Error('Too many decimals read from buffer. Will lose precision with more than 8 decimals')
        // }

        if (integer.eq(MAX_INT64) && fraction.eq(MAX_INT64)) {
          setter(MAX_INT64)
        } else {
          setter(integer.plus(fraction.dividedBy(ONE_HUNDRED_MILLION)))
        }
      },
      toBuffer: (buffer: SmartBuffer): void => {
        const maxPrice = getter()
        if (maxPrice.decimalPlaces() > 8) {
          throw new Error('Too many decimals to be correctly represented. Will lose precision with more than 8 decimals')
        }

        if (maxPrice.eq(MAX_INT64)) {
          writeBigNumberUInt64(MAX_INT64, buffer)
          writeBigNumberUInt64(MAX_INT64, buffer)
        } else {
          const n = maxPrice.multipliedBy(ONE_HUNDRED_MILLION)
          const fraction = n.mod(ONE_HUNDRED_MILLION)
          const integer = n.minus(fraction).dividedBy(ONE_HUNDRED_MILLION)
          writeBigNumberUInt64(integer, buffer)
          writeBigNumberUInt64(fraction, buffer)
        }
      }
    }
  }
}