src/resolve.js

"use strict"
/**
 * Contains functions that probe the blockchain or federation servers to collect
 * datas.
 *
 * @exports resolve
 */
const resolve = exports

const misc = require("@cosmic-plus/jsutils/es5/misc")
const StellarSdk = require("@cosmic-plus/base/es5/stellar-sdk")

const specs = require("./specs")
const status = require("./status")

/**
 * Returns the
 * [Server]{@link https://stellar.github.io/js-stellar-sdk/Server.html} object
 * for **horizon**, or for **network**, or for the current network.
 *
 * @param {string} [network] 'public', 'test' or a network passphrase
 * @param {string} [horizon] A horizon URL
 * @returns {Server} A StellarSdk Server object
 */
resolve.server = function (
  conf,
  network = conf.network,
  horizon = conf.horizon
) {
  if (!horizon) horizon = resolve.horizon(conf, network)
  if (!horizon) throw new Error("No horizon node defined for selected network.")
  if (!conf.current.server[horizon]) {
    conf.current.server[horizon] = new StellarSdk.Server(horizon)
  }
  return conf.current.server[horizon]
}

/**
 * Switch to the current network, or to **network** if provided.
 *
 * @deprecated StellarSdk global `Network` setting is deprecated.
 * @param {string} [network] 'public', 'test' or a network passphrase
 * @returns {Server} A StellarSdk Server object
 */
resolve.useNetwork = function (conf, network = conf.network) {
  // DEPRECATED - to be removed when in sync with stellar-sdk 3.x.
  console.warn(
    "`.selectNetwork()`, `.useNetwork()`, as well as StellarSdk global `Network` setting are deprecated. Please use `cosmicLib.config.network` or pass parameter explicitely."
  )

  const passphrase = resolve.networkPassphrase(conf, network)
  const currentPassphrase = resolve.networkPassphrase()
  if (passphrase !== currentPassphrase) {
    // eslint-disable-next-line no-console
    console.log("Switch to network: " + network)
    StellarSdk.Network.use(new StellarSdk.Network(passphrase))
  }
}

/**
 * Returns the curent Horizon node URL, or the Horizon node URL for **network**
 * if provided.
 *
 * @param {string} [network] A network name or passphrase.
 */
resolve.horizon = function (conf, network = conf.network) {
  if (conf.horizon) {
    return conf.horizon
  } else {
    const passphrase = resolve.networkPassphrase(conf, network)
    if (conf.current && conf.current.horizon[passphrase]) {
      return conf.current.horizon[passphrase]
    }
  }
}

/**
 * Returns the current network passphrase, or the passphrase for **network** is
 * provided.
 */
resolve.networkPassphrase = function (conf = {}, network = conf.network) {
  if (network === undefined) {
    // DEPRECATED: To be removed in sync with stellar-sdk 3.x.
    const currentNetwork = StellarSdk.Network.current()
    if (currentNetwork) return currentNetwork.networkPassphrase()
  } else {
    return conf.current.passphrase[network] || network
  }
}

/**
 * Returns the network name for **network passphrase**, or `undefined`.
 *
 * @param {string} networkPassphrase
 * @return {string}
 */
resolve.networkName = function (conf = {}, networkPassphrase) {
  const index = Object.values(conf.current.passphrase).indexOf(
    networkPassphrase
  )
  if (index === -1) return networkPassphrase
  else return Object.keys(conf.current.passphrase)[index]
}

/**
 * Returns the federation server
 * [Account]{@link https://stellar.github.io/js-stellar-sdk/Account.html}
 * for **address**.
 *
 * @async
 * @param {string} address A Stellar public key or a federated address
 * @return {} Resolve to federation server response
 */
resolve.address = function (conf, address) {
  const cache = conf.cache
  if (cache && cache.destination[address]) return cache.destination[address]

  const promise = addressResolver(conf, address)
  if (cache) cache.destination[address] = promise
  return promise
}

async function addressResolver (conf, address) {
  try {
    const account = await StellarSdk.FederationServer.resolve(address)
    const accountId = account.account_id
    if (!accountId) throw new Error("Unknow address")
    if (!account.memo_type) delete account.memo
    if (address !== accountId) account.address = address
    if (conf.aliases && conf.aliases[accountId]) {
      account.alias = conf.aliases[accountId]
    }
    return account
  } catch (error) {
    console.error(error)
    status.error(conf, "Can't resolve: " + misc.shorter(address))
    status.fail(conf, "Unresolved address", "throw")
  }
}

/**
 * Returns the
 * [AccountResponse]{@link https://stellar.github.io/js-stellar-sdk/AccountResponse.html}
 * object for **address**.
 *
 * @param {string} address A public key or a federated address
 * @return {Object} The AccountResponse
 */
resolve.account = async function (conf, address, quietFlag) {
  const account = await resolve.address(conf, address)
  const accountId = account.account_id
  const cache = conf.cache
  if (cache && cache.account[accountId]) return cache.account[accountId]

  const promise = accountResolver(conf, accountId, quietFlag)
  if (cache) cache.account[accountId] = promise
  return promise
}

