priv.js

const { Transaction } = require("ethereumjs-tx");
const PrivateTransaction = require("./privateTransaction");
const { privateToAddress } = require("./util/custom-ethjs-util");
const { PrivateSubscription } = require("./privateSubscription");
const { intToHex } = require("./util");
const common = require("./common");

/**
 * @module priv
 */
function Priv(web3) {
  const GAS_PRICE = 0;
  const GAS_LIMIT = 3000000;
  let chainId = null;
  web3.extend({
    property: "priv",
    methods: [
      /**
       * Invokes a private contract function locally and does not change the privacy group state.
       * @function call
       * @param {String} privacyGroupId privacy group ID
       * @param {Object} call transaction call object
       * @param {String} blockNumber integer representing a block number or one of the string tags latest, earliest, or pending
       * @return {Data} result return value of the executed contract
       */
      {
        name: "call",
        call: "priv_call",
        params: 3,
        inputFormatter: [
          null, // privacyGroupId
          null, // tx
          web3.extend.formatters.inputDefaultBlockNumberFormatter,
        ],
      },
      /**
       * @function debugGetStateRoot
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {String|Number} blockNumber
       * @return {String} 32-byte state root
       */
      {
        name: "debugGetStateRoot",
        call: "priv_debugGetStateRoot",
        params: 2,
      },
      /**
       * @function distributeRawTransaction
       * @param {String} transaction signed RLP-encoded private transaction
       * @return {String} 32-byte enclave key
       */
      {
        name: "distributeRawTransaction",
        call: "priv_distributeRawTransaction",
        params: 1,
      },
      /**
       * @function getEeaTransactionCount
       * @param {String} address account address
       * @param {String} sender base64-encoded Enclave address of the sender
       * @param {String[]} recipients base64-encoded Enclave addresses of recipients
       * @return {String} integer representing the number of private transactions sent from the address to the specified group of sender and recipients
       */
      {
        name: "getEeaTransactionCount",
        call: "priv_getEeaTransactionCount",
        params: 3,
      },
      /**
       * @function getFilterChanges
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {String} filterId filter ID
       * @return {Object[]} list of log objects, or an empty list if nothing has changed since the last poll
       */
      {
        name: "getFilterChanges",
        call: "priv_getFilterChanges",
        params: 2,
        outputFormatter: web3.extend.formatters.outputLogFormatter,
      },
      /**
       * @function getFilterLogs
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {String} filterId filter ID
       * @return {Object[]} list of log objects
       */
      {
        name: "getFilterLogs",
        call: "priv_getFilterLogs",
        params: 2,
        outputFormatter: web3.extend.formatters.outputLogFormatter,
      },
      /**
       * @function getLogs
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {Object} filterOptions filter options object
       * @return {Object[]} list of log objects
       */
      {
        name: "getLogs",
        call: "priv_getLogs",
        params: 2,
        outputFormatter: web3.extend.formatters.outputLogFormatter,
      },
      /**
       * @function getPrivacyPrecompileAddress
       * @return {String} address of the privacy precompile
       */
      {
        name: "getPrivacyPrecompileAddress",
        call: "priv_getPrivacyPrecompileAddress",
        params: 0,
      },
      /**
       * @function getPrivateTransaction
       * @param {String} transaction transaction hash returned by eea_sendRawTransaction or eea_sendTransaction.
       * @return {Object} private transaction object, or null if not a participant in the private transaction
       */
      {
        name: "getPrivateTransaction",
        call: "priv_getPrivateTransaction",
        params: 1,
      },
      /**
       * @function createPrivacyGroup
       * @param {Object} options request options object with the following fields:
       * @param {String[]} options.addresses list of nodes specified by Enclave public keys
       * @param {String} options.name (optional) privacy group name
       * @param {String} options.description (optional) privacy group description
       * @return {String} privacy group ID
       */
      {
        name: "createPrivacyGroup",
        call: "priv_createPrivacyGroup",
        params: 1,
      },
      /**
       * @function deletePrivacyGroup
       * @param {String} privacyGroupId privacy group ID
       * @return {String} deleted privacy group ID
       */
      {
        name: "deletePrivacyGroup",
        call: "priv_deletePrivacyGroup",
        params: 1,
      },
      /**
       * @function findPrivacyGroup
       * @param {String[]} members members specified by Enclave public keys
       * @return {Object[]} privacy group objects
       */
      {
        name: "findPrivacyGroup",
        call: "priv_findPrivacyGroup",
        params: 1,
      },
      /**
       * @function getCode
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {String} address 20-byte contract address
       * @param {String|Number} blockNumber
       * @return {String} code stored at the specified address
       */
      {
        name: "getCode",
        call: "priv_getCode",
        params: 3,
      },
      /**
       * @function getTransactionCount
       * @param {String} address account address
       * @param {String} privacyGroupId privacy group ID
       * @return {String} integer representing the number of private transactions sent from the address to the specified privacy group
       */
      {
        name: "getTransactionCount",
        call: "priv_getTransactionCount",
        params: 2,
        outputFormatter: (output) => {
          return parseInt(output, 16);
        },
      },
      /**
       * @function getTransactionReceipt
       * @param {String} transaction 32-byte hash of a transaction
       * @return {Object} private Transaction receipt object, or null if no receipt found
       */
      {
        name: "getTransactionReceipt",
        call: "priv_getTransactionReceipt",
        params: 1,
      },
      /**
       * @function newFilter
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {Object} filterOptions filter options object
       * @return {String} filter ID
       */
      {
        name: "newFilter",
        call: "priv_newFilter",
        params: 2,
      },
      /**
       * @function uninstallFilter
       * @param {String} privacyGroupId 32-byte privacy Group ID
       * @param {Object} filterOptions filter options object
       * @return {Boolean} indicates if the filter is successfully uninstalled
       */
      {
        name: "uninstallFilter",
        call: "priv_uninstallFilter",
        params: 2,
      },
      /**
       * @function sendRawTransaction
       * @param {String} transaction signed RLP-encoded private transaction
       * @return {String} 32-byte transaction hash of the Privacy Marker Transaction
       */
      {
        name: "sendRawTransaction",
        call: "eea_sendRawTransaction",
        params: 1,
      },
      /**
       * @function subscribe
       * @param {String} privacyGroupId
       * @param {String} type
       * @param {Object} filter
       * @return {Sting} Subscription ID
       */
      {
        name: "subscribe",
        call: "priv_subscribe",
        params: 3, // privacyGroupId, type, filter
      },
      /**
       * @function unsubscribe
       * @param {String} privacyGroupId
       * @param {String} subscriptionId
       * @return {Boolean} true if subscription successfully unsubscribed; otherwise, returns an error.
       */
      {
        name: "unsubscribe",
        call: "priv_unsubscribe",
        params: 2, // privacyGroupId, subscriptionId
      },
    ],
  });

  /**
   * Get the private transaction Receipt with waiting until the receipt is ready.
   * @function waitForTransactionReceipt
   * @param {string} txHash Transaction Hash of the marker transaction
   * @param {int} [retries=300] Number of retries to be made to get the private marker transaction receipt
   * @param {int} [delay=1000] The delay between the retries in milliseconds
   * @returns {Promise<T>}
   */
  const waitForTransactionReceipt = (txHash, retries = 300, delay = 1000) => {
    const operation = () => {
      return web3.eth.getTransactionReceipt(txHash);
    };

    return common
      .waitForTransactionWithRetries(operation, txHash, retries, delay)
      .then((receipt) => {
        if (web3.isQuorum) {
          return receipt;
        }
        return web3.priv.getTransactionReceipt(txHash);
      });
  };

  const getTransactionPayload = (options) => {
    if (options.isPrivate) {
      return web3.ptm.storeRaw(options);
    }
    return options.data;
  };

  const serializeSignedTransaction = (options, data) => {
    const rawTransaction = {
      nonce: intToHex(options.nonce),
      from: options.from,
      to: options.to,
      value: intToHex(options.value),
      gasLimit: intToHex(options.gasLimit),
      gasPrice: intToHex(options.gasPrice),
      data: `0x${data}`,
    };

    const tx = new Transaction(rawTransaction, {
      chain: "mainnet",
      hardfork: "homestead",
    });
    tx.sign(Buffer.from(options.from.privateKey.substring(2), "hex"));

    const serializedTx = tx.serialize();
    return `0x${serializedTx.toString("hex")}`;
  };

  const sendRawRequest = async (payload, privacyParams) => {
    const txHash = await web3.eth.sendRawPrivateTransaction(
      payload,
      privacyParams
    );
    return waitForTransactionReceipt(txHash);
  };

  const genericSendRawTransaction = async (options, method) => {
    if (web3.isQuorum) {
      const transactionPayload = await getTransactionPayload(options);
      const serializedTx = serializeSignedTransaction(
        options,
        transactionPayload
      );

      const privateTx = web3.utils.setPrivate(serializedTx);
      return sendRawRequest(`0x${privateTx.toString("hex")}`, {
        privateFor: options.privateFor,
        privacyFlag: options.privacyFlag,
        mandatoryFor: options.mandatoryFor,
      });
    }
    if (options.privacyGroupId && options.privateFor) {
      throw Error("privacyGroupId and privateFor are mutually exclusive");
    }
    chainId = chainId || (await web3.eth.getChainId());
    const tx = new PrivateTransaction();
    const privateKeyBuffer = Buffer.from(options.privateKey, "hex");
    const from = `0x${privateToAddress(privateKeyBuffer).toString("hex")}`;
    const privacyGroupId =
      options.privacyGroupId || web3.utils.generatePrivacyGroup(options);
    const transactionCount =
      options.nonce ||
      (await web3.priv.getTransactionCount(from, privacyGroupId));
    tx.nonce = options.nonce || transactionCount;
    tx.gasPrice = options.gasPrice || GAS_PRICE;
    tx.gasLimit = options.gasLimit || GAS_LIMIT;
    tx.to = options.to;
    tx.value = 0;
    tx.data = options.data;
    tx._chainId = chainId;
    tx.privateFrom = options.privateFrom;

    if (options.privateFor) {
      tx.privateFor = options.privateFor;
    }
    if (options.privacyGroupId) {
      tx.privacyGroupId = options.privacyGroupId;
    }
    tx.restriction = options.restriction || "restricted";

    tx.sign(privateKeyBuffer);

    const signedRlpEncoded = tx.serialize().toString("hex");

    let result;
    if (method === "eea_sendRawTransaction") {
      result = web3.priv.sendRawTransaction(signedRlpEncoded);
    } else if (method === "priv_distributeRawTransaction") {
      result = web3.priv.distributeRawTransaction(signedRlpEncoded);
    }
    if (result != null) {
      return result;
    }

    throw new Error(`Unknown method ${method}`);
  };

  /**
   * Generate and distribute the Raw transaction to the Besu node using the `priv_distributeRawTransaction` JSON-RPC call
   * @function generateAndDistributeRawTransaction
   * @param {object} options Map to send a raw transaction to besu
   * @param {string} options.privateKey Private Key used to sign transaction with
   * @param {string} options.privateFrom Enclave public key
   * @param {string} options.privateFor Enclave keys to send the transaction to
   * @param {string} options.privacyGroupId Enclave id representing the receivers of the transaction
   * @param {string} [options.nonce] If not provided, will be calculated using `priv_getTransactionCount`
   * @param {string} options.to The address to send the transaction
   * @param {string} options.data Data to be sent in the transaction
   *
   * @returns {Promise<T>}
   */
  const generateAndDistributeRawTransaction = (options) => {
    return genericSendRawTransaction(options, "priv_distributeRawTransaction");
  };

  /**
   * Generate and send the Raw transaction to the Besu node using the `eea_sendRawTransaction` JSON-RPC call
   * @function generateAndSendRawTransaction
   * @param {object} options Map to send a raw transaction to besu
   * @param {string} options.privateKey Private Key used to sign transaction with
   * @param {string} options.privateFrom Enclave public key
   * @param {string} options.privateFor Enclave keys to send the transaction to
   * @param {string} options.privacyGroupId Enclave id representing the receivers of the transaction
   * @param {string} [options.nonce] If not provided, will be calculated using `priv_getTransactionCount`
   * @param {string} options.to The address to send the transaction
   * @param {string} options.data Data to be sent in the transaction
   * @param {string} options.gasLimit The gas limit to use for the privacy marker transaction (if applicable)
   * @param {string} options.gasPrice The gas price to use for the privacy marker transaction (if applicable)
   *
   * @returns {Promise<T>}
   */
  const generateAndSendRawTransaction = (options) => {
    return genericSendRawTransaction(options, "eea_sendRawTransaction");
  };

  /**
   * Subscribe to new logs matching a filter
   *
   * If the provider supports subscriptions, it uses `priv_subscribe`, otherwise
   * it uses polling and `priv_getFilterChanges` to get new logs. Returns an
   * error to the callback if there is a problem subscribing or creating the filter.
   * @function subscribeWithPooling
   * @param {string} privacyGroupId
   * @param {*} filter
   * @param {function} callback returns the filter/subscription ID, or an error
   * @return {PrivateSubscription} a subscription object that manages the
   * lifecycle of the filter or subscription
   */
  const subscribeWithPooling = async (privacyGroupId, filter, callback) => {
    const sub = new PrivateSubscription(web3, privacyGroupId, filter);

    let filterId;
    try {
      filterId = await sub.subscribe();
      callback(undefined, filterId);
    } catch (error) {
      callback(error);
    }

    return sub;
  };

  Object.assign(web3.priv, {
    subscriptionPollingInterval: 1000,
    waitForTransactionReceipt,
    generateAndDistributeRawTransaction,
    generateAndSendRawTransaction,
    subscribeWithPooling,
  });

  return web3;
}

module.exports = Priv;