base/operation.js

import { default as xdr } from './generated/xdr_generated'
import { Keypair } from './keypair'
import { UnsignedHyper, Hyper } from 'js-xdr'
import { encodeCheck } from './strkey'
import isUndefined from 'lodash/isUndefined'
import isString from 'lodash/isString'
import isBoolean from 'lodash/isBoolean'

import BigNumber from 'bignumber.js'
import { Hasher } from './util/hasher'

// TODO: split

const CURRENCY_PRECISION = 2

/**
 * Lot types.
 */
export const lotTypes = Object.freeze({
  noAuction: 'lotTypeNoAuction',
  englishAuction: 'lotTypeEng'
})

/**
 * Auction close actions.
 */
export const closeAuctionActions = Object.freeze({
  markSold: 'closeAuctionActionMarkSold',
  markFinished: 'closeAuctionActionMarkFinished'
})

/**
 * Manage participant actions.
 */
export const manageParticipantActions = Object.freeze({
  chooseWinner: 'manageParticipantActionChooseWinner',
  reject: 'manageParticipantActionReject',
  approveBuyNow: 'manageParticipantActionApproveBuyNow'
})

/**
 * Review request actions.
 */
export const reviewRequestActions = Object.freeze({
  approve: 'reviewRequestActionApprove',
  reject: 'reviewRequestActionReject',
  permanentReject: 'reviewRequestActionPermanentReject'
})

/**
 * Reviewable request types.
 */
export const reviewableRequestTypes = Object.freeze({
  lot: 'requestTypeCreateLot',
  participation: 'requestTypeRequestParticipation',
  bid: 'requestTypeCreateBid'
})

/**
 * Account types.
 */
export const accountTypes = Object.freeze({
  master: 'accountTypeMaster',
  platform: 'accountTypePlatform',
  dealer: 'accountTypeDealer',
  user: 'accountTypeUser',
  admin: 'accountTypeAdmin'
})

/**
 * `Operation` class represents [operations](https://www.stellar.org/developers/learn/concepts/operations.html) in Stellar network.
 * Use one of static methods to create operations:
 * * `{@link Operation.createAccount}`
 * * `{@link Operation.payment}`
 * * `{@link Operation.pathPayment}`
 * * `{@link Operation.manageOffer}`
 * * `{@link Operation.createPassiveOffer}`
 * * `{@link Operation.setOptions}`
 * * `{@link Operation.changeTrust}`
 * * `{@link Operation.allowTrust}`
 * * `{@link Operation.accountMerge}`
 * * `{@link Operation.inflation}`
 *
 * @class Operation
 */