async function accountResolver (conf, accountId, quietFlag) {
  const server = resolve.server(conf)
  try {
    const accountResponse = await server.loadAccount(accountId)
    return accountResponse
  } catch (error) {
    if (quietFlag) {
      throw error
    } else {
      if (error.response) {
        status.error(conf, "Empty account: " + misc.shorter(accountId), "throw")
      } else {
        status.error(
          conf,
          "Invalid horizon node: " + resolve.horizon(conf),
          "throw"
        )
      }
    }
  }
}

/**
 * Returns `true` if **address** account is empty, `false` otherwise.
 *
 * @async
 * @param {string} address Public key or federated address
 * @return {boolean}
 */
resolve.isAccountEmpty = function (conf, address) {
  return resolve
    .account(conf, address, true)
    .then(() => false)
    .catch(() => true)
}

/**
 * Returns the account object for transaction source **address`** with sequence
 * set at **sequence** if provided. If **address** is not provided, returns the
 * neutral account object instead (as in SEP-0007 specifications).
 *
 * @param {string} [address]
 * @param {string|numbre} [sequence]
 * @return {AccountResponse}
 */
resolve.txSourceAccount = async function (conf, address, sequence) {
  if (!address) {
    return makeAccountResponse(conf, specs.neutralAccountId, "-1")
  } else {
    const destination = await resolve.address(conf, address)
    if (destination.memo)
      status.error(
        conf,
        "Invalid transaction source address (requires a memo)",
        "throw"
      )
    const account = await resolve.account(conf, destination.account_id)
    if (sequence) {
      const baseAccount = new StellarSdk.Account(account.id, sequence)
      baseAccount.sequence = baseAccount.sequence.sub(1)
      account._baseAccount = baseAccount
    }
    return account
  }
}

/**
 * Creates an AccountResponse object with signers set for an empty account.
 *
 * @param  {string} publicKey
 * @param  {string} sequence [description]
 * @return {AccountResponse}
 */
function makeAccountResponse (conf, publicKey, sequence) {
  const account = new StellarSdk.Account(publicKey, sequence)
  if (conf.cache) conf.cache.account[publicKey] = account
  account.id = publicKey

  account.signers = [
    {
      public_key: publicKey,
      weight: 1,
      key: publicKey,
      type: "ed25519_public_key"
    }
  ]

  return account
}

/**
 * Returns the array of all source accounts ID involved in **transaction**.
 *
 * @param {Transaction} transaction
 * @return {Array}
 */
resolve.txSources = function (conf, transaction) {
  if (!transaction.source) throw new Error("No source for transaction")

  const extra = resolve.extra(conf, transaction)
  if (extra.cache.txSources) return extra.cache.txSources

  const array = extra.cache.txSources = [transaction.source]
  for (let index in transaction.operations) {
    const source = transaction.operations[index].source
    if (source && !array.find((a) => a === source)) array.push(source)
  }

  return array
}

/**
 * Returns an object such as:
 *
 * ```js
 * {
 *    $accountId: $accountSigners
 *    ...
 * }
 * ```
 *
 * @param {Transaction} transaction
 * @return {Object}
 */
resolve.txSigners = async function (conf, transaction) {
  const extra = resolve.extra(conf, transaction)
  if (extra.cache.txSigners) return extra.cache.txSigners

  const txSources = resolve.txSources(extra, transaction)
  const signers = extra.cache.txSigners = {}

  for (let index in txSources) {
    const source = txSources[index]
    const account = await resolveTxSource(extra, source)
    if (!signers[account.id]) {
      signers[account.id] = account.signers.filter((signer) => {
        return signer.type !== "preauthTx"
      })
    }
  }

  return signers
}

async function resolveTxSource (conf, address) {
  try {
    return await resolve.account(conf, address, "quiet")
  } catch (error) {
    return makeAccountResponse(conf, address, "0")
  }
}

/**
 * Returns an Array containing the keys for all legit signers of **transaction**.
 *
 * @param {Transaction} transaction
 * @return {Array}
 */
resolve.txSignersList = async function (conf, transaction) {
  const extra = resolve.extra(conf, transaction)
  if (!extra.cache.txSignersList) {
    const txSigners = await resolve.txSigners(extra, transaction)
    extra.cache.txSignersList = signersTableToSignersList(txSigners)
  }
  return extra.cache.txSignersList
}

function signersTableToSignersList (signersTable) {
  const array = []
  for (let accountId in signersTable) {
    signersTable[accountId].forEach((signer) => {
      if (!array.find((key) => key === signer.key)) array.push(signer.key)
    })
  }
  return array
}

/**
 * Add an extra field to **object** that embed cache and local configuration.
 *
 * @private
 */
resolve.extra = function (conf, object) {
  if (!object._cosmicplus) {
    misc.setHiddenProperty(object, "_cosmicplus", {})
    if (conf.cache) object._cosmicplus.cache = conf.cache
    else object._cosmicplus.cache = { destination: {}, account: {} }
    object._cosmicplus.network = conf.network
    object._cosmicplus.current = conf.current
  }
  return object._cosmicplus
}