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;