export class Operation {
  /**
   * Create and fund a non existent account.
   * @param {object} opts
   * @param {string} opts.destination - Destination account ID to create an account for.
   * @param {string} opts.platformId - id of platform account belongs to
   * @param {xdr.AccountType} opts.accountType - account type
   * @param {string} [opts.source] - The source account for the payment. Defaults to the transaction's source account.
   * @returns {xdr.CreateAccountOp}
   */
  static createAccount (opts) {
    if (!Keypair.isValidAccountId(opts.destination)) {
      throw new Error('destination is invalid')
    }
    if (!Keypair.isValidAccountId(opts.destination)) {
      throw new Error('platformId is invalid')
    }

    if (opts.accountType === undefined) {
      opts.accountType = accountTypes.user
    }

    let attributes = {
      destination: Keypair.fromAccountId(opts.destination).xdrAccountId(),
      accountType: xdr.AccountType.fromName(opts.accountType),
      platformId: Keypair.fromAccountId(opts.platformId).xdrAccountId(),
      ext: new xdr.CreateAccountOpExt('emptyVersion')
    }
    let createAccountOp = new xdr.CreateAccountOp(attributes)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.createAccount(createAccountOp)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Creates a lot.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.requestId Lot creation request ID.
   * @param {string} opts.type Lot type.
   * @param {string} opts.startPrice Start price.
   * @param {string} opts.minStep Minimum bid step.
   * @param {string} opts.maxStep Maximum bid step.
   * @param {string} opts.duration Auction duration in seconds.
   * @param {string} opts.details Lot details.
   * @param {boolean} [opts.buyNowSupport] Allow "buy now" feature for this lot.
   * @param {string} [opts.buyNowPrice] "Buy now" price.
   * @param {string} [opts.deposit] Required  deposit amount.
   * @param {string} opts.startTime Auction start time.
   * @param {string} opts.currency Lot's currency.
   *
   * @return {Operation}
   */
  static createLot (opts) {
    if (!this.isValidBigNumeric(opts.startPrice, CURRENCY_PRECISION, true)) {
      throw new TypeError('startPrice argument must be of type String and represent a positive number with valid precision or zero')
    }

    if (
      !(
        isUndefined(opts.deposit) ||
        this.isValidBigNumeric(opts.deposit, CURRENCY_PRECISION)
      )
    ) {
      throw new TypeError('deposit argument must be of type String and represent a positive number with valid precision')
    }
    if (!this.isValidBigNumeric(opts.minStep, CURRENCY_PRECISION, true)) {
      throw new TypeError('minStep argument must be of type String and represent a positive number with valid precision or zero')
    }
    if (!this.isValidBigNumeric(opts.maxStep, CURRENCY_PRECISION, true)) {
      throw new TypeError('maxStep argument must be of type String and represent a positive number with valid precision or zero')
    }
    if (!this.isValidBigNumeric(opts.duration, 0, true)) {
      throw new TypeError('duration argument must be of type String and represent a positive number or zero')
    }
    if (!this.isValidBigNumeric(opts.startTime, 0, true)) {
      throw new TypeError('startTime argument must be of type String and represent a positive number or zero')
    }
    if (!this.isValidBigNumeric(opts.buyNowPrice, CURRENCY_PRECISION, true)) {
      throw new Error('buyNowPrice argument must be of type String and represent a positive number or zero')
    }
    if (!this.isValidBigNumeric(opts.requestId, 0, true)) {
      throw new TypeError('requestId argument must be of type String and represent positivie number or zero')
    }
    if (!(isUndefined(opts.buyNowSupport) || isBoolean(opts.buyNowSupport))) {
      throw new TypeError('buyNowSupport argument must be of type Boolean')
    }
    if (!this.isValidString(opts.currency, 3, 3)) {
      throw new TypeError('currency argument must be of type String with length 3')
    }

    let attributes = {}

    attributes.requestId = UnsignedHyper.fromString(opts.requestId)
    attributes.lotType = xdr.LotType.fromName(opts.type)
    attributes.buyNowSupport = opts.buyNowSupport || false
    attributes.startPrice = this._toXDRPrice(opts.startPrice)
    attributes.minStep = this._toXDRPrice(opts.minStep)
    attributes.maxStep = this._toXDRPrice(opts.maxStep)
    attributes.buyNowPrice = this._toXDRPrice(opts.buyNowPrice || '0')
    attributes.deposit = this._toXDRPrice(opts.deposit || '0')
    attributes.details = opts.details
    attributes.duration = UnsignedHyper.fromString(opts.duration || '0')
    attributes.startTime = Hyper.fromString(opts.startTime)
    attributes.currency = opts.currency
    attributes.ext = new xdr.CreateLotOpExt(xdr.LedgerVersion.emptyVersion())

    let createLotOp = new xdr.CreateLotOp(attributes)
    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.createLot(createLotOp)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   *  Approve or reject reviewable request
   *
   *  @param {object} opts
   *  @param {string} opts.requestId - id of reviewable request
   *  @param {string} opts.hash - hash of the body a request (need this to ensure that we review what we expect)
   *  @param {string} opts.requestType - type of the request to review
   *  @param {string} opts.action - on of the {approve, reject, permanentReject}
   *  @param {string} opts.rejectReason - reason of reject or permanent reject (for approve pass empty string)
   */
  static reviewRequest (opts) {
    let attrs = {}
    if (isUndefined(opts.requestId) || opts.requestId === '0') {
      throw new Error('opts.requestId is invalid')
    }
    attrs.requestId = UnsignedHyper.fromString(opts.requestId)

    attrs.hash = Hasher.hash(opts.hash)

    if (!this.isValidString(opts.rejectReason, 0, 256)) {
      throw new Error('opts.rejectReason is invalid')
    }
    attrs.rejectReason = opts.rejectReason

    if (!Object.values(reviewRequestActions).find(x => x === opts.action)) {
      throw new Error(`action must be one of the following: ${Object.keys(reviewRequestActions)}`)
    }
    attrs.action = xdr.ReviewRequestAction.fromName(opts.action)

    if (!Object
      .values(reviewableRequestTypes)
      .find(x => x === opts.requestType)) {
      throw new Error(`requestType must be one of the following: ${Object.keys(reviewableRequestTypes)}`)
    }

    let requestType = xdr.ReviewableRequestType.fromName(opts.requestType)
    attrs.requestDetails = new xdr.ReviewRequestOpRequestDetails(requestType)
    attrs.ext = new xdr.ReviewRequestOpExt(xdr.LedgerVersion.emptyVersion())

    let reviewRequestOp = new xdr.ReviewRequestOp(attrs)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.reviewRequest(reviewRequestOp)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * CancelRequestOp
   * cancels request with a given id.
   * Only creator of the request is allowed to perform this operation.
   *
   * @param {string} opts.requestId - id of the request to cancel
   * @param {string} opts.source - source of the operation
   * @return {xdr.Operation}
   */
  static cancelRequest (opts) {
    let attrs = {}

    if (isUndefined(opts.requestId) || opts.requestId <= '0') {
      throw new Error('opts.requestId must be a string and represent a positive number')
    }

    attrs.requestId = UnsignedHyper.fromString(opts.requestId)
    attrs.ext = new xdr.CancelRequestOpExt(xdr.LedgerVersion.emptyVersion())

    let cancelRequestOp = new xdr.CancelRequestOp(attrs)

    let opAttrs = {}
    opAttrs.body = new xdr.OperationBody.cancelRequest(cancelRequestOp)
    this.setSourceAccount(opAttrs, opts)

    return new xdr.Operation(opAttrs)
  }

  /**
   * Request "buy now".
   *
   * @param {object} opts
   * @param {string} opts.lotId ID of the lot to be bought.
   *
   * @return {xdr.RequestBuyNowOp}
   */
  static requestBuyNow (opts) {
    let attributes = {}
    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.ext = new xdr.RequestBuyNowOpExt(
      xdr.LedgerVersion.emptyVersion()
    )
    let requestBuyNowOp = new xdr.RequestBuyNowOp(attributes)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.requestBuyNow(requestBuyNowOp)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Create a recovery op.
   *
   * @param {object} opts
   * @param {string} opts.account - The target account to recover
   * @param {string} opts.newSigner - Signer to recover to.
   * @param {string} [opts.source] - The source account for the payment. Defaults to the transaction's source account.
   * @returns {xdr.RecoverOp}
   */
  static recover (opts) {
    if (!Keypair.isValidPublicKey(opts.account)) {
      throw new TypeError('account is invalid')
    }
    if (!Keypair.isValidPublicKey(opts.newSigner)) {
      throw new TypeError('newSigner is invalid')
    }

    let attributes = {
      ext: new xdr.RecoverOpExt(xdr.LedgerVersion.emptyVersion())
    }
    attributes.account = Keypair.fromAccountId(opts.account).xdrAccountId()
    attributes.newSigner = Keypair.fromAccountId(opts.newSigner).xdrAccountId()

    let recover = new xdr.RecoverOp(attributes)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.recover(recover)
    Operation.setSourceAccount(opAttributes, opts)
    return new xdr.Operation(opAttributes)
  }

  /**
   * Create a bid.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.lotId ID of the lot.
   * @param {string} opts.amount Amount of the bid.
   *
   * @return {Operation}
   */
  static createBid (opts) {
    if (!this.isValidBigNumeric(opts.lotId, 0, true)) {
      throw new TypeError('lotId argument must be of type String and represent positivie number or zero')
    }
    if (!this.isValidBigNumeric(opts.amount, CURRENCY_PRECISION)) {
      throw new TypeError('amount argument must be of type String and represent a positive number with valid precision')
    }
    let attributes = {}
    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.amount = this._toXDRPrice(opts.amount)
    attributes.ext = new xdr.CreateBidOpExt(xdr.LedgerVersion.emptyVersion())
    let createBid1 = new xdr.CreateBidOp(attributes)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.createBid(createBid1)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Leave a message for the lot.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.lotId The lot to which this message is assigned to.
   * @param {string} opts.receiverId accountId of the receiver.
   * @param {string} opts.text Message content.
   *
   * @return {Operation}
   *
   */
  static sendMessage (opts) {
    let attributes = {}

    if (!this.isValidBigNumeric(opts.lotId, 0, true)) {
      throw new TypeError('lotId argument must be of type String and represent positivie number or zero')
    }

    if (!Keypair.isValidPublicKey(opts.receiverId)) {
      throw new TypeError('receiverId is invalid')
    }

    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.receiverId = Keypair
      .fromAccountId(opts.receiverId)
      .xdrAccountId()

    attributes.text = opts.text
    attributes.ext = new xdr.SendMessageOpExt(xdr.LedgerVersion.emptyVersion())

    let sendMessage = new xdr.SendMessageOp(attributes)
    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.sendMessage(sendMessage)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Returns an XDR SetOptionsOp. A "set options" operations set or clear account flags,
   * set the account's inflation destination, and/or add new signers to the account.
   * The account flags are the xdr.AccountFlags enum, which are:
   *   - AUTH_REQUIRED_FLAG = 0x1
   *   - AUTH_REVOCABLE_FLAG = 0x2
   * @param {object} opts
   * @param {string} [opts.inflationDest] - Set this account ID as the account's inflation destination.
   * @param {number} [opts.clearFlags] - Bitmap integer for which flags to clear.
   * @param {number} [opts.setFlags] - Bitmap integer for which flags to set.
   * @param {number} [opts.masterWeight] - The master key weight.
   * @param {number} [opts.lowThreshold] - The sum weight for the low threshold.
   * @param {number} [opts.medThreshold] - The sum weight for the medium threshold.
   * @param {number} [opts.highThreshold] - The sum weight for the high threshold.
   * @param {object} [opts.signer] - Add or remove a signer from the account. The signer is
   *                                 deleted if the weight is 0.
   * @param {string} [opts.signer.address] - The address of the new signer.
   * @param {number} [opts.signer.weight] - The weight of the new signer (0 to delete or 1-255)
   * @param {string} [opts.homeDomain] - sets the home domain used for reverse federation lookup.
   * @param {string} [opts.source] - The source account (defaults to transaction source).
   * @returns {xdr.SetOptionsOp}
   */
  static setOptions (opts) {
    let attributes = {}

    attributes.clearFlags = opts.clearFlags
    attributes.setFlags = opts.setFlags

    let masterWeightInvalid = !isUndefined(opts.masterWeight) &&
      (opts.masterWeight < 0 || opts.masterWeight > 255)
    if (masterWeightInvalid) {
      throw new Error('masterWeight value must be between 0 and 255')
    }

    let lowThresholdInvalid = !isUndefined(opts.lowThreshold) &&
      (opts.lowThreshold < 0 || opts.lowThreshold > 255)
    if (lowThresholdInvalid) {
      throw new Error('lowThreshold value must be between 0 and 255')
    }

    let medThresholdInvalid = !isUndefined(opts.medThreshold) &&
      (opts.medThreshold < 0 || opts.medThreshold > 255)
    if (medThresholdInvalid) {
      throw new Error('medThreshold value must be between 0 and 255')
    }

    let highThresholdInvalid = !isUndefined(opts.highThreshold) &&
      (opts.highThreshold < 0 || opts.highThreshold > 255)
    if (highThresholdInvalid) {
      throw new Error('highThreshold value must be between 0 and 255')
    }

    attributes.masterWeight = opts.masterWeight
    attributes.lowThreshold = opts.lowThreshold
    attributes.medThreshold = opts.medThreshold
    attributes.highThreshold = opts.highThreshold

    if (!isUndefined(opts.homeDomain) && !isString(opts.homeDomain)) {
      throw new TypeError('homeDomain argument must be of type String')
    }
    attributes.homeDomain = opts.homeDomain

    if (opts.signer) {
      if (!Keypair.isValidAccountId(opts.signer.address)) {
        throw new Error('signer.address is invalid')
      }

      if (opts.signer.weight < 0 || opts.signer.weight > 255) {
        throw new Error('signer.weight value must be between 0 and 255')
      }

      attributes.signer = new xdr.Signer({
        pubKey: Keypair.fromAccountId(opts.signer.address).xdrAccountId(),
        weight: opts.signer.weight,
        ext: new xdr.SignerExt(xdr.LedgerVersion.emptyVersion())
      })
    }
    attributes.ext = new xdr.SetOptionsOpExt(xdr.LedgerVersion.emptyVersion())
    let setOptionsOp = new xdr.SetOptionsOp(attributes)

    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.setOption(setOptionsOp)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Make a request to participate in an auction.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.lotId ID of the lot.
   * @param {boolean} opts.isBuyNow do buy now after registration
   *
   * @return {Operation}
   */
  static requestParticipation (opts) {
    let attributes = {}

    if (!this.isValidBigNumeric(opts.lotId, 0, true)) {
      throw new TypeError('lotId argument must be of type String and represent positivie number or zero')
    }

    if (isUndefined(opts.isBuyNow)) {
      opts.isBuyNow = false
    }

    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.isBuyNow = opts.isBuyNow
    attributes.ext = new xdr.RequestParticipationOpExt(
      xdr.LedgerVersion.emptyVersion()
    )

    let request = new xdr.RequestParticipationOp(attributes)
    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.requestParticipation(request)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Close the auction.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.lotId ID of the lot.
   * @param {string} opts.action Auction resolution action.
   *
   * @return {xdr.CloseAuctionOp}
   */

  static closeAuction (opts) {
    let attributes = {}

    if (!Object.values(closeAuctionActions).find(x => x === opts.action)) {
      throw new Error(`actions must be on the following: ${Object.keys(closeAuctionActions)}`)
    }
    if (!this.isValidBigNumeric(opts.lotId, 0, true)) {
      throw new TypeError('lotId argument must be of type String and represent positivie number or zero')
    }

    attributes.action = xdr.CloseAuctionAction.fromName(opts.action)
    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.ext = new xdr.CloseAuctionOpExt(
      xdr.LedgerVersion.emptyVersion()
    )

    let closeAuction = new xdr.CloseAuctionOp(attributes)
    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.closeAuction(closeAuction)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  /**
   * Manage participant.
   *
   * @param {object} opts Operation options.
   * @param {string} opts.lotId ID of the lot.
   * @param {string} opts.action Participant manage action.
   * @param {string} opts.accountId Participant's account ID.

   *
   * @return {xdr.manageParticipantOp}
   */

  static manageParticipant (opts) {
    let attributes = {}

    if (!Object.values(manageParticipantActions).find(x => x === opts.action)) {
      throw new Error(`actions must be on the following: ${Object.keys(manageParticipantActions)}`)
    }
    if (!this.isValidBigNumeric(opts.lotId, 0, true)) {
      throw new TypeError('lotId argument must be of type String and represent positivie number or zero')
    }
    if (!Keypair.isValidPublicKey(opts.accountId)) {
      throw new TypeError('accountId is invalid')
    }

    attributes.action = xdr.ManageParticipantAction.fromName(opts.action)
    attributes.lotId = UnsignedHyper.fromString(opts.lotId)
    attributes.accountId = Keypair.fromAccountId(opts.accountId).xdrAccountId()
    attributes.ext = new xdr.ManageParticipantOpExt(
      xdr.LedgerVersion.emptyVersion()
    )

    let manageParticipant = new xdr.ManageParticipantOp(attributes)
    let opAttributes = {}
    opAttributes.body = xdr.OperationBody.manageParticipant(manageParticipant)
    this.setSourceAccount(opAttributes, opts)

    return new xdr.Operation(opAttributes)
  }

  static setSourceAccount (opAttributes, opts) {
    if (opts.source) {
      if (!Keypair.isValidAccountId(opts.source)) {
        throw new Error('Source address is invalid')
      }
      opAttributes.sourceAccount = Keypair
        .fromAccountId(opts.source)
        .xdrAccountId()
    }

    opAttributes.ext = new xdr.OperationExt(xdr.LedgerVersion.emptyVersion())
  }

  /**
   * Converts the XDR Operation object to the opts object used to create the XDR
   * operation.
   * @param {xdr.Operation} operation - An XDR Operation.
   * @return {Operation}
   */
  static operationToObject (operation) {
    function accountIdtoAddress (accountId) {
      return encodeCheck('accountId', accountId.ed25519())
    }

    let result = {}
    if (operation.sourceAccount()) {
      result.source = accountIdtoAddress(operation.sourceAccount())
    }

    let attrs = operation.body().value()
    switch (operation.body().switch().name) {
      case 'createLot':
        result.type = 'createLot'
        result.requestId = Operation._fromXDRBigNumeric(attrs.requestId())
        result.lotType = attrs.lotType().name
        result.buyNowSupport = attrs.buyNowSupport()
        result.startPrice = Operation._fromXDRBigNumeric(attrs.startPrice())
        result.buyNowPrice = Operation._fromXDRBigNumeric(attrs.buyNowPrice())
        result.minStep = Operation._fromXDRBigNumeric(attrs.minStep())
        result.maxStep = Operation._fromXDRBigNumeric(attrs.maxStep())
        result.deposit = Operation._fromXDRBigNumeric(attrs.deposit())
        result.startTime = Operation._fromXDRBigNumeric(attrs.startTime())
        result.currency = attrs.currency().toString('utf8')
        result.duration = Operation._fromXDRBigNumeric(attrs.duration())
        result.details = attrs.details().toString('utf8')
        break
      case 'reviewRequest':
        result.type = 'reviewRequest'
        result.requestId = attrs.requestId().toString()
        result.hash = attrs.hash().toString('hex')
        result.requestType = attrs.requestDetails().switch().name
        result.action = attrs.action().name
        result.rejectReason = attrs.rejectReason().toString('utf8')
        break
      case 'cancelRequest':
        result.type = 'cancelRequest'
        result.requestId = attrs.requestId().toString()
        break
      case 'changeLot':
        result.type = 'changeLot'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.details = attrs.details().toString('utf8')
        break
      case 'requestBuyNow':
        result.type = 'requestBuyNow'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        break
      case 'reviewBuyNow':
        result.type = 'reviewBuyNow'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.accountId = accountIdtoAddress(attrs.accountId())
        result.paperHash = attrs.paperHash()
        result.paperLink = attrs.paperLink()
        result.isApproved = attrs.isApproved()
        break
      case 'recover':
        result.type = 'recover'
        result.account = accountIdtoAddress(attrs.account())
        result.newSigner = accountIdtoAddress(attrs.newSigner())
        break
      case 'createBid':
        result.type = 'createBid'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.amount = Operation._fromXDRBigNumeric(attrs.amount())
        break
      case 'requestParticipation':
        result.type = 'requestParticipation'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.isBuyNow = attrs.isBuyNow()
        break
      case 'createAccount':
        result.type = 'createAccount'
        result.destination = accountIdtoAddress(attrs.destination())
        result.platformId = accountIdtoAddress(attrs.platformId())
        result.accountType = attrs.accountType()
        break
      case 'sendMessage':
        result.type = 'sendMessage'
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.text = attrs.text().toString('utf8')
        result.receiverId = accountIdtoAddress(attrs.receiverId())
        break
      case 'setOption':
        result.type = 'setOptions'
        if (attrs.inflationDest()) {
          result.inflationDest = accountIdtoAddress(attrs.inflationDest())
        }

        result.clearFlags = attrs.clearFlags()
        result.setFlags = attrs.setFlags()
        result.masterWeight = attrs.masterWeight()
        result.lowThreshold = attrs.lowThreshold()
        result.medThreshold = attrs.medThreshold()
        result.highThreshold = attrs.highThreshold()
        result.homeDomain = attrs.homeDomain()

        if (attrs.signer()) {
          let signer = {}
          signer.address = accountIdtoAddress(attrs.signer().pubKey())
          signer.weight = attrs.signer().weight()
          result.signer = signer
        }
        break
      case 'closeAuction':
        result.type = 'closeAuction'
        result.action = attrs.action().name
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        break
      case 'manageParticipant':
        result.type = 'manageParticipant'
        result.action = attrs.action().name
        result.lotId = Operation._fromXDRBigNumeric(attrs.lotId())
        result.accountId = accountIdtoAddress(attrs.accountId())
        break
      default:
        throw new Error('Unknown operation')
    }
    return result
  }

  static isValidString (value, minSize, maxSize) {
    if (!isString(value)) {
      return false
    }

    if (!isUndefined(minSize) && value.length < minSize) {
      return false
    }

    if (!isUndefined(maxSize) && value.length > maxSize) {
      return false
    }

    return true
  }

  static isValidBigNumeric (value, decimalPlaces = 0, allowZero = false) {
    if (!isString(value)) {
      return false
    }

    let numeric
    try {
      numeric = new BigNumber(value)
    } catch (e) {
      return false
    }

    // == 0
    if (!allowZero && numeric.isZero()) {
      return false
    }

    // < 0
    if (numeric.isNegative()) {
      return false
    }

    if (numeric.decimalPlaces() > decimalPlaces) {
      return false
    }

    // Infinity
    if (!numeric.isFinite()) {
      return false
    }

    // NaN
    if (numeric.isNaN()) {
      return false
    }

    return true
  }

  /**
   * @private
   */
  static _toXDRBigNumeric (value) {
    let numeric = new BigNumber(value)
    return Hyper.fromString(numeric.toString())
  }

  /**
   * @private
   */
  static _fromXDRBigNumeric (value) {
    return new BigNumber(value).toString()
  }

  /**
   * @private
   */
  static _fromXDRPrice (price) {
    let result = new BigNumber(price)
    return (result / Math.pow(10, CURRENCY_PRECISION)).toString()
  }

  /**
   * @private
   */
  static _toXDRPrice (price) {
    let numeric = new BigNumber(price) * Math.pow(10, CURRENCY_PRECISION)
    return UnsignedHyper.fromString(numeric.toString())
  }
}