Source: client.js

const axios = require("axios");
const ethers = require("ethers");
const _ = require("lodash/core");

const { reputationABI } = require("./abi");

// TODO: populate once I deploy the registry
const DEFAULT_REPUTATION_ADDRESSES = {
  KOVAN: "0xc5069F6E373Bf38b6bd55BDc3F6096B656aaC6c0"
};

// NOTE: We may want this to be an arg in the future
const DEFAULT_RELAYER_BATCH_SIZE = 10;

const getFeeRoute = locator => {
  return `${locator}/fee`;
};

const getSubmitTxRoute = locator => {
  return `${locator}/submit_tx`;
};

/**
 * Class representing a single surrogeth client. Maintains state about which relayers it's already tried to
 * communicate with.
 */
class SurrogethClient {
  constructor(
    provider,
    network = "KOVAN",
    reputationAddress = DEFAULT_REPUTATION_ADDRESSES[network]
  ) {
    this.network = network;
    this.provider = provider;
    this.reputationAddress = reputationAddress;
  }

  /**
   * Get the highest reputation relayers from the reputation contract.
   *
   * @param {number} numRelayers - The number of relayers to return.
   * @param {Set<string>} addressesToIgnore - Any relayer addresses to skip over.
   * @param {Set<string>} allowedLocatorTypes - The locator types to include.
   *
   * @returns {Array<{locator: string, locatorType: string, burn: number, address: string}>} An array of
   * information objects corresponding to relayers
   */
  async getRelayers(
    numRelayers = 1,
    addressesToIgnore = new Set([]),
    allowedLocatorTypes = new Set(["ip", "tor"])
  ) {
    const contract = new ethers.Contract(
      this.reputationAddress,
      reputationABI,
      this.provider
    );

    const candidates = [];

    const nextRelayerId = (await contract.nextRelayer()).toNumber();

    // TODO: batch these calls with multicall
    for (var relayerId = 1; relayerId < nextRelayerId; relayerId++) {
      const relayerAddress = await contract.relayerList(relayerId);

      if (!addressesToIgnore.has(relayerAddress)) {
        candidates.push(relayerAddress);
      }
    }

    // No registered relayers in the reputation contract!
    if (candidates.length === 0) {
      return [];
    }

    // TODO: batch these calls with multicall
    const candidatesWithBurn = await Promise.all(
      _.map(candidates, async candidate => {
        const burn = await contract.relayerToBurn(candidate);
        return { burn, address: candidate };
      })
    );

    const sortedCandidates = _.sortBy(
      candidatesWithBurn,
      ({ burn }) => -1 * burn
    );

    // Iterate backwards through candidates until we hit 'numRelayers' of an allowed locator type
    let toReturn = [];
    for (const candidate of sortedCandidates) {
      const { address, burn } = candidate;
      const { locator, locatorType } = await contract.relayerToLocator(address);

      if (allowedLocatorTypes.has(locatorType)) {
        toReturn.push({ locator, locatorType, address, burn });
      }

      if (toReturn.length >= numRelayers) {
        break;
      }
    }

    return toReturn;
  }

  /**
   * Returns the fee for the specified relayer.
   *
   * @param {{locator: string, locatorType: string}} relayer - The relayer whose fee to return, as specified
   * by a locator (i.e. IP address) and locatorType string (i.e. 'ip')
   *
   * @returns {number|null} The fee in Wei advertised by the specified relayer.
   */
  async getRelayerFee(relayer) {
    const { locator, locatorType } = relayer;

    if (locatorType !== "ip") {
      console.log(
        `Can't communicate with relayer at ${locator} of locatorType ${locatorType} because only IP supported right now.`
      );
      return null;
    }

    const resp = await axios.get(getFeeRoute(locator));

    if (resp.statusCode !== 200) {
      console.log(
        `${resp.status} error retrieving fee from relayer ${locator}`
      );
      return null;
    }

    return resp.data["fee"];
  }

  /**
   * Submit the specified transaction to the specified relayer.
   *
   * @param {{locator: string, locatorType: string}} relayer - The relayer whose fee to return, as specified
   * by a locator (i.e. IP address) and locatorType string (i.e. 'ip')
   * @param {{to: string, data: string, value: number}} tx - The transaction info to submit. 'to' is a hex string
   * representing the address to send to and 'data' is a hex string or an empty string representing the data
   * payload of the transaction
   *
   * @returns {string|null} The transaction hash of the submitted transaction
   */
  async submitTx(tx, relayer) {
    const { locator, locatorType } = relayer;
    const { to, data, value } = tx;

    if (locatorType !== "ip") {
      console.log(
        `Can't communicate with relayer at ${locator} of locatorType ${locatorType} because only IP supported right now.`
      );
      return null;
    }

    const resp = await axios.post(getSubmitTxRoute(locator), {
      to,
      data,
      value,
      network: this.network
    });

    if (resp.statusCode != 200) {
      console.log(`${resp.status} error submitting tx to relayer ${locator}`);
    }

    return resp.data.hash;
  }
}

module.exports = {
  SurrogethClient
};