sendTransactionConfirmation.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. /**
  2. Template Controllers
  3. @module Templates
  4. */
  5. var setWindowSize = function(template) {
  6. Tracker.afterFlush(function() {
  7. ipc.send(
  8. 'backendAction_setWindowSize',
  9. 580,
  10. template.$('.popup-windows .inner-container').height() + 240
  11. );
  12. });
  13. };
  14. var defaultEstimateGas = 3000000;
  15. /**
  16. The sendTransaction confirmation popup window template
  17. @class [template] popupWindows_sendTransactionConfirmation
  18. @constructor
  19. */
  20. /**
  21. Takes a 4-byte function signature and does a best-effort conversion to a
  22. human readable text signature.
  23. @method (lookupFunctionSignature)
  24. */
  25. var lookupFunctionSignature = function(data, remoteLookup) {
  26. return new Q(function(resolve, reject) {
  27. if (data && data.length > 8) {
  28. var bytesSignature =
  29. data.substr(0, 2) === '0x'
  30. ? data.substr(0, 10)
  31. : '0x' + data.substr(0, 8);
  32. if (remoteLookup) {
  33. https
  34. .get(
  35. 'https://www.4byte.directory/api/v1/signatures/?hex_signature=' +
  36. bytesSignature,
  37. function(response) {
  38. var body = '';
  39. response.on('data', function(chunk) {
  40. body += chunk;
  41. });
  42. response.on('end', function() {
  43. var responseData = JSON.parse(body);
  44. if (responseData.results.length) {
  45. resolve(responseData.results[0].text_signature);
  46. } else {
  47. resolve(bytesSignature);
  48. }
  49. });
  50. }
  51. )
  52. .on('error', function(error) {
  53. console.warn('Error querying Function Signature Registry.', err);
  54. reject(bytesSignature);
  55. });
  56. } else if (_.first(window.SIGNATURES[bytesSignature])) {
  57. resolve(_.first(window.SIGNATURES[bytesSignature]));
  58. } else {
  59. reject(bytesSignature);
  60. }
  61. } else {
  62. reject(undefined);
  63. }
  64. });
  65. };
  66. var localSignatureLookup = function(data) {
  67. return lookupFunctionSignature(data, false);
  68. };
  69. var remoteSignatureLookup = function(data) {
  70. return lookupFunctionSignature(data, true);
  71. };
  72. var signatureLookupCallback = function(textSignature) {
  73. // Clean version of function signature. Striping params
  74. TemplateVar.set(
  75. template,
  76. 'executionFunction',
  77. textSignature.replace(/\(.+$/g, '')
  78. );
  79. TemplateVar.set(template, 'hasSignature', true);
  80. var params = textSignature.match(/\((.+)\)/i);
  81. if (params) {
  82. console.log('params sent', params);
  83. TemplateVar.set(template, 'executionFunctionParamTypes', params);
  84. ipc.send('backendAction_decodeFunctionSignature', textSignature, data.data);
  85. }
  86. };
  87. Template['popupWindows_sendTransactionConfirmation'].onCreated(function() {
  88. var template = this;
  89. setNetwork(template);
  90. ipc.on('uiAction_decodedFunctionSignatures', function(event, params) {
  91. console.log('params returned', params);
  92. TemplateVar.set(template, 'params', params);
  93. });
  94. // check reactively if provided gas is enough
  95. this.autorun(function() {
  96. if (
  97. TemplateVar.get('estimatedGas') > Number(TemplateVar.get('providedGas'))
  98. ) {
  99. TemplateVar.set('gasError', 'notEnoughGas');
  100. } else if (TemplateVar.get('estimatedGas') > 8000000) {
  101. TemplateVar.set('gasError', 'overBlockGasLimit');
  102. } else if (TemplateVar.get('estimatedGas') == defaultEstimateGas) {
  103. TemplateVar.set('gasError', 'defaultGas');
  104. } else {
  105. TemplateVar.set('gasError', null);
  106. }
  107. });
  108. // check inital data and gas estimates
  109. this.autorun(function() {
  110. TemplateVar.set(template, 'displayDecodedParams', true);
  111. var data = Session.get('data');
  112. if (data) {
  113. // set window size
  114. setWindowSize(template);
  115. // set provided gas to templateVar
  116. const gas = web3.utils.toBN(data.gas || 0);
  117. TemplateVar.set('providedGas', gas.toNumber());
  118. TemplateVar.set('initialProvidedGas', gas.toNumber());
  119. // add gasPrice if not set
  120. if (!data.gasPrice) {
  121. web3.eth.getGasPrice(function(e, res) {
  122. if (!e) {
  123. data.gasPrice = '0x' + res.toString(16);
  124. Session.set('data', data);
  125. }
  126. });
  127. }
  128. // check if to is a contract
  129. if (data.to) {
  130. web3.eth.getCode(data.to, function(e, res) {
  131. if (!e && res && res.length > 2) {
  132. TemplateVar.set(template, 'toIsContract', true);
  133. setWindowSize(template);
  134. }
  135. });
  136. if (data.data) {
  137. localSignatureLookup(data.data)
  138. .then(function(textSignature) {
  139. // Clean version of function signature. Striping params
  140. TemplateVar.set(
  141. template,
  142. 'executionFunction',
  143. textSignature.replace(/\(.+$/g, '')
  144. );
  145. TemplateVar.set(template, 'hasSignature', true);
  146. var params = textSignature.match(/\((.+)\)/i);
  147. if (params) {
  148. TemplateVar.set(
  149. template,
  150. 'executionFunctionParamTypes',
  151. params
  152. );
  153. ipc.send(
  154. 'backendAction_decodeFunctionSignature',
  155. textSignature,
  156. data.data
  157. );
  158. }
  159. })
  160. .catch(function(bytesSignature) {
  161. TemplateVar.set(template, 'executionFunction', bytesSignature);
  162. TemplateVar.set(template, 'hasSignature', false);
  163. });
  164. }
  165. }
  166. if (data.from) {
  167. web3.eth.getCode(data.from, function(e, res) {
  168. if (!e && res && res.length > 2) {
  169. TemplateVar.set(template, 'fromIsContract', true);
  170. }
  171. });
  172. }
  173. // Estimate gas usage
  174. TemplateVar.set(template, 'gasLoading', true);
  175. var estimateData = _.clone(data);
  176. estimateData.gas = defaultEstimateGas;
  177. // In case estimateGas fails returning
  178. // (which seems to be happening in manual testing)
  179. // We'll set gasLoading back to false after 10s
  180. // so the user can see an error message
  181. const gasEstimationFailed = e => {
  182. console.log(
  183. 'Gas estimation failed. Falling back to max(provided, default) value',
  184. e
  185. );
  186. TemplateVar.set(template, 'gasLoading', false);
  187. TemplateVar.set(template, 'estimatedGas', defaultEstimateGas);
  188. TemplateVar.set(
  189. template,
  190. 'providedGas',
  191. Math.max(gas, defaultEstimateGas)
  192. );
  193. };
  194. var estimationFailEvent = setTimeout(gasEstimationFailed, 20000);
  195. web3.eth
  196. .estimateGas(estimateData)
  197. .then(function(value, error) {
  198. clearTimeout(estimationFailEvent);
  199. console.log('Estimated gas: ', value, error);
  200. if (!error && value) {
  201. // set the gas to the estimation, if not provided or lower
  202. Tracker.nonreactive(function() {
  203. var gas = Number(TemplateVar.get(template, 'providedGas'));
  204. if (value === defaultEstimateGas) {
  205. return TemplateVar.set(template, 'estimatedGas', 'invalid');
  206. }
  207. const estimatedGas = web3.utils.toBN(value || 0);
  208. TemplateVar.set(
  209. template,
  210. 'estimatedGas',
  211. estimatedGas.toNumber()
  212. );
  213. if (!gas && value) {
  214. TemplateVar.set(
  215. template,
  216. 'providedGas',
  217. estimatedGas.add(web3.utils.toBN(100000)).toNumber()
  218. );
  219. TemplateVar.set(
  220. template,
  221. 'initialProvidedGas',
  222. estimatedGas.add(web3.utils.toBN(100000)).toNumber()
  223. );
  224. }
  225. });
  226. } else {
  227. TemplateVar.set(template, 'estimatedGas', defaultEstimateGas);
  228. TemplateVar.set(template, 'providedGas', defaultEstimateGas);
  229. }
  230. TemplateVar.set(template, 'gasLoading', false);
  231. })
  232. .catch(gasEstimationFailed);
  233. }
  234. });
  235. });
  236. Template['popupWindows_sendTransactionConfirmation'].onRendered(function() {
  237. var template = this;
  238. Meteor.setTimeout(function() {
  239. template.$('input[type="password"]').focus();
  240. }, 200);
  241. });
  242. Template['popupWindows_sendTransactionConfirmation'].helpers({
  243. /**
  244. Returns the total amount
  245. @method (totalAmount)
  246. */
  247. totalAmount: function() {
  248. var amount = EthTools.formatBalance(
  249. web3.utils.toBN(this.value || 0),
  250. '0,0.00[0000000000000000]',
  251. 'ether'
  252. );
  253. var dotPos = ~amount.indexOf('.')
  254. ? amount.indexOf('.') + 3
  255. : amount.indexOf(',') + 3;
  256. return amount
  257. ? amount.substr(0, dotPos) +
  258. '<small style="font-size: 0.5em;">' +
  259. amount.substr(dotPos) +
  260. '</small>'
  261. : '0';
  262. },
  263. /**
  264. Calculates the fee used for this transaction in ether
  265. @method (estimatedFee)
  266. */
  267. estimatedFee: function() {
  268. var gas = TemplateVar.get('estimatedGas');
  269. if (gas && this.gasPrice) {
  270. const balance = web3.utils
  271. .toBN(gas || 0)
  272. .mul(web3.utils.toBN(this.gasPrice || 0));
  273. return EthTools.formatBalance(balance, '0,0.0[0000000] unit', 'ether');
  274. }
  275. },
  276. /**
  277. Calculates the provided gas amount in ether
  278. @method (providedGas)
  279. */
  280. providedGas: function() {
  281. var gas = TemplateVar.get('providedGas');
  282. if (gas && this.gasPrice) {
  283. const balance = web3.utils
  284. .toBN(gas || 0)
  285. .mul(web3.utils.toBN(this.gasPrice || 0));
  286. return EthTools.formatBalance(balance, '0,0.0[0000000]', 'ether');
  287. }
  288. },
  289. /**
  290. Shortens the address to 0xffff...ffff
  291. @method (shortenAddress)
  292. */
  293. shortenAddress: function(address) {
  294. if (_.isString(address)) {
  295. return address.substr(0, 6) + '...' + address.substr(-4);
  296. }
  297. },
  298. /**
  299. Formats the data so that all zeros are wrapped in span.zero
  300. @method (formattedData)
  301. */
  302. formattedData: function() {
  303. return TemplateVar.get('toIsContract')
  304. ? this.data
  305. .replace(/([0]{2,})/g, '<span class="zero">$1</span>')
  306. .replace(/(0x[a-f0-9]{8})/i, '<span class="function">$1</span>')
  307. : this.data.replace(/([0]{2,})/g, '<span class="zero">$1</span>');
  308. },
  309. params: function() {
  310. return TemplateVar.get('params');
  311. },
  312. /**
  313. Formats parameters
  314. @method (showFormattedParams)
  315. */
  316. showFormattedParams: function() {
  317. return TemplateVar.get('params') && TemplateVar.get('displayDecodedParams');
  318. },
  319. /**
  320. Checks if transaction will be invalid
  321. @method (transactionInvalid)
  322. */
  323. transactionInvalid: function() {
  324. return (
  325. TemplateVar.get('estimatedGas') === 'invalid' ||
  326. TemplateVar.get('estimatedGas') === 0 ||
  327. typeof TemplateVar.get('estimatedGas') === 'undefined'
  328. );
  329. }
  330. });
  331. Template['popupWindows_sendTransactionConfirmation'].events({
  332. /**
  333. Gets the new provided gas in ether amount and calculates the resulting providedGas
  334. @event change .provided-gas, input .provided-gas
  335. */
  336. 'change .provided-gas, input .provided-gas': function(e, template) {
  337. var gas = template
  338. .$('.provided-gas')
  339. .text()
  340. .replace(/[, ]+/g, ''); // template.$('.provided-gas').text();
  341. TemplateVar.set('providedGas', gas);
  342. },
  343. /**
  344. Increase the estimated gas
  345. @event click .not-enough-gas
  346. */
  347. 'click .not-enough-gas': function() {
  348. const gas = web3.utils
  349. .toBN(TemplateVar.get('estimatedGas') || 0)
  350. .add(web3.utils.toBN(100000))
  351. .toNumber();
  352. TemplateVar.set('initialProvidedGas', gas);
  353. TemplateVar.set('providedGas', gas);
  354. },
  355. /**
  356. Cancel the transaction confirmation and close the popup
  357. @event click .cancel
  358. */
  359. 'click .cancel': function() {
  360. ipc.send(
  361. 'backendAction_unlockedAccountAndSentTransaction',
  362. 'Transaction not confirmed'
  363. );
  364. ipc.send('backendAction_closePopupWindow');
  365. },
  366. /**
  367. Confirm the transaction
  368. @event submit form
  369. */
  370. 'submit form': async function(e, template) {
  371. e.preventDefault();
  372. var data = Session.get('data'),
  373. pw = template.find('input[type="password"]').value,
  374. gas = web3.utils.numberToHex(TemplateVar.get('providedGas'));
  375. // check if account is about to send to itself
  376. if (data.to && data.from === data.to.toLowerCase()) {
  377. GlobalNotification.warning({
  378. content: TAPi18n.__(
  379. 'mist.popupWindows.sendTransactionConfirmation.errors.sameAccount'
  380. ),
  381. duration: 5
  382. });
  383. return false;
  384. }
  385. console.log('Choosen Gas: ', gas, TemplateVar.get('providedGas'));
  386. if (!gas || !_.isFinite(gas)) {
  387. return;
  388. } else {
  389. data.gas = gas;
  390. }
  391. TemplateVar.set('unlocking', true);
  392. // get nonce
  393. const nonce = await web3.eth.getTransactionCount(data.from);
  394. const tx = Object.assign({}, data, {
  395. nonce: `0x${nonce.toString(16)}`
  396. });
  397. var signedTx;
  398. // sign transaction
  399. await web3.eth.personal.signTransaction(tx, pw || '', function(
  400. error,
  401. result
  402. ) {
  403. pw = null;
  404. if (error) {
  405. TemplateVar.set(template, 'unlocking', false);
  406. console.log(error);
  407. Tracker.afterFlush(function() {
  408. template.find('input[type="password"]').value = '';
  409. template.$('input[type="password"]').focus();
  410. });
  411. if (error.message.includes('Unable to connect to socket: timeout')) {
  412. GlobalNotification.warning({
  413. content: TAPi18n.__(
  414. 'mist.popupWindows.sendTransactionConfirmation.errors.connectionTimeout'
  415. ),
  416. duration: 5
  417. });
  418. } else if (
  419. error.message.includes('could not decrypt key with given passphrase')
  420. ) {
  421. GlobalNotification.warning({
  422. content: TAPi18n.__(
  423. 'mist.popupWindows.sendTransactionConfirmation.errors.wrongPassword'
  424. ),
  425. duration: 3
  426. });
  427. } else if (error.message.includes('multiple keys match address')) {
  428. GlobalNotification.warning({
  429. content: TAPi18n.__(
  430. 'mist.popupWindows.sendTransactionConfirmation.errors.multipleKeysMatchAddress'
  431. ),
  432. duration: 10
  433. });
  434. } else {
  435. GlobalNotification.warning({
  436. content: error.message || error,
  437. duration: 5
  438. });
  439. }
  440. return;
  441. }
  442. signedTx = result.raw;
  443. });
  444. if (!signedTx) {
  445. console.log('Error: no signedTx');
  446. TemplateVar.set(template, 'unlocking', false);
  447. return;
  448. }
  449. // send transaction!
  450. web3.eth.sendSignedTransaction(signedTx, (error, hash) => {
  451. TemplateVar.set(template, 'unlocking', false);
  452. if (error) {
  453. console.error(`Error from sendSignedTransaction: ${error}`);
  454. if (error.message.includes('Unable to connect to socket: timeout')) {
  455. GlobalNotification.warning({
  456. content: TAPi18n.__(
  457. 'mist.popupWindows.sendTransactionConfirmation.errors.connectionTimeout'
  458. ),
  459. duration: 5
  460. });
  461. } else if (
  462. error.message.includes('Insufficient funds for gas * price + value')
  463. ) {
  464. GlobalNotification.warning({
  465. content: TAPi18n.__(
  466. 'mist.popupWindows.sendTransactionConfirmation.errors.insufficientFundsForGas'
  467. ),
  468. duration: 5
  469. });
  470. } else {
  471. GlobalNotification.warning({
  472. content: error.message || error,
  473. duration: 5
  474. });
  475. }
  476. return;
  477. }
  478. ipc.send('backendAction_unlockedAccountAndSentTransaction', null, hash);
  479. });
  480. // In case sendSignedTransaction doesn't return,
  481. // after 75s we'll reset the form so at least
  482. // they can try again.
  483. setTimeout(() => {
  484. TemplateVar.set(template, 'unlocking', false);
  485. }, 75000);
  486. },
  487. 'click .data .toggle-panel': function() {
  488. TemplateVar.set('displayDecodedParams', true);
  489. },
  490. 'click .parameters .toggle-panel': function() {
  491. TemplateVar.set('displayDecodedParams', false);
  492. },
  493. 'click .lookup-function-signature': function(e, template) {
  494. var data = Session.get('data');
  495. TemplateVar.set('lookingUpFunctionSignature', true);
  496. remoteSignatureLookup(data.data)
  497. .then(function(textSignature) {
  498. TemplateVar.set(template, 'lookingUpFunctionSignature', false);
  499. // Clean version of function signature. Striping params
  500. TemplateVar.set(
  501. template,
  502. 'executionFunction',
  503. textSignature.replace(/\(.+$/g, '')
  504. );
  505. TemplateVar.set(template, 'hasSignature', true);
  506. var params = textSignature.match(/\((.+)\)/i);
  507. if (params) {
  508. console.log('params:', params);
  509. TemplateVar.set(template, 'executionFunctionParamTypes', params);
  510. ipc.send(
  511. 'backendAction_decodeFunctionSignature',
  512. textSignature,
  513. data.data
  514. );
  515. }
  516. })
  517. .catch(function(bytesSignature) {
  518. TemplateVar.set(template, 'lookingUpFunctionSignature', false);
  519. TemplateVar.set(template, 'executionFunction', bytesSignature);
  520. TemplateVar.set(template, 'hasSignature', false);
  521. });
  522. }
  523. });
  524. /**
  525. Set TemplateVar 'network'
  526. @method setNetwork
  527. */
  528. var setNetwork = function(template) {
  529. TemplateVar.set(template, 'network', store.getState().nodes.network);
  530. this.storeUnsubscribe = store.subscribe(() => {
  531. if (
  532. store.getState().nodes.network !== TemplateVar.get(template, 'network')
  533. ) {
  534. TemplateVar.set(template, 'network', store.getState().nodes.network);
  535. }
  536. });
  537. };