Source: blockchains/ethereum.js

  1. 'use strict';
  2. /**
  3. * @module lib/blockchains/ethereum
  4. * @summary Whiteflag API Ethereum blockchain implementation
  5. * @description Module to use Ethereum as underlying blockchain for Whiteflag
  6. * @tutorial modules
  7. * @tutorial ethereum
  8. */
  9. module.exports = {
  10. // Ethereum blockchain functions
  11. init: initEthereum,
  12. sendMessage,
  13. getMessage,
  14. requestSignature,
  15. requestKeys,
  16. getBinaryAddress,
  17. transferFunds,
  18. createAccount,
  19. updateAccount,
  20. deleteAccount
  21. };
  22. // Node.js core and external modules //
  23. const ethereumUtil = require('ethereumjs-util');
  24. const keccak = require('keccak');
  25. const KeyEncoder = require('key-encoder').default;
  26. // Whiteflag common functions and classes //
  27. const log = require('../common/logger');
  28. const { hash } = require('../common/crypto');
  29. const { ProcessingError } = require('../common/errors');
  30. const { getEmptyState, createSignature } = require('./common');
  31. // Whiteflag modules //
  32. const wfState = require('../protocol/state');
  33. // Ethereum sub-modules //
  34. const ethRpc = require('./ethereum/rpc');
  35. const ethAccounts = require('./ethereum/accounts');
  36. const ethListener = require('./ethereum/listener');
  37. const ethTransactions = require('./ethereum/transactions');
  38. const { formatHexEthereum, formatHexApi, formatAddressApi, formatPubkeyApi } = require('./ethereum/common');
  39. // Module constants //
  40. const KEYIDLENGTH = 12;
  41. const SIGNALGORITHM = 'ES256';
  42. const SIGNKEYTYPE = 'secp256k1';
  43. const BINENCODING = 'hex';
  44. // Module variables //
  45. let _blockchainName = 'ethereum';
  46. let _ethState = {};
  47. let _web3;
  48. let _transactionValue = '0';
  49. /**
  50. * Initialises the Ethereum blockchain
  51. * @function initEthereum
  52. * @alias module:lib/blockchains/ethereum.init
  53. * @param {Object} ethConfig the blockchain configuration
  54. * @param {blockchainInitCb} callback function to be called after intitialising Ethereum
  55. */
  56. function initEthereum(ethConfig, callback) {
  57. _blockchainName = ethConfig.name;
  58. log.trace(_blockchainName, 'Initialising the Ethereum blockchain...');
  59. // Get Ethereum blockchain state
  60. wfState.getBlockchainData(_blockchainName, function blockchainsGetStateDb(err, ethState) {
  61. if (err) return callback(err, _blockchainName);
  62. // Check and preserve Ethereum state
  63. if (!ethState) {
  64. log.info(_blockchainName, 'Creating new Ethereum entry in internal state');
  65. ethState = getEmptyState();
  66. wfState.updateBlockchainData(_blockchainName, ethState);
  67. }
  68. _ethState = ethState;
  69. // Connect to node, determine blocks, and start listener
  70. ethRpc.init(ethConfig, _ethState)
  71. .then(web3 => (_web3 = web3))
  72. .then(() => ethTransactions.init(ethConfig, _ethState, _web3))
  73. .then(() => ethListener.init(ethConfig, _ethState))
  74. .then(() => ethAccounts.init(ethConfig, _ethState, _web3))
  75. .then(() => wfState.updateBlockchainData(_blockchainName, _ethState))
  76. .then(() => callback(null, _blockchainName))
  77. .catch(initErr => callback(initErr, _blockchainName));
  78. });
  79. }
  80. /**
  81. * Sends an encoded message on the Ethereum blockchain
  82. * @function sendMessage
  83. * @alias module:lib/blockchains/ethereum.sendMessage
  84. * @param {wfMessage} wfMessage the Whiteflag message to be sent on Ethereum
  85. * @param {blockchainSendMessageCb} callback function to be called after sending Whiteflag message
  86. */
  87. function sendMessage(wfMessage, callback) {
  88. ethAccounts.get(wfMessage.MetaHeader.originatorAddress)
  89. .then(account => {
  90. const toAddress = account.address;
  91. const encodedMessage = wfMessage.MetaHeader.encodedMessage;
  92. return ethTransactions.send(account, toAddress, _transactionValue, encodedMessage);
  93. })
  94. .then((transactionHash, blockNumber) => {
  95. return callback(null, transactionHash, blockNumber);
  96. })
  97. .catch(err => {
  98. log.error(_blockchainName, `Error sending Whiteflag message: ${err.message}`);
  99. callback(err);
  100. });
  101. }
  102. /**
  103. * Performs a simple query to find a message on Ethereum by transaction hash
  104. * @function getMessage
  105. * @alias module:lib/blockchains/ethereum.getMessage
  106. * @param {Object} wfQuery the property of the transaction to look up
  107. * @param {blockchainLookupMessageCb} callback function to be called after Whiteflag message lookup
  108. */
  109. function getMessage(wfQuery, callback) {
  110. const transactionHash = wfQuery['MetaHeader.transactionHash'];
  111. ethTransactions.get(transactionHash)
  112. .then(transaction => {
  113. return ethTransactions.extractMessage(transaction);
  114. })
  115. .then(wfMessage => callback(null, wfMessage))
  116. .catch(err => {
  117. if (err instanceof ProcessingError) {
  118. log.debug(_blockchainName, `No Whiteflag message with transaction hash ${transactionHash} found: ${err.message}`);
  119. } else {
  120. log.error(_blockchainName, `Error retrieving Whiteflag message with transaction hash ${transactionHash}: ${err.message}`);
  121. }
  122. return callback(err, null);
  123. });
  124. }
  125. /**
  126. * Requests a Whiteflag signature for a specific Ethereum address
  127. * @function requestSignature
  128. * @alias module:lib/blockchains/ethereum.requestSignature
  129. * @param {wfSignaturePayload} payload the JWS payload for the Whiteflag signature
  130. * @param {blockchainRequestSignatureCb} callback function to be called upon completion
  131. */
  132. function requestSignature(payload, callback) {
  133. log.trace(_blockchainName, `Generating signature: ${JSON.stringify(payload)}`);
  134. // Get Ethereum account, address and private key
  135. ethAccounts.get(payload.addr)
  136. .then(account => {
  137. payload.addr = formatAddressApi(account.address);
  138. const privateKeyId = hash(_blockchainName + account.address, KEYIDLENGTH);
  139. wfState.getKey('blockchainKeys', privateKeyId, function ethGetKeyCb(keyErr, privateKey) {
  140. if (keyErr) return callback(keyErr);
  141. // Create JSON serialization of JWS token from array
  142. let wfSignature;
  143. try {
  144. wfSignature = createSignature(payload, formatHexApi(privateKey), SIGNKEYTYPE, SIGNALGORITHM);
  145. } catch(err) {
  146. log.error(_blockchainName, `Could not not sign payload: ${err.message}`);
  147. return callback(err);
  148. }
  149. // Callback with any error and signature
  150. return callback(null, wfSignature);
  151. });
  152. })
  153. .catch(err => callback(err));
  154. }
  155. /**
  156. * Requests the Ethereum address and correctly encoded public key of an originator
  157. * @function requestKeys
  158. * @alias module:lib/blockchains/ethereum.requestKeys
  159. * @param {string} publicKey the raw hex public key of the originator
  160. * @param {blockchainRequestKeysCb} callback function to be called upon completion
  161. */
  162. function requestKeys(publicKey, callback) {
  163. log.trace(_blockchainName, `Getting address and encoded keys for public key: ${publicKey}`);
  164. // Create data structure for requested keys
  165. let originatorKeys = {
  166. address: null,
  167. publicKey: {
  168. hex: null,
  169. pem: null
  170. },
  171. check: {
  172. keccak: null
  173. }
  174. };
  175. try {
  176. // Ethereum public key in HEX and PEM encoding
  177. const keyEncoder = new KeyEncoder(SIGNKEYTYPE);
  178. const publicKeyBuffer = Buffer.from(formatPubkeyApi(publicKey), BINENCODING);
  179. originatorKeys.publicKey.hex = publicKeyBuffer.toString(BINENCODING);
  180. originatorKeys.publicKey.pem = keyEncoder.encodePublic(originatorKeys.publicKey.hex, 'raw', 'pem');
  181. // Keccak double check
  182. let keccakPubkeyBuffer;
  183. if (originatorKeys.publicKey.hex.length === 130) keccakPubkeyBuffer = Buffer.from(originatorKeys.publicKey.hex.substr(2), BINENCODING);
  184. else keccakPubkeyBuffer = Buffer.from(originatorKeys.publicKey.hex, BINENCODING);
  185. originatorKeys.check.keccak = keccak('keccak256').update(keccakPubkeyBuffer).digest('hex');
  186. // Ethereum Address
  187. originatorKeys.address = formatAddressApi(ethereumUtil.pubToAddress(publicKeyBuffer, true).toString(BINENCODING));
  188. } catch(err) {
  189. log.error(_blockchainName, `Could not get key and address: ${err.message}`);
  190. return callback(err);
  191. }
  192. return callback(null, originatorKeys);
  193. }
  194. /**
  195. * Returns an Ethereum address in binary encoded form
  196. * @param {string} address the blockchain address
  197. * @param {blockchainBinaryAddressCb} callback function to be called upon completion
  198. */
  199. function getBinaryAddress(address, callback) {
  200. log.trace(_blockchainName, `Binary encoding address: ${address}`);
  201. if (_web3.utils.isAddress(formatHexEthereum(address))) {
  202. return callback(null, Buffer.from(formatAddressApi(address), BINENCODING));
  203. }
  204. return callback(new ProcessingError(`Invalid Ethereum address: ${address}`, null, 'WF_API_PROCESSING_ERROR'));
  205. }
  206. /**
  207. * Transfers ether from one Ethereum address to an other address
  208. * @function transferFunds
  209. * @alias module:lib/blockchains/ethereum.transferFunds
  210. * @param {Object} transfer the object with the transaction details to transfer funds
  211. * @param {blockchainTransferValueCb} callback function to be called upon completion
  212. */
  213. function transferFunds(transfer, callback) {
  214. log.trace(_blockchainName, `Transferring funds: ${JSON.stringify(transfer)}`);
  215. ethAccounts.get(transfer.fromAddress)
  216. .then(account => {
  217. return ethTransactions.send(account, transfer.toAddress, transfer.value, '');
  218. })
  219. .then((transactionHash, blockNumber) => callback(null, transactionHash, blockNumber))
  220. .catch(err => {
  221. log.error(_blockchainName, `Error transferring funds: ${err.message}`);
  222. return callback(err);
  223. });
  224. }
  225. /**
  226. * Creates a new Ethereum blockchain account
  227. * @function createAccount
  228. * @alias module:lib/blockchains/ethereum.createAccount
  229. * @param {string} [privateKey] hexadecimal encoded private key
  230. * @param {blockchainCreateAccountCb} callback function to be called upon completion
  231. */
  232. function createAccount(privateKey, callback) {
  233. ethAccounts.create(privateKey)
  234. .then(account => {
  235. log.info(_blockchainName, `Ethereum account created: ${account.address}`);
  236. return callback(null, account);
  237. })
  238. .catch(err => callback(err));
  239. }
  240. /**
  241. * Updates Ethereum blockchain account attributes
  242. * @function updateAccount
  243. * @alias module:lib/blockchains/ethereum.updateAccount
  244. * @param {Object} account the account information including address to be updated
  245. * @param {blockchainUpdateAccountCb} callback function to be called upon completion
  246. */
  247. function updateAccount(account, callback) {
  248. ethAccounts.update(account)
  249. .then(updatedAccount => {
  250. log.debug(_blockchainName, `Ethereum account updated: ${account.address}`);
  251. return callback(null, updatedAccount);
  252. })
  253. .catch(err => callback(err));
  254. }
  255. /**
  256. * Deletes Ethereum blockchain account
  257. * @function deleteAccount
  258. * @alias module:lib/blockchains/ethereum.deleteAccount
  259. * @param {string} address the address of the account to be deleted
  260. * @param {blockchainDeleteAccountCb} callback function to be called upon completion
  261. */
  262. function deleteAccount(address, callback) {
  263. ethAccounts.delete(address)
  264. .then(account => {
  265. log.info(_blockchainName, `Ethereum account deleted: ${account.address}`);
  266. return callback(null, account);
  267. })
  268. .catch(err => callback(err));
  269. }