Merge pull request #5704 from MetaMask/new-gas-customize-feature-branch-d

Gas customization features
feature/default_network_editable
Dan J Miller 6 years ago committed by GitHub
commit d1996509de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 57
      app/_locales/en/messages.json
  2. 23
      app/scripts/controllers/transactions/index.js
  3. 7
      app/scripts/metamask-controller.js
  4. 315
      development/states/send-edit.json
  5. 315
      development/states/send-new-ui.json
  6. 317
      development/states/send.json
  7. 2
      gulpfile.js
  8. 2
      package.json
  9. 5
      test/e2e/beta/fetch-mocks.js
  10. 17
      test/e2e/beta/from-import-beta-ui.spec.js
  11. 8
      test/e2e/beta/metamask-beta-responsive-ui.spec.js
  12. 54
      test/e2e/beta/metamask-beta-ui.spec.js
  13. 35
      test/integration/lib/send-new-ui.js
  14. 2
      test/setup.js
  15. 67
      test/unit/app/controllers/transactions/tx-controller-test.js
  16. 53
      ui/app/actions.js
  17. 23
      ui/app/app.js
  18. 14
      ui/app/components/button-group/button-group.component.js
  19. 14
      ui/app/components/button-group/tests/button-group-component.test.js
  20. 6
      ui/app/components/button/button.component.js
  21. 144
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  22. 1
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js
  23. 191
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss
  24. 273
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js
  25. 1
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js
  26. 17
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss
  27. 30
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js
  28. 33
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js
  29. 11
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js
  30. 34
      ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js
  31. 1
      ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js
  32. 28
      ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss
  33. 82
      ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js
  34. 178
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  35. 283
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  36. 1
      ui/app/components/gas-customization/gas-modal-page-container/index.js
  37. 148
      ui/app/components/gas-customization/gas-modal-page-container/index.scss
  38. 273
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
  39. 360
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  40. 89
      ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js
  41. 1
      ui/app/components/gas-customization/gas-price-button-group/index.js
  42. 235
      ui/app/components/gas-customization/gas-price-button-group/index.scss
  43. 233
      ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js
  44. 108
      ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js
  45. 354
      ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js
  46. 1
      ui/app/components/gas-customization/gas-price-chart/index.js
  47. 132
      ui/app/components/gas-customization/gas-price-chart/index.scss
  48. 218
      ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
  49. 48
      ui/app/components/gas-customization/gas-slider/gas-slider.component.js
  50. 1
      ui/app/components/gas-customization/gas-slider/index.js
  51. 54
      ui/app/components/gas-customization/gas-slider/index.scss
  52. 14
      ui/app/components/gas-customization/gas.selectors.js
  53. 5
      ui/app/components/gas-customization/index.scss
  54. 10
      ui/app/components/index.scss
  55. 50
      ui/app/components/modals/modal.js
  56. 3
      ui/app/components/page-container/index.scss
  57. 6
      ui/app/components/page-container/page-container-footer/page-container-footer.component.js
  58. 7
      ui/app/components/page-container/page-container-header/page-container-header.component.js
  59. 11
      ui/app/components/page-container/page-container.component.js
  60. 16
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
  61. 3
      ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
  62. 4
      ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
  63. 11
      ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
  64. 18
      ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
  65. 26
      ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
  66. 59
      ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
  67. 5
      ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
  68. 42
      ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
  69. 94
      ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
  70. 13
      ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
  71. 14
      ui/app/components/send/send.component.js
  72. 8
      ui/app/components/send/send.container.js
  73. 10
      ui/app/components/send/send.selectors.js
  74. 50
      ui/app/components/send/tests/send-component.test.js
  75. 10
      ui/app/components/send/tests/send-container.test.js
  76. 2
      ui/app/components/send/tests/send-selectors.test.js
  77. 7
      ui/app/components/sidebars/index.scss
  78. 112
      ui/app/components/sidebars/sidebar-content.scss
  79. 21
      ui/app/components/sidebars/sidebar.component.js
  80. 9
      ui/app/components/sidebars/tests/sidebars-component.test.js
  81. 1
      ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
  82. 11
      ui/app/components/transaction-list-item/transaction-list-item.component.js
  83. 20
      ui/app/components/transaction-list-item/transaction-list-item.container.js
  84. 51
      ui/app/css/itcss/components/gas-slider.scss
  85. 1
      ui/app/css/itcss/components/newui-sections.scss
  86. 27
      ui/app/css/itcss/components/send.scss
  87. 4
      ui/app/css/itcss/generic/index.scss
  88. 3
      ui/app/css/itcss/settings/variables.scss
  89. 468
      ui/app/ducks/gas.duck.js
  90. 3
      ui/app/ducks/mock-gas-estimate-data.js
  91. 19
      ui/app/ducks/send.duck.js
  92. 544
      ui/app/ducks/tests/gas-duck.test.js
  93. 37
      ui/app/ducks/tests/send-duck.test.js
  94. 2
      ui/app/helpers/confirm-transaction/util.js
  95. 40
      ui/app/helpers/conversions.util.js
  96. 3
      ui/app/helpers/formatters.js
  97. 3
      ui/app/reducers.js
  98. 1
      ui/app/reducers/app.js
  99. 5
      ui/app/selectors.js
  100. 270
      ui/app/selectors/custom-gas.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -65,6 +65,9 @@
"address": {
"message": "Address"
},
"advancedOptions": {
"message": "Advanced Options"
},
"addCustomToken": {
"message": "Add custom token"
},
@ -80,12 +83,18 @@
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
"advanced": {
"message": "Advanced"
},
"amount": {
"message": "Amount"
},
"amountPlusGas": {
"message": "Amount + Gas"
},
"amountPlusTxFee": {
"message": "Amount + TX Fee"
},
"appDescription": {
"message": "Ethereum Browser Extension",
"description": "The description of the application"
@ -115,6 +124,9 @@
"available": {
"message": "Available"
},
"average": {
"message": "Average"
},
"back": {
"message": "Back"
},
@ -127,6 +139,9 @@
"balanceIsInsufficientGas": {
"message": "Insufficient balance for current gas total"
},
"basic": {
"message": "Basic"
},
"beta": {
"message": "BETA"
},
@ -303,6 +318,9 @@
"customGas": {
"message": "Customize Gas"
},
"customGasSubTitle": {
"message": "Increasing fee may decrease processing times, but it is not guaranteed."
},
"customToken": {
"message": "Custom Token"
},
@ -427,6 +445,15 @@
"failed": {
"message": "Failed"
},
"fast": {
"message": "Fast"
},
"fastest": {
"message": "Fastest"
},
"feeChartTitle": {
"message": "Live Transaction Fee Predictions"
},
"fiat": {
"message": "Fiat",
"description": "Exchange type"
@ -481,6 +508,9 @@
"gasPrice": {
"message": "Gas Price (GWEI)"
},
"gasPriceNoDenom": {
"message": "Gas Price"
},
"gasPriceCalculation": {
"message": "We calculate the suggested gas prices based on network success rates."
},
@ -689,6 +719,9 @@
"missingYourTokens": {
"message": "Don't see your tokens?"
},
"minutesShorthand": {
"message": "Min"
},
"myAccounts": {
"message": "My Accounts"
},
@ -755,6 +788,12 @@
"optionalNickname": {
"message": "Nickname (optional)"
},
"newTotal": {
"message": "New Total"
},
"newTransactionFee": {
"message": "New Transaction Fee"
},
"next": {
"message": "Next"
},
@ -820,6 +859,9 @@
"parameters": {
"message": "Parameters"
},
"originalTotal": {
"message": "Original Total"
},
"password": {
"message": "Password"
},
@ -993,6 +1035,9 @@
"save": {
"message": "Save"
},
"slow": {
"message": "Slow"
},
"saveAsCsvFile": {
"message": "Save as CSV File"
},
@ -1018,6 +1063,9 @@
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
"secondsShorthand": {
"message": "Sec"
},
"seedPhraseReq": {
"message": "Seed phrases are 12 words long"
},
@ -1039,6 +1087,9 @@
"send": {
"message": "Send"
},
"sendAmount": {
"message": "Send Amount"
},
"sendETH": {
"message": "Send ETH"
},
@ -1244,12 +1295,18 @@
"transactionErrorNoContract": {
"message": "Trying to call a function on a non-contract address."
},
"transactionFee": {
"message": "Transaction Fee"
},
"transactionMemo": {
"message": "Transaction memo (optional)"
},
"transactionNumber": {
"message": "Transaction Number"
},
"transactionTime": {
"message": "Transaction Time"
},
"transfer": {
"message": "Transfer"
},

@ -290,6 +290,29 @@ class TransactionController extends EventEmitter {
return newTxMeta
}
async createSpeedUpTransaction (originalTxId, customGasPrice) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice } = txParams
const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newTxMeta = this.txStateManager.generateTxMeta({
txParams: {
...txParams,
gasPrice: newGasPrice,
},
lastGasPrice,
loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED,
type: TRANSACTION_TYPE_RETRY,
})
this.addTx(newTxMeta)
await this.approveTransaction(newTxMeta.id)
return newTxMeta
}
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta

@ -445,6 +445,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
createCancelTransaction: nodeify(this.createCancelTransaction, this),
createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this),
@ -1162,6 +1163,12 @@ module.exports = class MetamaskController extends EventEmitter {
return state
}
async createSpeedUpTransaction (originalTxId, customGasPrice, cb) {
await this.txController.createSpeedUpTransaction(originalTxId, customGasPrice)
const state = await this.getState()
return state
}
estimateGas (estimateGasParams) {
return new Promise((resolve, reject) => {
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {

@ -176,5 +176,320 @@
"hexGasTotal": "",
"nonce": "",
"fetchingMethodData": false
},
"gas": {
"customData": {
"price": null,
"limit": "0x186a0"
},
"basicEstimates": {
"average": 73,
"avgWait": 10.6,
"blockTime": 13.871657754010695,
"blockNum": 6655504,
"fast": 160,
"fastest": 290,
"fastestWait": 0.5,
"fastWait": 0.6,
"safeLow": 50,
"safeLowWait": 16.1,
"speed": 0.6702462692280712
},
"basicEstimateIsLoading": false,
"gasEstimatesLoading": false,
"priceAndTimeEstimates": [
{
"expectedTime": "1374.1168296452973076627",
"expectedWait": 99.0593088449,
"gasprice": 4.1
},
{
"expectedTime": "1280.88976972896682763716",
"expectedWait": 92.3386225672,
"gasprice": 4.4
},
{
"expectedTime": "1245.13314632680319175597",
"expectedWait": 89.7609477113,
"gasprice": 4.8
},
{
"expectedTime": "1227.99925007911014385881",
"expectedWait": 88.5257747744,
"gasprice": 4.9
},
{
"expectedTime": "965.52572720362993349654",
"expectedWait": 69.6042062402,
"gasprice": 5
},
{
"expectedTime": "917.466895447437420776",
"expectedWait": 66.1396721082,
"gasprice": 5.1
},
{
"expectedTime": "915.81694044041496090521",
"expectedWait": 66.0207277804,
"gasprice": 5.2
},
{
"expectedTime": "902.13145619709089691874",
"expectedWait": 65.034148924,
"gasprice": 5.3
},
{
"expectedTime": "890.83591122200105749896",
"expectedWait": 64.2198594443,
"gasprice": 5.4
},
{
"expectedTime": "879.10469542971335712248",
"expectedWait": 63.3741627006,
"gasprice": 5.5
},
{
"expectedTime": "876.99737395823100420974",
"expectedWait": 63.2222470818,
"gasprice": 5.6
},
{
"expectedTime": "865.96781957003849098957",
"expectedWait": 62.4271327138,
"gasprice": 5.7
},
{
"expectedTime": "865.44839472121496158482",
"expectedWait": 62.3896876688,
"gasprice": 5.8
},
{
"expectedTime": "802.16173170976255602161",
"expectedWait": 57.8273877524,
"gasprice": 6
},
{
"expectedTime": "780.79313908053047074843",
"expectedWait": 56.2869379368,
"gasprice": 6.1
},
{
"expectedTime": "770.04888359616469549233",
"expectedWait": 55.5123906062,
"gasprice": 6.2
},
{
"expectedTime": "745.01007965146736962697",
"expectedWait": 53.7073573226,
"gasprice": 6.3
},
{
"expectedTime": "735.19921111598501681816",
"expectedWait": 53.0000973318,
"gasprice": 6.6
},
{
"expectedTime": "705.68767153912619368694",
"expectedWait": 50.8726270539,
"gasprice": 6.7
},
{
"expectedTime": "705.26438593445239690121",
"expectedWait": 50.8421126329,
"gasprice": 6.9
},
{
"expectedTime": "652.51573119854865429742",
"expectedWait": 47.0394918019,
"gasprice": 7
},
{
"expectedTime": "635.51471669299464383162",
"expectedWait": 45.813898235,
"gasprice": 7.1
},
{
"expectedTime": "634.37181911960854759036",
"expectedWait": 45.7315073922,
"gasprice": 7.2
},
{
"expectedTime": "633.23097691113902888918",
"expectedWait": 45.6492647195,
"gasprice": 7.3
},
{
"expectedTime": "112.7753456245379663928",
"expectedWait": 8.1299111919,
"gasprice": 7.6
},
{
"expectedTime": "102.9665314468898381829",
"expectedWait": 7.4227992986,
"gasprice": 8
},
{
"expectedTime": "100.94784507024919649891",
"expectedWait": 7.2772733339,
"gasprice": 8.1
},
{
"expectedTime": "100.46445647447807351078",
"expectedWait": 7.2424261221,
"gasprice": 8.8
},
{
"expectedTime": "84.91686745986737853339",
"expectedWait": 6.1216091808,
"gasprice": 9
},
{
"expectedTime": "80.39566429296684383503",
"expectedWait": 5.7956781892,
"gasprice": 9.1
},
{
"expectedTime": "78.24522052614759252715",
"expectedWait": 5.6406539084,
"gasprice": 9.2
},
{
"expectedTime": "77.1685119880459882636",
"expectedWait": 5.5630345959,
"gasprice": 9.5
},
{
"expectedTime": "72.43649507646737870178",
"expectedWait": 5.2219061601,
"gasprice": 9.8
},
{
"expectedTime": "71.48259532351443753818",
"expectedWait": 5.1531400638,
"gasprice": 9.9
},
{
"expectedTime": "58.23892805162994573827",
"expectedWait": 4.1984115442,
"gasprice": 10
},
{
"expectedTime": "53.13065124862245917617",
"expectedWait": 3.8301587446,
"gasprice": 10.1
},
{
"expectedTime": "53.03510209647058751971",
"expectedWait": 3.82327066,
"gasprice": 10.3
},
{
"expectedTime": "49.06846157804491912403",
"expectedWait": 3.5373177776,
"gasprice": 11
},
{
"expectedTime": "48.30893330101818116637",
"expectedWait": 3.4825638116,
"gasprice": 11.1
},
{
"expectedTime": "48.25099734861818116715",
"expectedWait": 3.4783872414,
"gasprice": 11.3
},
{
"expectedTime": "47.64416885027272662988",
"expectedWait": 3.4346413165,
"gasprice": 11.9
},
{
"expectedTime": "46.76354741392085498401",
"expectedWait": 3.3711578128,
"gasprice": 12.6
},
{
"expectedTime": "44.99427448545882292232",
"expectedWait": 3.2436119232,
"gasprice": 13
},
{
"expectedTime": "44.61790554199251276697",
"expectedWait": 3.2164796979,
"gasprice": 13.1
},
{
"expectedTime": "42.87832690973048070488",
"expectedWait": 3.0910744534,
"gasprice": 14
},
{
"expectedTime": "42.21224091308663044649",
"expectedWait": 3.0430566888,
"gasprice": 14.9
},
{
"expectedTime": "41.15715335111336842864",
"expectedWait": 2.9669960203,
"gasprice": 15
},
{
"expectedTime": "40.9600723880876999821",
"expectedWait": 2.9527885646,
"gasprice": 15.1
},
{
"expectedTime": "38.89138450301711177472",
"expectedWait": 2.8036580193,
"gasprice": 15.8
},
{
"expectedTime": "37.89655640860213852611",
"expectedWait": 2.7319414219,
"gasprice": 16
},
{
"expectedTime": "37.35265517364705831954",
"expectedWait": 2.692731888,
"gasprice": 17.4
},
{
"expectedTime": "36.79447683873796741798",
"expectedWait": 2.652493126,
"gasprice": 17.8
},
{
"expectedTime": "36.11439350850802090309",
"expectedWait": 2.6034663015,
"gasprice": 19
},
{
"expectedTime": "31.32676199432192471101",
"expectedWait": 2.2583286403,
"gasprice": 20
},
{
"expectedTime": "30.76792490132192471855",
"expectedWait": 2.2180423888,
"gasprice": 20.1
},
{
"expectedTime": "29.94493658520962526441",
"expectedWait": 2.1587136243,
"gasprice": 25
},
{
"expectedTime": "29.53287347625561457478",
"expectedWait": 2.1290082267,
"gasprice": 29
},
{
"expectedTime": "29.09318627175614934008",
"expectedWait": 2.0973114236,
"gasprice": 47
}
],
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
}
}

@ -158,5 +158,320 @@
"hexGasTotal": "",
"nonce": "",
"fetchingMethodData": false
},
"gas": {
"customData": {
"price": null,
"limit": "0x186a0"
},
"basicEstimates": {
"average": 73,
"avgWait": 10.6,
"blockTime": 13.871657754010695,
"blockNum": 6655504,
"fast": 160,
"fastest": 290,
"fastestWait": 0.5,
"fastWait": 0.6,
"safeLow": 50,
"safeLowWait": 16.1,
"speed": 0.6702462692280712
},
"basicEstimateIsLoading": false,
"gasEstimatesLoading": false,
"priceAndTimeEstimates": [
{
"expectedTime": "1374.1168296452973076627",
"expectedWait": 99.0593088449,
"gasprice": 4.1
},
{
"expectedTime": "1280.88976972896682763716",
"expectedWait": 92.3386225672,
"gasprice": 4.4
},
{
"expectedTime": "1245.13314632680319175597",
"expectedWait": 89.7609477113,
"gasprice": 4.8
},
{
"expectedTime": "1227.99925007911014385881",
"expectedWait": 88.5257747744,
"gasprice": 4.9
},
{
"expectedTime": "965.52572720362993349654",
"expectedWait": 69.6042062402,
"gasprice": 5
},
{
"expectedTime": "917.466895447437420776",
"expectedWait": 66.1396721082,
"gasprice": 5.1
},
{
"expectedTime": "915.81694044041496090521",
"expectedWait": 66.0207277804,
"gasprice": 5.2
},
{
"expectedTime": "902.13145619709089691874",
"expectedWait": 65.034148924,
"gasprice": 5.3
},
{
"expectedTime": "890.83591122200105749896",
"expectedWait": 64.2198594443,
"gasprice": 5.4
},
{
"expectedTime": "879.10469542971335712248",
"expectedWait": 63.3741627006,
"gasprice": 5.5
},
{
"expectedTime": "876.99737395823100420974",
"expectedWait": 63.2222470818,
"gasprice": 5.6
},
{
"expectedTime": "865.96781957003849098957",
"expectedWait": 62.4271327138,
"gasprice": 5.7
},
{
"expectedTime": "865.44839472121496158482",
"expectedWait": 62.3896876688,
"gasprice": 5.8
},
{
"expectedTime": "802.16173170976255602161",
"expectedWait": 57.8273877524,
"gasprice": 6
},
{
"expectedTime": "780.79313908053047074843",
"expectedWait": 56.2869379368,
"gasprice": 6.1
},
{
"expectedTime": "770.04888359616469549233",
"expectedWait": 55.5123906062,
"gasprice": 6.2
},
{
"expectedTime": "745.01007965146736962697",
"expectedWait": 53.7073573226,
"gasprice": 6.3
},
{
"expectedTime": "735.19921111598501681816",
"expectedWait": 53.0000973318,
"gasprice": 6.6
},
{
"expectedTime": "705.68767153912619368694",
"expectedWait": 50.8726270539,
"gasprice": 6.7
},
{
"expectedTime": "705.26438593445239690121",
"expectedWait": 50.8421126329,
"gasprice": 6.9
},
{
"expectedTime": "652.51573119854865429742",
"expectedWait": 47.0394918019,
"gasprice": 7
},
{
"expectedTime": "635.51471669299464383162",
"expectedWait": 45.813898235,
"gasprice": 7.1
},
{
"expectedTime": "634.37181911960854759036",
"expectedWait": 45.7315073922,
"gasprice": 7.2
},
{
"expectedTime": "633.23097691113902888918",
"expectedWait": 45.6492647195,
"gasprice": 7.3
},
{
"expectedTime": "112.7753456245379663928",
"expectedWait": 8.1299111919,
"gasprice": 7.6
},
{
"expectedTime": "102.9665314468898381829",
"expectedWait": 7.4227992986,
"gasprice": 8
},
{
"expectedTime": "100.94784507024919649891",
"expectedWait": 7.2772733339,
"gasprice": 8.1
},
{
"expectedTime": "100.46445647447807351078",
"expectedWait": 7.2424261221,
"gasprice": 8.8
},
{
"expectedTime": "84.91686745986737853339",
"expectedWait": 6.1216091808,
"gasprice": 9
},
{
"expectedTime": "80.39566429296684383503",
"expectedWait": 5.7956781892,
"gasprice": 9.1
},
{
"expectedTime": "78.24522052614759252715",
"expectedWait": 5.6406539084,
"gasprice": 9.2
},
{
"expectedTime": "77.1685119880459882636",
"expectedWait": 5.5630345959,
"gasprice": 9.5
},
{
"expectedTime": "72.43649507646737870178",
"expectedWait": 5.2219061601,
"gasprice": 9.8
},
{
"expectedTime": "71.48259532351443753818",
"expectedWait": 5.1531400638,
"gasprice": 9.9
},
{
"expectedTime": "58.23892805162994573827",
"expectedWait": 4.1984115442,
"gasprice": 10
},
{
"expectedTime": "53.13065124862245917617",
"expectedWait": 3.8301587446,
"gasprice": 10.1
},
{
"expectedTime": "53.03510209647058751971",
"expectedWait": 3.82327066,
"gasprice": 10.3
},
{
"expectedTime": "49.06846157804491912403",
"expectedWait": 3.5373177776,
"gasprice": 11
},
{
"expectedTime": "48.30893330101818116637",
"expectedWait": 3.4825638116,
"gasprice": 11.1
},
{
"expectedTime": "48.25099734861818116715",
"expectedWait": 3.4783872414,
"gasprice": 11.3
},
{
"expectedTime": "47.64416885027272662988",
"expectedWait": 3.4346413165,
"gasprice": 11.9
},
{
"expectedTime": "46.76354741392085498401",
"expectedWait": 3.3711578128,
"gasprice": 12.6
},
{
"expectedTime": "44.99427448545882292232",
"expectedWait": 3.2436119232,
"gasprice": 13
},
{
"expectedTime": "44.61790554199251276697",
"expectedWait": 3.2164796979,
"gasprice": 13.1
},
{
"expectedTime": "42.87832690973048070488",
"expectedWait": 3.0910744534,
"gasprice": 14
},
{
"expectedTime": "42.21224091308663044649",
"expectedWait": 3.0430566888,
"gasprice": 14.9
},
{
"expectedTime": "41.15715335111336842864",
"expectedWait": 2.9669960203,
"gasprice": 15
},
{
"expectedTime": "40.9600723880876999821",
"expectedWait": 2.9527885646,
"gasprice": 15.1
},
{
"expectedTime": "38.89138450301711177472",
"expectedWait": 2.8036580193,
"gasprice": 15.8
},
{
"expectedTime": "37.89655640860213852611",
"expectedWait": 2.7319414219,
"gasprice": 16
},
{
"expectedTime": "37.35265517364705831954",
"expectedWait": 2.692731888,
"gasprice": 17.4
},
{
"expectedTime": "36.79447683873796741798",
"expectedWait": 2.652493126,
"gasprice": 17.8
},
{
"expectedTime": "36.11439350850802090309",
"expectedWait": 2.6034663015,
"gasprice": 19
},
{
"expectedTime": "31.32676199432192471101",
"expectedWait": 2.2583286403,
"gasprice": 20
},
{
"expectedTime": "30.76792490132192471855",
"expectedWait": 2.2180423888,
"gasprice": 20.1
},
{
"expectedTime": "29.94493658520962526441",
"expectedWait": 2.1587136243,
"gasprice": 25
},
{
"expectedTime": "29.53287347625561457478",
"expectedWait": 2.1290082267,
"gasprice": 29
},
{
"expectedTime": "29.09318627175614934008",
"expectedWait": 2.0973114236,
"gasprice": 47
}
],
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
}
}

@ -107,5 +107,320 @@
"scrollToBottom": false,
"forgottenPassword": null
},
"identities": {}
"identities": {},
"gas": {
"customData": {
"price": null,
"limit": "0x186a0"
},
"basicEstimates": {
"average": 73,
"avgWait": 10.6,
"blockTime": 13.871657754010695,
"blockNum": 6655504,
"fast": 160,
"fastest": 290,
"fastestWait": 0.5,
"fastWait": 0.6,
"safeLow": 50,
"safeLowWait": 16.1,
"speed": 0.6702462692280712
},
"basicEstimateIsLoading": false,
"gasEstimatesLoading": false,
"priceAndTimeEstimates": [
{
"expectedTime": "1374.1168296452973076627",
"expectedWait": 99.0593088449,
"gasprice": 4.1
},
{
"expectedTime": "1280.88976972896682763716",
"expectedWait": 92.3386225672,
"gasprice": 4.4
},
{
"expectedTime": "1245.13314632680319175597",
"expectedWait": 89.7609477113,
"gasprice": 4.8
},
{
"expectedTime": "1227.99925007911014385881",
"expectedWait": 88.5257747744,
"gasprice": 4.9
},
{
"expectedTime": "965.52572720362993349654",
"expectedWait": 69.6042062402,
"gasprice": 5
},
{
"expectedTime": "917.466895447437420776",
"expectedWait": 66.1396721082,
"gasprice": 5.1
},
{
"expectedTime": "915.81694044041496090521",
"expectedWait": 66.0207277804,
"gasprice": 5.2
},
{
"expectedTime": "902.13145619709089691874",
"expectedWait": 65.034148924,
"gasprice": 5.3
},
{
"expectedTime": "890.83591122200105749896",
"expectedWait": 64.2198594443,
"gasprice": 5.4
},
{
"expectedTime": "879.10469542971335712248",
"expectedWait": 63.3741627006,
"gasprice": 5.5
},
{
"expectedTime": "876.99737395823100420974",
"expectedWait": 63.2222470818,
"gasprice": 5.6
},
{
"expectedTime": "865.96781957003849098957",
"expectedWait": 62.4271327138,
"gasprice": 5.7
},
{
"expectedTime": "865.44839472121496158482",
"expectedWait": 62.3896876688,
"gasprice": 5.8
},
{
"expectedTime": "802.16173170976255602161",
"expectedWait": 57.8273877524,
"gasprice": 6
},
{
"expectedTime": "780.79313908053047074843",
"expectedWait": 56.2869379368,
"gasprice": 6.1
},
{
"expectedTime": "770.04888359616469549233",
"expectedWait": 55.5123906062,
"gasprice": 6.2
},
{
"expectedTime": "745.01007965146736962697",
"expectedWait": 53.7073573226,
"gasprice": 6.3
},
{
"expectedTime": "735.19921111598501681816",
"expectedWait": 53.0000973318,
"gasprice": 6.6
},
{
"expectedTime": "705.68767153912619368694",
"expectedWait": 50.8726270539,
"gasprice": 6.7
},
{
"expectedTime": "705.26438593445239690121",
"expectedWait": 50.8421126329,
"gasprice": 6.9
},
{
"expectedTime": "652.51573119854865429742",
"expectedWait": 47.0394918019,
"gasprice": 7
},
{
"expectedTime": "635.51471669299464383162",
"expectedWait": 45.813898235,
"gasprice": 7.1
},
{
"expectedTime": "634.37181911960854759036",
"expectedWait": 45.7315073922,
"gasprice": 7.2
},
{
"expectedTime": "633.23097691113902888918",
"expectedWait": 45.6492647195,
"gasprice": 7.3
},
{
"expectedTime": "112.7753456245379663928",
"expectedWait": 8.1299111919,
"gasprice": 7.6
},
{
"expectedTime": "102.9665314468898381829",
"expectedWait": 7.4227992986,
"gasprice": 8
},
{
"expectedTime": "100.94784507024919649891",
"expectedWait": 7.2772733339,
"gasprice": 8.1
},
{
"expectedTime": "100.46445647447807351078",
"expectedWait": 7.2424261221,
"gasprice": 8.8
},
{
"expectedTime": "84.91686745986737853339",
"expectedWait": 6.1216091808,
"gasprice": 9
},
{
"expectedTime": "80.39566429296684383503",
"expectedWait": 5.7956781892,
"gasprice": 9.1
},
{
"expectedTime": "78.24522052614759252715",
"expectedWait": 5.6406539084,
"gasprice": 9.2
},
{
"expectedTime": "77.1685119880459882636",
"expectedWait": 5.5630345959,
"gasprice": 9.5
},
{
"expectedTime": "72.43649507646737870178",
"expectedWait": 5.2219061601,
"gasprice": 9.8
},
{
"expectedTime": "71.48259532351443753818",
"expectedWait": 5.1531400638,
"gasprice": 9.9
},
{
"expectedTime": "58.23892805162994573827",
"expectedWait": 4.1984115442,
"gasprice": 10
},
{
"expectedTime": "53.13065124862245917617",
"expectedWait": 3.8301587446,
"gasprice": 10.1
},
{
"expectedTime": "53.03510209647058751971",
"expectedWait": 3.82327066,
"gasprice": 10.3
},
{
"expectedTime": "49.06846157804491912403",
"expectedWait": 3.5373177776,
"gasprice": 11
},
{
"expectedTime": "48.30893330101818116637",
"expectedWait": 3.4825638116,
"gasprice": 11.1
},
{
"expectedTime": "48.25099734861818116715",
"expectedWait": 3.4783872414,
"gasprice": 11.3
},
{
"expectedTime": "47.64416885027272662988",
"expectedWait": 3.4346413165,
"gasprice": 11.9
},
{
"expectedTime": "46.76354741392085498401",
"expectedWait": 3.3711578128,
"gasprice": 12.6
},
{
"expectedTime": "44.99427448545882292232",
"expectedWait": 3.2436119232,
"gasprice": 13
},
{
"expectedTime": "44.61790554199251276697",
"expectedWait": 3.2164796979,
"gasprice": 13.1
},
{
"expectedTime": "42.87832690973048070488",
"expectedWait": 3.0910744534,
"gasprice": 14
},
{
"expectedTime": "42.21224091308663044649",
"expectedWait": 3.0430566888,
"gasprice": 14.9
},
{
"expectedTime": "41.15715335111336842864",
"expectedWait": 2.9669960203,
"gasprice": 15
},
{
"expectedTime": "40.9600723880876999821",
"expectedWait": 2.9527885646,
"gasprice": 15.1
},
{
"expectedTime": "38.89138450301711177472",
"expectedWait": 2.8036580193,
"gasprice": 15.8
},
{
"expectedTime": "37.89655640860213852611",
"expectedWait": 2.7319414219,
"gasprice": 16
},
{
"expectedTime": "37.35265517364705831954",
"expectedWait": 2.692731888,
"gasprice": 17.4
},
{
"expectedTime": "36.79447683873796741798",
"expectedWait": 2.652493126,
"gasprice": 17.8
},
{
"expectedTime": "36.11439350850802090309",
"expectedWait": 2.6034663015,
"gasprice": 19
},
{
"expectedTime": "31.32676199432192471101",
"expectedWait": 2.2583286403,
"gasprice": 20
},
{
"expectedTime": "30.76792490132192471855",
"expectedWait": 2.2180423888,
"gasprice": 20.1
},
{
"expectedTime": "29.94493658520962526441",
"expectedWait": 2.1587136243,
"gasprice": 25
},
{
"expectedTime": "29.53287347625561457478",
"expectedWait": 2.1290082267,
"gasprice": 29
},
{
"expectedTime": "29.09318627175614934008",
"expectedWait": 2.0973114236,
"gasprice": 47
}
],
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
}
}

@ -30,10 +30,12 @@ const packageJSON = require('./package.json')
const dependencies = Object.keys(packageJSON && packageJSON.dependencies || {})
const materialUIDependencies = ['@material-ui/core']
const reactDepenendencies = dependencies.filter(dep => dep.match(/react/))
const d3Dependencies = ['c3', 'd3']
const uiDependenciesToBundle = [
...materialUIDependencies,
...reactDepenendencies,
...d3Dependencies,
]
function gulpParallel (...args) {

@ -98,11 +98,13 @@
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4",
"browserify-unibabel": "^3.0.0",
"c3": "^0.6.7",
"classnames": "^2.2.5",
"clone": "^2.1.2",
"copy-to-clipboard": "^3.0.8",
"css-loader": "^0.28.11",
"currency-formatter": "^1.4.2",
"d3": "^5.7.0",
"debounce": "1.1.0",
"debounce-stream": "^2.0.0",
"deep-extend": "^0.5.1",

File diff suppressed because one or more lines are too long

@ -17,6 +17,7 @@ const {
findElement,
findElements,
} = require('./helpers')
const fetchMockResponses = require('./fetch-mocks.js')
describe('Using MetaMask with an existing account', function () {
@ -62,6 +63,18 @@ describe('Using MetaMask with an existing account', function () {
await driver.get(extensionUrl)
})
beforeEach(async function () {
await driver.executeScript(
'window.fetch = ' +
'(...args) => { ' +
'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasBasic + '\')) }); } else if ' +
'(args[0] === "https://ethgasstation.info/json/predictTable.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } ' +
'return window.fetch(...args); }'
)
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
@ -230,7 +243,7 @@ describe('Using MetaMask with an existing account', function () {
})
describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () {
it('starts a send transaction', async function () {
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
@ -241,7 +254,7 @@ describe('Using MetaMask with an existing account', function () {
await inputAmount.sendKeys('1')
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn'))
await configureGas.click()
await delay(regularDelayMs)

@ -276,9 +276,11 @@ describe('MetaMask', function () {
const inputValue = await inputAmount.getAttribute('value')
assert.equal(inputValue, '1')
})
it('opens and closes the gas modal', async function () {
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn'))
await configureGas.click()
await delay(regularDelayMs)
@ -286,9 +288,11 @@ describe('MetaMask', function () {
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
await driver.wait(until.stalenessOf(gasModal), 10000)
await delay(regularDelayMs)
})
it('clicks through to the confirm screen', async function () {
// Continue to next screen
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()

@ -22,6 +22,7 @@ const {
verboseReportOnFailure,
waitUntilXWindowHandles,
} = require('./helpers')
const fetchMockResponses = require('./fetch-mocks.js')
describe('MetaMask', function () {
let extensionId
@ -66,6 +67,20 @@ describe('MetaMask', function () {
await driver.get(extensionUrl)
})
beforeEach(async function () {
await driver.executeScript(
'window.fetch = ' +
'(...args) => { ' +
'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasBasic + '\')) }); } else if ' +
'(args[0] === "https://ethgasstation.info/json/predictTable.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' +
'(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' +
'return window.fetch(...args); }'
)
})
afterEach(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await checkBrowserForConsoleErrors(driver)
@ -336,7 +351,7 @@ describe('MetaMask', function () {
})
describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () {
it('starts a send transaction', async function () {
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
@ -350,12 +365,11 @@ describe('MetaMask', function () {
assert.equal(inputValue, '1')
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn'))
await configureGas.click()
await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
@ -404,12 +418,12 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click()
})
it('initiates a send from the dapp', async () => {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
})
it('initiates a send from the dapp', async () => {
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(5000)
@ -658,9 +672,12 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const gasModal = await findElement(driver, By.css('span .modal'))
await driver.wait(until.elementLocated(By.css('.customize-gas__title')), 10000)
await delay(regularDelayMs)
const modalTabs = await findElements(driver, By.css('.page-container__tab'))
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
await gasPriceInput.clear()
await gasPriceInput.sendKeys('10')
await gasLimitInput.clear()
@ -815,15 +832,16 @@ describe('MetaMask', function () {
await inputAmount.sendKeys('50')
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
const configureGas = await findElement(driver, By.css('.advanced-gas-options-btn'))
await configureGas.click()
await delay(regularDelayMs)
gasModal = await driver.findElement(By.css('span .modal'))
await delay(regularDelayMs)
})
it('opens customizes gas modal', async () => {
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
it('opens customize gas modal', async () => {
await driver.wait(until.elementLocated(By.css('.page-container__title')))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await delay(regularDelayMs)
@ -919,9 +937,11 @@ describe('MetaMask', function () {
})
it('customizes gas', async () => {
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const modalTabs = await findElements(driver, By.css('.page-container__tab'))
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
await gasPriceInput.clear()
await delay(tinyDelayMs)
await gasPriceInput.sendKeys('10')
@ -938,7 +958,7 @@ describe('MetaMask', function () {
await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
const save = await findElement(driver, By.css('.customize-gas__save'))
const save = await findElement(driver, By.css('.page-container__footer-button'))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
@ -1042,9 +1062,11 @@ describe('MetaMask', function () {
})
it('customizes gas', async () => {
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const modalTabs = await findElements(driver, By.css('.page-container__tab'))
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
await gasPriceInput.clear()
await delay(tinyDelayMs)
await gasPriceInput.sendKeys('10')
@ -1061,7 +1083,7 @@ describe('MetaMask', function () {
await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
const save = await findElement(driver, By.css('.customize-gas__save'))
const save = await findElement(driver, By.css('.page-container__footer-button'))
await save.click()
await driver.wait(until.stalenessOf(gasModal))

@ -21,37 +21,6 @@ global.ethQuery = {
global.ethereumProvider = {}
async function customizeGas (assert, price, limit, ethFee, usdFee) {
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
sendGasOpenCustomizeModalButton[0].click()
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
assert.ok(customizeGasModal[0], 'should render the customize gas modal')
const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input')
customizeGasPriceInput.val(price)
reactTriggerChange(customizeGasPriceInput[0])
const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input')
customizeGasLimitInput.val(limit)
reactTriggerChange(customizeGasLimitInput[0])
const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save')
customizeGasSaveButton[0].click()
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal(
(await findAsync(sendGasField, '.currency-display-component'))[0].textContent,
ethFee,
'send gas field should show customized gas total'
)
assert.equal(
(await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent,
usdFee,
'send gas field should show customized gas total converted to USD'
)
}
async function runSendFlowTest (assert, done) {
console.log('*** start runSendFlowTest')
const selectState = await queryAsync($, 'select')
@ -112,10 +81,6 @@ async function runSendFlowTest (assert, done) {
errorMessage = $('.send-v2__error')
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected')
await customizeGas(assert, 0, 21000, '0ETH', '$0.00USD')
await customizeGas(assert, 1, 21000, '0.000021ETH', '$0.03USD')
await customizeGas(assert, 500, 60000, '0.03ETH', '$36.03USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
sendButton[0].click()

@ -3,3 +3,5 @@ require('babel-register')({
})
require('./helper')
window.SVGPathElement = window.SVGPathElement || { prototype: {} }

@ -5,6 +5,9 @@ const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const sinon = require('sinon')
const TransactionController = require('../../../../../app/scripts/controllers/transactions')
const {
TRANSACTION_TYPE_RETRY,
} = require('../../../../../app/scripts/controllers/transactions/enums')
const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider')
const noop = () => true
@ -392,6 +395,70 @@ describe('Transaction Controller', function () {
})
describe('#createSpeedUpTransaction', () => {
let addTxSpy
let approveTransactionSpy
let txParams
let expectedTxParams
beforeEach(() => {
addTxSpy = sinon.spy(txController, 'addTx')
approveTransactionSpy = sinon.spy(txController, 'approveTransaction')
txParams = {
nonce: '0x00',
from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
gas: '0x5209',
gasPrice: '0xa',
}
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [{}] },
])
expectedTxParams = Object.assign({}, txParams, { gasPrice: '0xb'})
})
afterEach(() => {
addTxSpy.restore()
approveTransactionSpy.restore()
})
it('should call this.addTx and this.approveTransaction with the expected args', async () => {
await txController.createSpeedUpTransaction(1)
assert.equal(addTxSpy.callCount, 1)
const addTxArgs = addTxSpy.getCall(0).args[0]
assert.deepEqual(addTxArgs.txParams, expectedTxParams)
const { lastGasPrice, type } = addTxArgs
assert.deepEqual({ lastGasPrice, type }, {
lastGasPrice: '0xa',
type: TRANSACTION_TYPE_RETRY,
})
})
it('should call this.approveTransaction with the id of the returned tx', async () => {
const result = await txController.createSpeedUpTransaction(1)
assert.equal(approveTransactionSpy.callCount, 1)
const approveTransactionArg = approveTransactionSpy.getCall(0).args[0]
assert.equal(result.id, approveTransactionArg)
})
it('should return the expected txMeta', async () => {
const result = await txController.createSpeedUpTransaction(1)
assert.deepEqual(result.txParams, expectedTxParams)
const { lastGasPrice, type } = result
assert.deepEqual({ lastGasPrice, type }, {
lastGasPrice: '0xa',
type: TRANSACTION_TYPE_RETRY,
})
})
})
describe('#publishTransaction', function () {
let hash, txMeta
beforeEach(function () {

@ -3,7 +3,6 @@ const pify = require('pify')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util')
const {
calcGasTotal,
calcTokenBalance,
estimateGas,
} = require('./components/send/send.utils')
@ -12,6 +11,7 @@ const { fetchLocale } = require('../i18n-helper')
const log = require('loglevel')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums')
const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
const gasDuck = require('./ducks/gas.duck')
const WebcamUtils = require('../lib/webcam-utils')
var actions = {
@ -325,6 +325,8 @@ var actions = {
clearPendingTokens,
createCancelTransaction,
createSpeedUpTransaction,
approveProviderRequest,
rejectProviderRequest,
clearApprovedOrigins,
@ -921,6 +923,7 @@ function setGasTotal (gasTotal) {
}
function updateGasData ({
gasPrice,
blockGasLimit,
recentBlocks,
selectedAddress,
@ -931,34 +934,19 @@ function updateGasData ({
}) {
return (dispatch) => {
dispatch(actions.gasLoadingStarted())
return new Promise((resolve, reject) => {
background.getGasPrice((err, data) => {
if (err) return reject(err)
return resolve(data)
})
})
.then(estimateGasPrice => {
return Promise.all([
Promise.resolve(estimateGasPrice),
estimateGas({
return estimateGas({
estimateGasMethod: background.estimateGas,
blockGasLimit,
selectedAddress,
selectedToken,
to,
value,
estimateGasPrice,
estimateGasPrice: gasPrice,
data,
}),
])
})
.then(([gasPrice, gas]) => {
dispatch(actions.setGasPrice(gasPrice))
.then(gas => {
dispatch(actions.setGasLimit(gas))
return calcGasTotal(gas, gasPrice)
})
.then((gasEstimate) => {
dispatch(actions.setGasTotal(gasEstimate))
dispatch(gasDuck.setCustomGasLimit(gas))
dispatch(updateSendErrors({ gasLoadingError: null }))
dispatch(actions.gasLoadingFinished())
})
@ -1851,6 +1839,28 @@ function createCancelTransaction (txId, customGasPrice) {
}
}
function createSpeedUpTransaction (txId, customGasPrice) {
log.debug('background.createSpeedUpTransaction')
let newTx
return dispatch => {
return new Promise((resolve, reject) => {
background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => {
if (err) {
dispatch(actions.displayWarning(err.message))
reject(err)
}
const { selectedAddressTxList } = newState
newTx = selectedAddressTxList[selectedAddressTxList.length - 1]
resolve(newState)
})
})
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => newTx)
}
}
//
// config
//
@ -1951,12 +1961,13 @@ function hideModal (payload) {
}
}
function showSidebar ({ transitionName, type }) {
function showSidebar ({ transitionName, type, props }) {
return {
type: actions.SIDEBAR_OPEN,
value: {
transitionName,
type,
props,
},
}
}

@ -43,6 +43,10 @@ const Alert = require('./components/alert')
import AppHeader from './components/app-header'
import UnlockPage from './components/pages/unlock-page'
import {
submittedPendingTransactionsSelector,
} from './selectors/transactions'
// Routes
const {
DEFAULT_ROUTE,
@ -106,12 +110,21 @@ class App extends Component {
currentView,
setMouseUserState,
sidebar,
submittedPendingTransactions,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
this.getConnectingLabel(loadingMessage) : null
log.debug('Main ui render function')
const {
isOpen: sidebarIsOpen,
transitionName: sidebarTransitionName,
type: sidebarType,
props,
} = sidebar
const { transaction: sidebarTransaction } = props || {}
return (
h('.flex-column.full-height', {
className: classnames({ 'mouse-user-styles': isMouseUser }),
@ -139,10 +152,12 @@ class App extends Component {
// sidebar
h(Sidebar, {
sidebarOpen: sidebar.isOpen,
sidebarOpen: sidebarIsOpen,
sidebarShouldClose: sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id),
hideSidebar: this.props.hideSidebar,
transitionName: sidebar.transitionName,
type: sidebar.type,
transitionName: sidebarTransitionName,
type: sidebarType,
sidebarProps: sidebar.props,
}),
// network dropdown
@ -254,6 +269,7 @@ App.propTypes = {
activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string,
submittedPendingTransactions: PropTypes.array,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
@ -313,6 +329,7 @@ function mapStateToProps (state) {
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords,
submittedPendingTransactions: submittedPendingTransactionsSelector(state),
unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount,

@ -5,18 +5,30 @@ import classnames from 'classnames'
export default class ButtonGroup extends PureComponent {
static propTypes = {
defaultActiveButtonIndex: PropTypes.number,
noButtonActiveByDefault: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.array,
className: PropTypes.string,
style: PropTypes.object,
newActiveButtonIndex: PropTypes.number,
}
static defaultProps = {
className: 'button-group',
defaultActiveButtonIndex: 0,
}
state = {
activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
activeButtonIndex: this.props.noButtonActiveByDefault
? null
: this.props.defaultActiveButtonIndex,
}
componentDidUpdate (_, prevState) {
// Provides an API for dynamically updating the activeButtonIndex
if (typeof this.props.newActiveButtonIndex === 'number' && prevState.activeButtonIndex !== this.props.newActiveButtonIndex) {
this.setState({ activeButtonIndex: this.props.newActiveButtonIndex })
}
}
handleButtonClick (activeButtonIndex) {

@ -35,6 +35,20 @@ describe('ButtonGroup Component', function () {
ButtonGroup.prototype.renderButtons.resetHistory()
})
describe('componentDidUpdate', () => {
it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => {
assert.equal(wrapper.state('activeButtonIndex'), 1)
wrapper.setProps({ newActiveButtonIndex: 2 })
assert.equal(wrapper.state('activeButtonIndex'), 2)
})
it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => {
assert.equal(wrapper.state('activeButtonIndex'), 1)
wrapper.setProps({ newActiveButtonIndex: null })
assert.equal(wrapper.state('activeButtonIndex'), 1)
})
})
describe('handleButtonClick', () => {
it('should set the activeButtonIndex', () => {
assert.equal(wrapper.state('activeButtonIndex'), 1)

@ -22,7 +22,11 @@ export default class Button extends Component {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
PropTypes.element,
]),
}
render () {

@ -0,0 +1,144 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Loading from '../../../loading-screen'
import GasPriceChart from '../../gas-price-chart'
import debounce from 'lodash.debounce'
export default class AdvancedTabContent extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
gasEstimatesLoading: PropTypes.bool,
millisecondsRemaining: PropTypes.number,
totalFee: PropTypes.string,
timeRemaining: PropTypes.string,
gasChartProps: PropTypes.object,
insufficientBalance: PropTypes.bool,
}
constructor (props) {
super(props)
this.debouncedGasLimitReset = debounce((dVal) => {
if (dVal < 21000) {
props.updateCustomGasLimit(21000)
}
}, 1000, { trailing: true })
this.onChangeGasLimit = (val) => {
props.updateCustomGasLimit(val)
this.debouncedGasLimitReset(val)
}
}
gasInput (value, onChange, min, insufficientBalance, showGWEI) {
return (
<div className="advanced-tab__gas-edit-row__input-wrapper">
<input
className={classnames('advanced-tab__gas-edit-row__input', {
'advanced-tab__gas-edit-row__input--error': insufficientBalance,
})}
type="number"
value={value}
min={min}
onChange={event => onChange(Number(event.target.value))}
/>
<div className={classnames('advanced-tab__gas-edit-row__input-arrows', {
'advanced-tab__gas-edit-row__input-arrows--error': insufficientBalance,
})}>
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value + 1)}><i className="fa fa-sm fa-angle-up" /></div>
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value - 1)}><i className="fa fa-sm fa-angle-down" /></div>
</div>
{insufficientBalance && <div className="advanced-tab__gas-edit-row__insufficient-balance">
Insufficient Balance
</div>}
</div>
)
}
infoButton (onClick) {
return <i className="fa fa-info-circle" onClick={onClick} />
}
renderDataSummary (totalFee, timeRemaining) {
return (
<div className="advanced-tab__transaction-data-summary">
<div className="advanced-tab__transaction-data-summary__titles">
<span>{ this.context.t('newTransactionFee') }</span>
<span>~{ this.context.t('transactionTime') }</span>
</div>
<div className="advanced-tab__transaction-data-summary__container">
<div className="advanced-tab__transaction-data-summary__fee">
{totalFee}
</div>
<div className="time-remaining">{timeRemaining}</div>
</div>
</div>
)
}
renderGasEditRow (labelKey, ...gasInputArgs) {
return (
<div className="advanced-tab__gas-edit-row">
<div className="advanced-tab__gas-edit-row__label">
{ this.context.t(labelKey) }
{ this.infoButton(() => {}) }
</div>
{ this.gasInput(...gasInputArgs) }
</div>
)
}
renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) {
return (
<div className="advanced-tab__gas-edit-rows">
{ this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, true) }
{ this.renderGasEditRow('gasLimit', customGasLimit, this.onChangeGasLimit, customGasLimit, insufficientBalance) }
</div>
)
}
render () {
const {
updateCustomGasPrice,
updateCustomGasLimit,
timeRemaining,
customGasPrice,
customGasLimit,
insufficientBalance,
totalFee,
gasChartProps,
gasEstimatesLoading,
} = this.props
return (
<div className="advanced-tab">
{ this.renderDataSummary(totalFee, timeRemaining) }
<div className="advanced-tab__fee-chart">
{ this.renderGasEditRows(
customGasPrice,
updateCustomGasPrice,
customGasLimit,
updateCustomGasLimit,
insufficientBalance
) }
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div>
{!gasEstimatesLoading
? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />
: <Loading />
}
<div className="advanced-tab__fee-chart__speed-buttons">
<span>Slower</span>
<span>Faster</span>
</div>
</div>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './advanced-tab-content.component'

@ -0,0 +1,191 @@
@import './time-remaining/index';
.advanced-tab {
display: flex;
flex-flow: column;
&__transaction-data-summary,
&__fee-chart-title {
padding-left: 24px;
padding-right: 24px;
}
&__transaction-data-summary {
display: flex;
flex-flow: column;
color: $mid-gray;
margin-top: 12px;
padding-left: 18px;
padding-right: 18px;
&__titles,
&__container {
display: flex;
flex-flow: row;
justify-content: space-between;
font-size: 12px;
color: #888EA3;
}
&__container {
font-size: 16px;
margin-top: 0px;
}
&__fee {
font-size: 16px;
color: #313A5E;
}
}
&__fee-chart {
margin-top: 8px;
height: 265px;
background: #F8F9FB;
border-bottom: 1px solid #d2d8dd;
border-top: 1px solid #d2d8dd;
position: relative;
&__title {
font-size: 12px;
color: #313A5E;
margin-left: 22px;
}
&__speed-buttons {
position: absolute;
bottom: 13px;
display: flex;
justify-content: space-between;
padding-left: 20px;
padding-right: 19px;
width: 100%;
font-size: 10px;
color: #888EA3;
}
}
&__slider-container {
padding-left: 27px;
padding-right: 27px;
}
&__gas-edit-rows {
height: 73px;
display: flex;
flex-flow: row;
justify-content: space-between;
margin-left: 20px;
margin-right: 10px;
margin-top: 9px;
}
&__gas-edit-row {
display: flex;
flex-flow: column;
&__label {
color: #313B5E;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
.fa-info-circle {
color: $silver;
margin-left: 10px;
cursor: pointer;
}
.fa-info-circle:hover {
color: $mid-gray;
}
}
&__insufficient-balance {
font-size: 12px;
color: red;
}
&__input-wrapper {
position: relative;
}
&__input {
border: 1px solid $dusty-gray;
border-radius: 4px;
color: $mid-gray;
font-size: 16px;
height: 24px;
width: 155px;
padding-left: 8px;
padding-top: 2px;
margin-top: 7px;
}
&__input--error {
border: 1px solid $red;
}
&__input-arrows {
position: absolute;
top: 7px;
right: 0px;
width: 17px;
height: 24px;
border: 1px solid #dadada;
border-top-right-radius: 4px;
display: flex;
flex-direction: column;
color: #9b9b9b;
font-size: .8em;
border-bottom-right-radius: 4px;
cursor: pointer;
&__i-wrap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
cursor: pointer;
}
&__i-wrap:hover {
background: #4EADE7;
color: $white;
}
i:hover {
background: #4EADE7;
}
i {
font-size: 10px;
}
}
&__input-arrows--error {
border: 1px solid $red;
}
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
&__gwei-symbol {
position: absolute;
top: 8px;
right: 10px;
color: $dusty-gray;
}
}
}

@ -0,0 +1,273 @@
import React from 'react'
import assert from 'assert'
import shallow from '../../../../../../lib/shallow-with-context'
import sinon from 'sinon'
import AdvancedTabContent from '../advanced-tab-content.component.js'
import GasPriceChart from '../../../gas-price-chart'
import Loading from '../../../../loading-screen'
const propsMethodSpies = {
updateCustomGasPrice: sinon.spy(),
updateCustomGasLimit: sinon.spy(),
}
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow')
sinon.spy(AdvancedTabContent.prototype, 'gasInput')
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows')
sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary')
describe('AdvancedTabContent Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<AdvancedTabContent
updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice}
updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit}
customGasPrice={11}
customGasLimit={23456}
timeRemaining={21500}
totalFee={'$0.25'}
insufficientBalance={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
})
afterEach(() => {
propsMethodSpies.updateCustomGasPrice.resetHistory()
propsMethodSpies.updateCustomGasLimit.resetHistory()
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
AdvancedTabContent.prototype.gasInput.resetHistory()
AdvancedTabContent.prototype.renderGasEditRows.resetHistory()
AdvancedTabContent.prototype.renderDataSummary.resetHistory()
})
describe('render()', () => {
it('should render the advanced-tab root node', () => {
assert(wrapper.hasClass('advanced-tab'))
})
it('should render the expected four children of the advanced-tab div', () => {
const advancedTabChildren = wrapper.children()
assert.equal(advancedTabChildren.length, 2)
assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
const feeChartDiv = advancedTabChildren.at(1)
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title'))
assert(feeChartDiv.childAt(2).is(GasPriceChart))
assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons'))
})
it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => {
wrapper.setProps({ gasEstimatesLoading: true })
const advancedTabChildren = wrapper.children()
assert.equal(advancedTabChildren.length, 2)
assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary'))
assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart'))
const feeChartDiv = advancedTabChildren.at(1)
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title'))
assert(feeChartDiv.childAt(2).is(Loading))
assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons'))
})
it('should call renderDataSummary with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args
assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500])
})
it('should call renderGasEditRows with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
assert.deepEqual(renderGasEditRowArgs, [
11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, false,
])
})
})
describe('renderDataSummary()', () => {
let dataSummary
beforeEach(() => {
dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining'))
})
it('should render the transaction-data-summary root node', () => {
assert(dataSummary.hasClass('advanced-tab__transaction-data-summary'))
})
it('should render titles of the data', () => {
const titlesNode = dataSummary.children().at(0)
assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles'))
assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee')
assert.equal(titlesNode.children().at(1).text(), '~transactionTime')
})
it('should render the data', () => {
const dataNode = dataSummary.children().at(1)
assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container'))
assert.equal(dataNode.children().at(0).text(), 'mockTotalFee')
assert(dataNode.children().at(1).hasClass('time-remaining'))
assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining')
})
})
describe('renderGasEditRow()', () => {
let gasEditRow
beforeEach(() => {
AdvancedTabContent.prototype.gasInput.resetHistory()
gasEditRow = shallow(wrapper.instance().renderGasEditRow(
'mockLabelKey', 'argA', 'argB'
))
})
it('should render the gas-edit-row root node', () => {
assert(gasEditRow.hasClass('advanced-tab__gas-edit-row'))
})
it('should render a label and an input', () => {
const gasEditRowChildren = gasEditRow.children()
assert.equal(gasEditRowChildren.length, 2)
assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label'))
assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper'))
})
it('should render the label key and info button', () => {
const gasRowLabelChildren = gasEditRow.children().at(0).children()
assert.equal(gasRowLabelChildren.length, 2)
assert(gasRowLabelChildren.at(0), 'mockLabelKey')
assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle'))
})
it('should call this.gasInput with the correct args', () => {
const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args
assert.deepEqual(gasInputSpyArgs[0], [ 'argA', 'argB' ])
})
})
describe('renderGasEditRows()', () => {
let gasEditRows
let tempOnChangeGasLimit
beforeEach(() => {
tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit
wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit'
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
gasEditRows = shallow(wrapper.instance().renderGasEditRows(
'mockGasPrice',
() => 'mockUpdateCustomGasPriceReturn',
'mockGasLimit',
() => 'mockUpdateCustomGasLimitReturn',
false
))
})
afterEach(() => {
wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit
})
it('should render the gas-edit-rows root node', () => {
assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows'))
})
it('should render two rows', () => {
const gasEditRowsChildren = gasEditRows.children()
assert.equal(gasEditRowsChildren.length, 2)
assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row'))
assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row'))
})
it('should call this.renderGasEditRow twice, with the expected args', () => {
const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
assert.equal(renderGasEditRowSpyArgs.length, 2)
assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [
'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, true,
].map(String))
assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [
'gasLimit', 'mockGasLimit', () => 'mockOnChangeGasLimit', 'mockGasLimit', false,
].map(String))
})
})
describe('infoButton()', () => {
let infoButton
beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn'))
})
it('should render the i element', () => {
assert(infoButton.hasClass('fa-info-circle'))
})
it('should pass the onClick argument to the i tag onClick prop', () => {
assert(infoButton.props().onClick(), 'mockOnClickReturn')
})
})
describe('gasInput()', () => {
let gasInput
beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
gasInput = shallow(wrapper.instance().gasInput(
321,
value => value + 7,
0,
false,
8
))
})
it('should render the input-wrapper root node', () => {
assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper'))
})
it('should render two children, including an input', () => {
assert.equal(gasInput.children().length, 2)
assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input'))
})
it('should pass the correct value min and precision props to the input', () => {
const inputProps = gasInput.find('input').props()
assert.equal(inputProps.min, 0)
assert.equal(inputProps.value, 321)
})
it('should call the passed onChange method with the value of the input onChange event', () => {
const inputOnChange = gasInput.find('input').props().onChange
assert.equal(inputOnChange({ target: { value: 8} }), 15)
})
it('should have two input arrows', () => {
const upArrow = gasInput.find('.fa-angle-up')
assert.equal(upArrow.length, 1)
const downArrow = gasInput.find('.fa-angle-down')
assert.equal(downArrow.length, 1)
})
it('should call onChange with the value incremented decremented when its onchange method is called', () => {
gasInput = shallow(wrapper.instance().gasInput(
321,
value => value + 7,
0,
8,
false
))
const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0)
assert.equal(upArrow.props().onClick(), 329)
const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1)
assert.equal(downArrow.props().onClick(), 327)
})
})
})

@ -0,0 +1,17 @@
.time-remaining {
color: #313A5E;
font-size: 16px;
.minutes-num, .seconds-num {
font-size: 16px;
}
.seconds-num {
margin-left: 7px;
font-size: 16px;
}
.minutes-label, .seconds-label {
font-size: 16px;
}
}

@ -0,0 +1,30 @@
import React from 'react'
import assert from 'assert'
import shallow from '../../../../../../../lib/shallow-with-context'
import TimeRemaining from '../time-remaining.component.js'
describe('TimeRemaining Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<TimeRemaining
milliseconds={495000}
/>)
})
describe('render()', () => {
it('should render the time-remaining root node', () => {
assert(wrapper.hasClass('time-remaining'))
})
it('should render minutes and seconds numbers and labels', () => {
const timeRemainingChildren = wrapper.children()
assert.equal(timeRemainingChildren.length, 4)
assert.equal(timeRemainingChildren.at(0).text(), 8)
assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand')
assert.equal(timeRemainingChildren.at(2).text(), 15)
assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand')
})
})
})

@ -0,0 +1,33 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { getTimeBreakdown } from './time-remaining.utils'
export default class TimeRemaining extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
milliseconds: PropTypes.number,
}
render () {
const {
milliseconds,
} = this.props
const {
minutes,
seconds,
} = getTimeBreakdown(milliseconds)
return (
<div className="time-remaining">
<span className="minutes-num">{minutes}</span>
<span className="minutes-label">{this.context.t('minutesShorthand')}</span>
<span className="seconds-num">{seconds}</span>
<span className="seconds-label">{this.context.t('secondsShorthand')}</span>
</div>
)
}
}

@ -0,0 +1,11 @@
function getTimeBreakdown (milliseconds) {
return {
hours: Math.floor(milliseconds / 3600000),
minutes: Math.floor((milliseconds % 3600000) / 60000),
seconds: Math.floor((milliseconds % 60000) / 1000),
}
}
module.exports = {
getTimeBreakdown,
}

@ -0,0 +1,34 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Loading from '../../../loading-screen'
import GasPriceButtonGroup from '../../gas-price-button-group'
export default class BasicTabContent extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
gasPriceButtonGroupProps: PropTypes.object,
}
render () {
const { gasPriceButtonGroupProps } = this.props
return (
<div className="basic-tab-content">
<div className="basic-tab-content__title">Estimated Processing Times</div>
<div className="basic-tab-content__blurb">Select a higher gas fee to accelerate the processing of your transaction.*</div>
{!gasPriceButtonGroupProps.loading
? <GasPriceButtonGroup
className="gas-price-button-group--alt"
showCheck={true}
{...gasPriceButtonGroupProps}
/>
: <Loading />
}
<div className="basic-tab-content__footer-blurb">* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed.</div>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './basic-tab-content.component'

@ -0,0 +1,28 @@
.basic-tab-content {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-left: 21px;
height: 324px;
background: #F5F7F8;
border-bottom: 1px solid #d2d8dd;
&__title {
margin-top: 19px;
font-size: 16px;
color: $black;
}
&__blurb {
font-size: 12px;
color: $black;
margin-top: 5px;
margin-bottom: 15px;
}
&__footer-blurb {
font-size: 12px;
color: #979797;
margin-top: 15px;
}
}

@ -0,0 +1,82 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import BasicTabContent from '../basic-tab-content.component'
import GasPriceButtonGroup from '../../../gas-price-button-group/'
import Loading from '../../../../loading-screen'
const mockGasPriceButtonGroupProps = {
buttonDataLoading: false,
className: 'gas-price-button-group',
gasButtonInfo: [
{
feeInPrimaryCurrency: '$0.52',
feeInSecondaryCurrency: '0.0048 ETH',
timeEstimate: '~ 1 min 0 sec',
priceInHexWei: '0xa1b2c3f',
},
{
feeInPrimaryCurrency: '$0.39',
feeInSecondaryCurrency: '0.004 ETH',
timeEstimate: '~ 1 min 30 sec',
priceInHexWei: '0xa1b2c39',
},
{
feeInPrimaryCurrency: '$0.30',
feeInSecondaryCurrency: '0.00354 ETH',
timeEstimate: '~ 2 min 1 sec',
priceInHexWei: '0xa1b2c30',
},
],
handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice),
noButtonActiveByDefault: true,
showCheck: true,
}
describe('BasicTabContent Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<BasicTabContent
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
/>)
})
describe('render', () => {
it('should have a title', () => {
assert(wrapper.find('.basic-tab-content').childAt(0).hasClass('basic-tab-content__title'))
})
it('should render a GasPriceButtonGroup compenent', () => {
assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
})
it('should pass correct props to GasPriceButtonGroup', () => {
const {
buttonDataLoading,
className,
gasButtonInfo,
handleGasPriceSelection,
noButtonActiveByDefault,
showCheck,
} = wrapper.find(GasPriceButtonGroup).props()
assert.equal(wrapper.find(GasPriceButtonGroup).length, 1)
assert.equal(buttonDataLoading, mockGasPriceButtonGroupProps.buttonDataLoading)
assert.equal(className, mockGasPriceButtonGroupProps.className)
assert.equal(noButtonActiveByDefault, mockGasPriceButtonGroupProps.noButtonActiveByDefault)
assert.equal(showCheck, mockGasPriceButtonGroupProps.showCheck)
assert.deepEqual(gasButtonInfo, mockGasPriceButtonGroupProps.gasButtonInfo)
assert.equal(JSON.stringify(handleGasPriceSelection), JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection))
})
it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => {
wrapper.setProps({
gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, loading: true },
})
assert.equal(wrapper.find(GasPriceButtonGroup).length, 0)
assert.equal(wrapper.find(Loading).length, 1)
})
})
})

@ -0,0 +1,178 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainer from '../../page-container'
import { Tabs, Tab } from '../../tabs'
import AdvancedTabContent from './advanced-tab-content'
import BasicTabContent from './basic-tab-content'
export default class GasModalPageContainer extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
hideModal: PropTypes.func,
hideBasic: PropTypes.bool,
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object,
infoRowProps: PropTypes.shape({
originalTotalFiat: PropTypes.string,
originalTotalEth: PropTypes.string,
newTotalFiat: PropTypes.string,
newTotalEth: PropTypes.string,
}),
onSubmit: PropTypes.func,
customModalGasPriceInHex: PropTypes.string,
customModalGasLimitInHex: PropTypes.string,
cancelAndClose: PropTypes.func,
transactionFee: PropTypes.string,
blockTime: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
}
state = {}
componentDidMount () {
const promise = this.props.hideBasic
? Promise.resolve(this.props.blockTime)
: this.props.fetchBasicGasAndTimeEstimates()
.then(basicEstimates => basicEstimates.blockTime)
promise
.then(blockTime => {
this.props.fetchGasEstimates(blockTime)
})
}
renderBasicTabContent (gasPriceButtonGroupProps) {
return (
<BasicTabContent
gasPriceButtonGroupProps={gasPriceButtonGroupProps}
/>
)
}
renderAdvancedTabContent ({
convertThenUpdateCustomGasPrice,
convertThenUpdateCustomGasLimit,
customGasPrice,
customGasLimit,
newTotalFiat,
gasChartProps,
currentTimeEstimate,
insufficientBalance,
gasEstimatesLoading,
}) {
const { transactionFee } = this.props
return (
<AdvancedTabContent
updateCustomGasPrice={convertThenUpdateCustomGasPrice}
updateCustomGasLimit={convertThenUpdateCustomGasLimit}
customGasPrice={customGasPrice}
customGasLimit={customGasLimit}
timeRemaining={currentTimeEstimate}
transactionFee={transactionFee}
totalFee={newTotalFiat}
gasChartProps={gasChartProps}
insufficientBalance={insufficientBalance}
gasEstimatesLoading={gasEstimatesLoading}
/>
)
}
renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) {
return (
<div className="gas-modal-content__info-row-wrapper">
<div className="gas-modal-content__info-row">
<div className="gas-modal-content__info-row__send-info">
<span className="gas-modal-content__info-row__send-info__label">{this.context.t('sendAmount')}</span>
<span className="gas-modal-content__info-row__send-info__value">{sendAmount}</span>
</div>
<div className="gas-modal-content__info-row__transaction-info">
<span className={'gas-modal-content__info-row__transaction-info__label'}>{this.context.t('transactionFee')}</span>
<span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span>
</div>
<div className="gas-modal-content__info-row__total-info">
<span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span>
<span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span>
</div>
<div className="gas-modal-content__info-row__fiat-total-info">
<span className="gas-modal-content__info-row__fiat-total-info__value">{newTotalFiat}</span>
</div>
</div>
</div>
)
}
renderTabs ({
originalTotalFiat,
originalTotalEth,
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
},
{
gasPriceButtonGroupProps,
hideBasic,
...advancedTabProps
}) {
let tabsToRender = [
{ name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) },
{ name: 'advanced', content: this.renderAdvancedTabContent(advancedTabProps) },
]
if (hideBasic) {
tabsToRender = tabsToRender.slice(1)
}
return (
<Tabs>
{tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}>
<div className="gas-modal-content">
{ content }
{ this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
</div>
</Tab>
)}
</Tabs>
)
}
render () {
const {
cancelAndClose,
infoRowProps,
onSubmit,
customModalGasPriceInHex,
customModalGasLimitInHex,
...tabProps
} = this.props
return (
<div className="gas-modal-page-container">
<PageContainer
title={this.context.t('customGas')}
subtitle={this.context.t('customGasSubTitle')}
tabsComponent={this.renderTabs(infoRowProps, tabProps)}
disabled={tabProps.insufficientBalance}
onCancel={() => cancelAndClose()}
onClose={() => cancelAndClose()}
onSubmit={() => {
onSubmit(customModalGasLimitInHex, customModalGasPriceInHex)
}}
submitText={this.context.t('save')}
headerCloseText={'Close'}
hideCancel={true}
/>
</div>
)
}
}

@ -0,0 +1,283 @@
import { connect } from 'react-redux'
import { pipe, partialRight } from 'ramda'
import GasModalPageContainer from './gas-modal-page-container.component'
import {
hideModal,
setGasLimit,
setGasPrice,
createSpeedUpTransaction,
hideSidebar,
} from '../../../actions'
import {
setCustomGasPrice,
setCustomGasLimit,
resetCustomData,
setCustomTimeEstimate,
fetchGasEstimates,
fetchBasicGasAndTimeEstimates,
} from '../../../ducks/gas.duck'
import {
hideGasButtonGroup,
} from '../../../ducks/send.duck'
import {
updateGasAndCalculate,
} from '../../../ducks/confirm-transaction.duck'
import {
getCurrentCurrency,
conversionRateSelector as getConversionRate,
getSelectedToken,
getCurrentEthBalance,
} from '../../../selectors.js'
import {
formatTimeEstimate,
getFastPriceEstimateInHexWEI,
getBasicGasEstimateLoadingStatus,
getGasEstimatesLoadingStatus,
getCustomGasLimit,
getCustomGasPrice,
getDefaultActiveButtonIndex,
getEstimatedGasPrices,
getEstimatedGasTimes,
getRenderableBasicEstimateData,
getBasicGasEstimateBlockTime,
} from '../../../selectors/custom-gas'
import {
submittedPendingTransactionsSelector,
} from '../../../selectors/transactions'
import {
formatCurrency,
} from '../../../helpers/confirm-transaction/util'
import {
addHexWEIsToDec,
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
decGWEIToHexWEI,
hexWEIToDecGWEI,
} from '../../../helpers/conversions.util'
import {
formatETHFee,
} from '../../../helpers/formatters'
import {
calcGasTotal,
isBalanceSufficient,
} from '../../send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
const mapStateToProps = (state, ownProps) => {
const { transaction = {} } = ownProps
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id)
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const gasButtonInfo = getRenderableBasicEstimateData(state)
const currentCurrency = getCurrentCurrency(state)
const conversionRate = getConversionRate(state)
const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate)
const hideBasic = state.appState.modal.modalState.props.hideBasic
const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex)
const gasPrices = getEstimatedGasPrices(state)
const estimatedTimes = getEstimatedGasTimes(state)
const balance = getCurrentEthBalance(state)
const insufficientBalance = !isBalanceSufficient({
amount: value,
gasTotal,
balance,
conversionRate,
})
return {
hideBasic,
isConfirm: isConfirm(state),
customModalGasPriceInHex,
customModalGasLimitInHex,
customGasPrice,
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
newTotalFiat,
currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes),
blockTime: getBasicGasEstimateBlockTime(state),
gasPriceButtonGroupProps: {
buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
gasButtonInfo,
},
gasChartProps: {
currentPrice: customGasPrice,
gasPrices,
estimatedTimes,
gasPricesMax: gasPrices[gasPrices.length - 1],
estimatedTimesMax: estimatedTimes[0],
},
infoRowProps: {
originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate),
originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal),
newTotalFiat,
newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
sendAmount: addHexWEIsToRenderableEth(value, '0x0'),
},
isSpeedUp: transaction.status === 'submitted',
txId: transaction.id,
insufficientBalance,
gasEstimatesLoading,
}
}
const mapDispatchToProps = dispatch => {
const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice)))
return {
cancelAndClose: () => {
dispatch(resetCustomData())
dispatch(hideModal())
},
hideModal: () => dispatch(hideModal()),
updateCustomGasPrice,
convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)),
convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))),
setGasData: (newLimit, newPrice) => {
dispatch(setGasLimit(newLimit))
dispatch(setGasPrice(newPrice))
},
updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => {
updateCustomGasPrice(gasPrice)
dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16))))
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
},
createSpeedUpTransaction: (txId, gasPrice) => {
return dispatch(createSpeedUpTransaction(txId, gasPrice))
},
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
hideSidebar: () => dispatch(hideSidebar()),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { gasPriceButtonGroupProps, isConfirm, isSpeedUp, txId } = stateProps
const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup,
setGasData: dispatchSetGasData,
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
hideSidebar: dispatchHideSidebar,
cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal,
...otherDispatchProps
} = dispatchProps
return {
...stateProps,
...otherDispatchProps,
...ownProps,
onSubmit: (gasLimit, gasPrice) => {
if (isConfirm) {
dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice)
dispatchHideModal()
} else if (isSpeedUp) {
dispatchCreateSpeedUpTransaction(txId, gasPrice)
dispatchHideSidebar()
dispatchCancelAndClose()
} else {
dispatchSetGasData(gasLimit, gasPrice)
dispatchHideGasButtonGroup()
dispatchCancelAndClose()
}
},
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
handleGasPriceSelection: dispatchUpdateCustomGasPrice,
},
cancelAndClose: () => {
dispatchCancelAndClose()
if (isSpeedUp) {
dispatchHideSidebar()
}
},
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer)
function isConfirm (state) {
return Boolean(Object.keys(state.confirmTransaction.txData).length)
}
function calcCustomGasPrice (customGasPriceInHex) {
return Number(hexWEIToDecGWEI(customGasPriceInHex))
}
function calcCustomGasLimit (customGasLimitInHex) {
return parseInt(customGasLimitInHex, 16)
}
function getTxParams (state, transactionId) {
const { confirmTransaction: { txData }, metamask: { send } } = state
const pendingTransactions = submittedPendingTransactionsSelector(state)
const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId)
const { txParams: pendingTxParams } = pendingTransaction || {}
return txData.txParams || pendingTxParams || {
from: send.from,
gas: send.gasLimit,
gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true),
to: send.to,
value: getSelectedToken(state) ? '0x0' : send.amount,
}
}
function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
return pipe(
addHexWEIsToDec,
formatETHFee
)(aHexWEI, bHexWEI)
}
function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) {
return pipe(
addHexWEIsToDec,
partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]),
partialRight(formatCurrency, [convertedCurrency]),
)(aHexWEI, bHexWEI)
}
function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) {
const minGasPrice = gasPrices[0]
const maxGasPrice = gasPrices[gasPrices.length - 1]
let priceForEstimation = currentGasPrice
if (currentGasPrice < minGasPrice) {
priceForEstimation = minGasPrice
} else if (currentGasPrice > maxGasPrice) {
priceForEstimation = maxGasPrice
}
const {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue,
closestLowerValue,
} = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation })
const newTimeEstimate = extrapolateY({
higherY: estimatedTimes[closestHigherValueIndex],
lowerY: estimatedTimes[closestLowerValueIndex],
higherX: closestHigherValue,
lowerX: closestLowerValue,
xForExtrapolation: priceForEstimation,
})
return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice)
}

@ -0,0 +1 @@
export { default } from './gas-modal-page-container.container'

@ -0,0 +1,148 @@
@import './advanced-tab-content/index';
@import './basic-tab-content/index';
.gas-modal-page-container {
.page-container {
max-width: 391px;
min-height: 585px;
overflow-y: initial;
@media screen and (max-width: $break-small) {
max-width: 344px;
&__content {
display: flex;
overflow-y: initial;
}
}
&__header {
padding: 0px;
padding-top: 16px;
&--no-padding-bottom {
padding-bottom: 0;
}
}
&__footer {
header {
padding-top: 12px;
padding-bottom: 12px;
}
}
&__header-close-text {
font-size: 14px;
color: #4EADE7;
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
overflow: hidden;
}
&__title {
color: $black;
font-size: 16px;
font-weight: 500;
line-height: 16px;
display: flex;
justify-content: center;
align-items: flex-start;
margin-right: 0;
}
&__subtitle {
display: none;
}
&__tabs {
margin-top: 0px;
}
&__tab {
width: 100%;
font-size: 14px;
&:last-of-type {
margin-right: 0;
}
&--selected {
color: $curious-blue;
border-bottom: 2px solid $curious-blue;
}
}
}
}
.gas-modal-content {
@media screen and (max-width: $break-small) {
width: 100%;
}
&__basic-tab {
height: 219px;
}
&__info-row, &__info-row--fade {
width: 100%;
background: $polar;
padding: 15px 21px;
display: flex;
flex-flow: column;
color: $scorpion;
font-size: 12px;
@media screen and (max-width: $break-small) {
padding: 4px 21px;
}
&__send-info, &__transaction-info, &__total-info, &__fiat-total-info {
display: flex;
flex-flow: row;
justify-content: space-between;
}
&__fiat-total-info {
justify-content: flex-end;
}
&__total-info {
&__label {
font-size: 16px;
@media screen and (max-width: $break-small) {
font-size: 14px;
}
}
&__value {
font-size: 16px;
font-weight: bold;
@media screen and (max-width: $break-small) {
font-size: 14px;
}
}
}
&__transaction-info, &__send-info {
&__label {
font-size: 12px;
}
&__value {
font-size: 14px;
}
}
}
&__info-row--fade {
background: white;
color: $dusty-gray;
border-top: 1px solid $mischka;
}
}

@ -0,0 +1,273 @@
import React from 'react'
import assert from 'assert'
import shallow from '../../../../../lib/shallow-with-context'
import sinon from 'sinon'
import GasModalPageContainer from '../gas-modal-page-container.component.js'
import timeout from '../../../../../lib/test-timeout'
import PageContainer from '../../../page-container'
import { Tab } from '../../../tabs'
const mockBasicGasEstimates = {
blockTime: 'mockBlockTime',
}
const propsMethodSpies = {
cancelAndClose: sinon.spy(),
onSubmit: sinon.spy(),
fetchBasicGasAndTimeEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
fetchGasEstimates: sinon.spy(),
}
const mockGasPriceButtonGroupProps = {
buttonDataLoading: false,
className: 'gas-price-button-group',
gasButtonInfo: [
{
feeInPrimaryCurrency: '$0.52',
feeInSecondaryCurrency: '0.0048 ETH',
timeEstimate: '~ 1 min 0 sec',
priceInHexWei: '0xa1b2c3f',
},
{
feeInPrimaryCurrency: '$0.39',
feeInSecondaryCurrency: '0.004 ETH',
timeEstimate: '~ 1 min 30 sec',
priceInHexWei: '0xa1b2c39',
},
{
feeInPrimaryCurrency: '$0.30',
feeInSecondaryCurrency: '0.00354 ETH',
timeEstimate: '~ 2 min 1 sec',
priceInHexWei: '0xa1b2c30',
},
],
handleGasPriceSelection: 'mockSelectionFunction',
noButtonActiveByDefault: true,
showCheck: true,
newTotalFiat: 'mockNewTotalFiat',
newTotalEth: 'mockNewTotalEth',
}
const mockInfoRowProps = {
originalTotalFiat: 'mockOriginalTotalFiat',
originalTotalEth: 'mockOriginalTotalEth',
newTotalFiat: 'mockNewTotalFiat',
newTotalEth: 'mockNewTotalEth',
sendAmount: 'mockSendAmount',
transactionFee: 'mockTransactionFee',
}
const GP = GasModalPageContainer.prototype
describe('GasModalPageContainer Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasModalPageContainer
cancelAndClose={propsMethodSpies.cancelAndClose}
onSubmit={propsMethodSpies.onSubmit}
fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
customGasPrice={21}
customGasLimit={54321}
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
infoRowProps={mockInfoRowProps}
currentTimeEstimate={'1 min 31 sec'}
customGasPriceInHex={'mockCustomGasPriceInHex'}
customGasLimitInHex={'mockCustomGasLimitInHex'}
insufficientBalance={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
})
afterEach(() => {
propsMethodSpies.cancelAndClose.resetHistory()
})
describe('componentDidMount', () => {
it('should call props.fetchBasicGasAndTimeEstimates', () => {
propsMethodSpies.fetchBasicGasAndTimeEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 0)
wrapper.instance().componentDidMount()
assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 1)
})
it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasAndTimeEstimates', async () => {
propsMethodSpies.fetchGasEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0)
wrapper.instance().componentDidMount()
await timeout(250)
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1)
assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime')
})
})
describe('render', () => {
it('should render a PageContainer compenent', () => {
assert.equal(wrapper.find(PageContainer).length, 1)
})
it('should pass correct props to PageContainer', () => {
const {
title,
subtitle,
disabled,
} = wrapper.find(PageContainer).props()
assert.equal(title, 'customGas')
assert.equal(subtitle, 'customGasSubTitle')
assert.equal(disabled, false)
})
it('should pass the correct onCancel and onClose methods to PageContainer', () => {
const {
onCancel,
onClose,
} = wrapper.find(PageContainer).props()
assert.equal(propsMethodSpies.cancelAndClose.callCount, 0)
onCancel()
assert.equal(propsMethodSpies.cancelAndClose.callCount, 1)
onClose()
assert.equal(propsMethodSpies.cancelAndClose.callCount, 2)
})
it('should pass the correct renderTabs property to PageContainer', () => {
sinon.stub(GP, 'renderTabs').returns('mockTabs')
const renderTabsWrapperTester = shallow(<GasModalPageContainer
fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props()
assert.equal(tabsComponent, 'mockTabs')
GasModalPageContainer.prototype.renderTabs.restore()
})
})
describe('renderTabs', () => {
beforeEach(() => {
sinon.spy(GP, 'renderBasicTabContent')
sinon.spy(GP, 'renderAdvancedTabContent')
sinon.spy(GP, 'renderInfoRows')
})
afterEach(() => {
GP.renderBasicTabContent.restore()
GP.renderAdvancedTabContent.restore()
GP.renderInfoRows.restore()
})
it('should render a Tabs component with "Basic" and "Advanced" tabs', () => {
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
otherProps: 'mockAdvancedTabProps',
})
const renderedTabs = shallow(renderTabsResult)
assert.equal(renderedTabs.props().className, 'tabs')
const tabs = renderedTabs.find(Tab)
assert.equal(tabs.length, 2)
assert.equal(tabs.at(0).props().name, 'basic')
assert.equal(tabs.at(1).props().name, 'advanced')
assert.equal(tabs.at(0).childAt(0).props().className, 'gas-modal-content')
assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content')
})
it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => {
assert.equal(GP.renderBasicTabContent.callCount, 0)
assert.equal(GP.renderAdvancedTabContent.callCount, 0)
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
assert.equal(GP.renderBasicTabContent.callCount, 1)
assert.equal(GP.renderAdvancedTabContent.callCount, 1)
assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps)
assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { otherProps: 'mockAdvancedTabProps' })
})
it('should call renderInfoRows with the expected props', () => {
assert.equal(GP.renderInfoRows.callCount, 0)
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
assert.equal(GP.renderInfoRows.callCount, 2)
assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
})
it('should not render the basic tab if hideBasic is true', () => {
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
otherProps: 'mockAdvancedTabProps',
hideBasic: true,
})
const renderedTabs = shallow(renderTabsResult)
const tabs = renderedTabs.find(Tab)
assert.equal(tabs.length, 1)
assert.equal(tabs.at(0).props().name, 'advanced')
})
})
describe('renderBasicTabContent', () => {
it('should render', () => {
const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps)
assert.deepEqual(
renderBasicTabContentResult.props.gasPriceButtonGroupProps,
mockGasPriceButtonGroupProps
)
})
})
describe('renderAdvancedTabContent', () => {
it('should render with the correct props', () => {
const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({
convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice',
convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit',
customGasPrice: 123,
customGasLimit: 456,
newTotalFiat: '$0.30',
currentTimeEstimate: '1 min 31 sec',
gasEstimatesLoading: 'mockGasEstimatesLoading',
})
const advancedTabContentProps = renderAdvancedTabContentResult.props
assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice')
assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit')
assert.equal(advancedTabContentProps.customGasPrice, 123)
assert.equal(advancedTabContentProps.customGasLimit, 456)
assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec')
assert.equal(advancedTabContentProps.totalFee, '$0.30')
assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading')
})
})
describe('renderInfoRows', () => {
it('should render the info rows with the passed data', () => {
const baseClassName = 'gas-modal-content__info-row'
const renderedInfoRowsContainer = shallow(wrapper.instance().renderInfoRows(
'mockNewTotalFiat',
' mockNewTotalEth',
' mockSendAmount',
' mockTransactionFee'
))
assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName))
const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children()
assert.equal(renderedInfoRows.length, 4)
assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`))
assert(renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`))
assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`))
assert(renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`))
assert.equal(renderedInfoRows.at(0).text(), 'sendAmount mockSendAmount')
assert.equal(renderedInfoRows.at(1).text(), 'transactionFee mockTransactionFee')
assert.equal(renderedInfoRows.at(2).text(), 'newTotal mockNewTotalEth')
assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat')
})
})
})

@ -0,0 +1,360 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
let mapStateToProps
let mapDispatchToProps
let mergeProps
const actionSpies = {
hideModal: sinon.spy(),
setGasLimit: sinon.spy(),
setGasPrice: sinon.spy(),
}
const gasActionSpies = {
setCustomGasPrice: sinon.spy(),
setCustomGasLimit: sinon.spy(),
resetCustomData: sinon.spy(),
}
const confirmTransactionActionSpies = {
updateGasAndCalculate: sinon.spy(),
}
const sendActionSpies = {
hideGasButtonGroup: sinon.spy(),
}
proxyquire('../gas-modal-page-container.container.js', {
'react-redux': {
connect: (ms, md, mp) => {
mapStateToProps = ms
mapDispatchToProps = md
mergeProps = mp
return () => ({})
},
},
'../../../selectors/custom-gas': {
getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`,
getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`,
getDefaultActiveButtonIndex: (a, b) => a + b,
},
'../../../actions': actionSpies,
'../../../ducks/gas.duck': gasActionSpies,
'../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies,
'../../../ducks/send.duck': sendActionSpies,
'../../../selectors.js': {
getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
},
})
describe('gas-modal-page-container container', () => {
describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => {
const baseMockState = {
appState: {
modal: {
modalState: {
props: {
hideBasic: true,
},
},
},
},
metamask: {
send: {
gasLimit: '16',
gasPrice: '32',
amount: '64',
},
currentCurrency: 'abc',
conversionRate: 50,
},
gas: {
basicEstimates: {
blockTime: 12,
},
customData: {
limit: 'aaaaaaaa',
price: 'ffffffff',
},
gasEstimatesLoading: false,
priceAndTimeEstimates: [
{ gasprice: 3, expectedTime: 31 },
{ gasprice: 4, expectedTime: 62 },
{ gasprice: 5, expectedTime: 93 },
{ gasprice: 6, expectedTime: 124 },
],
},
confirmTransaction: {
txData: {
txParams: {
gas: '0x1600000',
gasPrice: '0x3200000',
value: '0x640000000000000',
},
},
},
}
const baseExpectedResult = {
isConfirm: true,
customGasPrice: 4.294967295,
customGasLimit: 2863311530,
currentTimeEstimate: '~1 min 11 sec',
newTotalFiat: '637.41',
blockTime: 12,
customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff',
gasChartProps: {
'currentPrice': 4.294967295,
estimatedTimes: ['31', '62', '93', '124'],
estimatedTimesMax: '31',
gasPrices: [3, 4, 5, 6],
gasPricesMax: 6,
},
gasPriceButtonGroupProps: {
buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4',
defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff',
gasButtonInfo: 'mockRenderableBasicEstimateData:4',
},
gasEstimatesLoading: false,
hideBasic: true,
infoRowProps: {
originalTotalFiat: '637.41',
originalTotalEth: '12.748189 ETH',
newTotalFiat: '637.41',
newTotalEth: '12.748189 ETH',
sendAmount: '0.45036 ETH',
transactionFee: '12.297829 ETH',
},
insufficientBalance: true,
isSpeedUp: false,
txId: 34,
}
const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [
{ mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps },
{
mockState: Object.assign({}, baseMockState, {
metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' },
}),
expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }),
mockOwnProps: baseMockOwnProps,
},
{
mockState: baseMockState,
mockOwnProps: Object.assign({}, baseMockOwnProps, {
transaction: { id: 34, status: 'submitted' },
}),
expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }),
},
]
let result
tests.forEach(({ mockState, mockOwnProps, expectedResult}) => {
result = mapStateToProps(mockState, mockOwnProps)
assert.deepEqual(result, expectedResult)
})
})
})
describe('mapDispatchToProps()', () => {
let dispatchSpy
let mapDispatchToPropsObject
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
})
afterEach(() => {
actionSpies.hideModal.resetHistory()
gasActionSpies.setCustomGasPrice.resetHistory()
gasActionSpies.setCustomGasLimit.resetHistory()
})
describe('hideGasButtonGroup()', () => {
it('should dispatch a hideGasButtonGroup action', () => {
mapDispatchToPropsObject.hideGasButtonGroup()
assert(dispatchSpy.calledOnce)
assert(sendActionSpies.hideGasButtonGroup.calledOnce)
})
})
describe('cancelAndClose()', () => {
it('should dispatch a hideModal action', () => {
mapDispatchToPropsObject.cancelAndClose()
assert(dispatchSpy.calledTwice)
assert(actionSpies.hideModal.calledOnce)
assert(gasActionSpies.resetCustomData.calledOnce)
})
})
describe('updateCustomGasPrice()', () => {
it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => {
mapDispatchToPropsObject.updateCustomGasPrice('ffff')
assert(dispatchSpy.calledOnce)
assert(gasActionSpies.setCustomGasPrice.calledOnce)
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff')
})
})
describe('convertThenUpdateCustomGasPrice()', () => {
it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => {
mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff')
assert(dispatchSpy.calledOnce)
assert(gasActionSpies.setCustomGasPrice.calledOnce)
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600')
})
})
describe('convertThenUpdateCustomGasLimit()', () => {
it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => {
mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16)
assert(dispatchSpy.calledOnce)
assert(gasActionSpies.setCustomGasLimit.calledOnce)
assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10')
})
})
describe('setGasData()', () => {
it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => {
mapDispatchToPropsObject.setGasData('ffff', 'aaaa')
assert(dispatchSpy.calledTwice)
assert(actionSpies.setGasPrice.calledOnce)
assert(actionSpies.setGasLimit.calledOnce)
assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff')
assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa')
})
})
describe('updateConfirmTxGasAndCalculate()', () => {
it('should dispatch a updateGasAndCalculate action with the correct props', () => {
mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa')
assert.equal(dispatchSpy.callCount, 3)
assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce)
assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' })
})
})
})
describe('mergeProps', () => {
let stateProps
let dispatchProps
let ownProps
beforeEach(() => {
stateProps = {
gasPriceButtonGroupProps: {
someGasPriceButtonGroupProp: 'foo',
anotherGasPriceButtonGroupProp: 'bar',
},
isConfirm: true,
someOtherStateProp: 'baz',
}
dispatchProps = {
updateCustomGasPrice: sinon.spy(),
hideGasButtonGroup: sinon.spy(),
setGasData: sinon.spy(),
updateConfirmTxGasAndCalculate: sinon.spy(),
someOtherDispatchProp: sinon.spy(),
createSpeedUpTransaction: sinon.spy(),
hideSidebar: sinon.spy(),
hideModal: sinon.spy(),
cancelAndClose: sinon.spy(),
}
ownProps = { someOwnProp: 123 }
})
afterEach(() => {
dispatchProps.updateCustomGasPrice.resetHistory()
dispatchProps.hideGasButtonGroup.resetHistory()
dispatchProps.setGasData.resetHistory()
dispatchProps.updateConfirmTxGasAndCalculate.resetHistory()
dispatchProps.someOtherDispatchProp.resetHistory()
dispatchProps.createSpeedUpTransaction.resetHistory()
dispatchProps.hideSidebar.resetHistory()
dispatchProps.hideModal.resetHistory()
})
it('should return the expected props when isConfirm is true', () => {
const result = mergeProps(stateProps, dispatchProps, ownProps)
assert.equal(result.isConfirm, true)
assert.equal(result.someOtherStateProp, 'baz')
assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
assert.equal(result.someOwnProp, 123)
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
assert.equal(dispatchProps.setGasData.callCount, 0)
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
assert.equal(dispatchProps.hideModal.callCount, 0)
result.onSubmit()
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1)
assert.equal(dispatchProps.setGasData.callCount, 0)
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
assert.equal(dispatchProps.hideModal.callCount, 1)
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
result.gasPriceButtonGroupProps.handleGasPriceSelection()
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
result.someOtherDispatchProp()
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
})
it('should return the expected props when isConfirm is false', () => {
const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps)
assert.equal(result.isConfirm, false)
assert.equal(result.someOtherStateProp, 'baz')
assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
assert.equal(result.someOwnProp, 123)
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
assert.equal(dispatchProps.setGasData.callCount, 0)
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
assert.equal(dispatchProps.cancelAndClose.callCount, 0)
result.onSubmit('mockNewLimit', 'mockNewPrice')
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
assert.equal(dispatchProps.setGasData.callCount, 1)
assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice'])
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1)
assert.equal(dispatchProps.cancelAndClose.callCount, 1)
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0)
result.gasPriceButtonGroupProps.handleGasPriceSelection()
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1)
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
result.someOtherDispatchProp()
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
})
it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => {
const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps)
result.onSubmit()
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0)
assert.equal(dispatchProps.setGasData.callCount, 0)
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0)
assert.equal(dispatchProps.cancelAndClose.callCount, 1)
assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1)
assert.equal(dispatchProps.hideSidebar.callCount, 1)
})
})
})

@ -0,0 +1,89 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ButtonGroup from '../../button-group'
import Button from '../../button'
const GAS_OBJECT_PROPTYPES_SHAPE = {
label: PropTypes.string,
feeInPrimaryCurrency: PropTypes.string,
feeInSecondaryCurrency: PropTypes.string,
timeEstimate: PropTypes.string,
priceInHexWei: PropTypes.string,
}
export default class GasPriceButtonGroup extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
buttonDataLoading: PropTypes.bool,
className: PropTypes.string,
defaultActiveButtonIndex: PropTypes.number,
gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)),
handleGasPriceSelection: PropTypes.func,
newActiveButtonIndex: PropTypes.number,
noButtonActiveByDefault: PropTypes.bool,
showCheck: PropTypes.bool,
}
renderButtonContent ({
labelKey,
feeInPrimaryCurrency,
feeInSecondaryCurrency,
timeEstimate,
}, {
className,
showCheck,
}) {
return (<div>
{ labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> }
{ timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> }
{ feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> }
{ feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> }
{ showCheck && <div className="button-check-wrapper"><i className="fa fa-check fa-sm" /></div> }
</div>)
}
renderButton ({
priceInHexWei,
...renderableGasInfo
}, {
buttonDataLoading,
handleGasPriceSelection,
...buttonContentPropsAndFlags
}, index) {
return (
<Button
onClick={() => handleGasPriceSelection(priceInHexWei)}
key={`gas-price-button-${index}`}
>
{this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)}
</Button>
)
}
render () {
const {
gasButtonInfo,
defaultActiveButtonIndex = 1,
newActiveButtonIndex,
noButtonActiveByDefault = false,
buttonDataLoading,
...buttonPropsAndFlags
} = this.props
return (
!buttonDataLoading
? <ButtonGroup
className={buttonPropsAndFlags.className}
defaultActiveButtonIndex={defaultActiveButtonIndex}
newActiveButtonIndex={newActiveButtonIndex}
noButtonActiveByDefault={noButtonActiveByDefault}
>
{ gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) }
</ButtonGroup>
: <div className={`${buttonPropsAndFlags.className}__loading-container`}>{ this.context.t('loading') }</div>
)
}
}

@ -0,0 +1 @@
export { default } from './gas-price-button-group.component'

@ -0,0 +1,235 @@
.gas-price-button-group {
margin-top: 22px;
display: flex;
justify-content: space-evenly;
width: 100%;
padding-left: 20px;
padding-right: 20px;
&__primary-currency {
font-size: 18px;
height: 20.5px;
margin-bottom: 7.5px;
}
&__time-estimate {
margin-top: 5.5px;
color: $silver-chalice;
height: 15.4px;
}
&__loading-container {
height: 130px;
}
.button-group__button, .button-group__button--active {
height: 130px;
max-width: 108px;
font-size: 12px;
flex-direction: column;
align-items: center;
display: flex;
padding-top: 17px;
border-radius: 4px;
border: 2px solid $spindle;
background: $white;
color: $scorpion;
div {
display: flex;
flex-direction: column;
align-items: center;
}
i {
&:last-child {
display: none;
}
}
}
.button-group__button--active {
border: 2px solid $curious-blue;
color: $scorpion;
i {
&:last-child {
display: flex;
color: $curious-blue;
margin-top: 8px
}
}
}
}
.gas-price-button-group--small {
display: flex;
justify-content: stretch;
max-width: 260px;
&__button-fiat-price {
font-size: 13px;
}
&__button-label {
font-size: 16px;
}
&__label {
font-weight: 500;
}
&__primary-currency {
font-size: 12px;
@media screen and (max-width: 575px) {
font-size: 10px;
}
}
&__secondary-currency {
font-size: 12px;
@media screen and (max-width: 575px) {
font-size: 10px;
}
}
&__loading-container {
height: 78px;
}
.button-group__button, .button-group__button--active {
height: 78px;
background: white;
color: $scorpion;
padding-top: 9px;
padding-left: 8.5px;
@media screen and (max-width: $break-small) {
padding-left: 4px;
}
div {
display: flex;
flex-flow: column;
align-items: flex-start;
justify-content: flex-start;
}
i {
&:last-child {
display: none;
}
}
}
.button-group__button--active {
color: $white;
background: $dodger-blue;
i {
&:last-child {
display: flex;
color: $curious-blue;
margin-top: 10px
}
}
}
}
.gas-price-button-group--alt {
display: flex;
justify-content: stretch;
width: 95%;
&__button-fiat-price {
font-size: 13px;
}
&__button-label {
font-size: 16px;
}
&__label {
font-weight: 500;
font-size: 10px;
text-transform: capitalize;
}
&__primary-currency {
font-size: 11px;
margin-top: 3px;
}
&__secondary-currency {
font-size: 11px;
}
&__loading-container {
height: 78px;
}
&__time-estimate {
font-size: 14px;
font-weight: 500;
margin-top: 4px;
color: $black;
}
.button-group__button, .button-group__button--active {
height: 78px;
background: white;
color: #2A4055;
width: 108px;
height: 97px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.151579);
border-radius: 6px;
border: none;
div {
display: flex;
flex-flow: column;;
align-items: flex-start;
justify-content: flex-start;
position: relative;
}
.button-check-wrapper {
display: none;
}
&:first-child {
margin-right: 6px;
}
&:last-child {
margin-left: 6px;
}
}
.button-group__button--active {
background: #F7FCFF;
border: 2px solid #2C8BDC;
.button-check-wrapper {
height: 16px;
width: 16px;
border-radius: 8px;
position: absolute;
top: -11px;
right: -10px;
background: #D5ECFA;
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
}
i {
display: flex;
color: $curious-blue;
font-size: 12px;
}
}
}

@ -0,0 +1,233 @@
import React from 'react'
import assert from 'assert'
import shallow from '../../../../../lib/shallow-with-context'
import sinon from 'sinon'
import GasPriceButtonGroup from '../gas-price-button-group.component'
import ButtonGroup from '../../../button-group/'
const mockGasPriceButtonGroupProps = {
buttonDataLoading: false,
className: 'gas-price-button-group',
gasButtonInfo: [
{
feeInPrimaryCurrency: '$0.52',
feeInSecondaryCurrency: '0.0048 ETH',
timeEstimate: '~ 1 min 0 sec',
priceInHexWei: '0xa1b2c3f',
},
{
feeInPrimaryCurrency: '$0.39',
feeInSecondaryCurrency: '0.004 ETH',
timeEstimate: '~ 1 min 30 sec',
priceInHexWei: '0xa1b2c39',
},
{
feeInPrimaryCurrency: '$0.30',
feeInSecondaryCurrency: '0.00354 ETH',
timeEstimate: '~ 2 min 1 sec',
priceInHexWei: '0xa1b2c30',
},
],
handleGasPriceSelection: sinon.spy(),
noButtonActiveByDefault: true,
defaultActiveButtonIndex: 2,
showCheck: true,
}
const mockButtonPropsAndFlags = Object.assign({}, {
className: mockGasPriceButtonGroupProps.className,
handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection,
showCheck: mockGasPriceButtonGroupProps.showCheck,
})
sinon.spy(GasPriceButtonGroup.prototype, 'renderButton')
sinon.spy(GasPriceButtonGroup.prototype, 'renderButtonContent')
describe('GasPriceButtonGroup Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasPriceButtonGroup
{...mockGasPriceButtonGroupProps}
/>)
})
afterEach(() => {
GasPriceButtonGroup.prototype.renderButton.resetHistory()
GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
mockGasPriceButtonGroupProps.handleGasPriceSelection.resetHistory()
})
describe('render', () => {
it('should render a ButtonGroup', () => {
assert(wrapper.is(ButtonGroup))
})
it('should render the correct props on the ButtonGroup', () => {
const {
className,
defaultActiveButtonIndex,
noButtonActiveByDefault,
} = wrapper.props()
assert.equal(className, 'gas-price-button-group')
assert.equal(defaultActiveButtonIndex, 2)
assert.equal(noButtonActiveByDefault, true)
})
function renderButtonArgsTest (i, mockButtonPropsAndFlags) {
assert.deepEqual(
GasPriceButtonGroup.prototype.renderButton.getCall(i).args,
[
Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[i]),
mockButtonPropsAndFlags,
i,
]
)
}
it('should call this.renderButton 3 times, with the correct args', () => {
assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3)
renderButtonArgsTest(0, mockButtonPropsAndFlags)
renderButtonArgsTest(1, mockButtonPropsAndFlags)
renderButtonArgsTest(2, mockButtonPropsAndFlags)
})
it('should show loading if buttonDataLoading', () => {
wrapper.setProps({ buttonDataLoading: true })
assert(wrapper.is('div'))
assert(wrapper.hasClass('gas-price-button-group__loading-container'))
assert.equal(wrapper.text(), 'loading')
})
})
describe('renderButton', () => {
let wrappedRenderButtonResult
beforeEach(() => {
GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
const renderButtonResult = GasPriceButtonGroup.prototype.renderButton(
Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]),
mockButtonPropsAndFlags
)
wrappedRenderButtonResult = shallow(renderButtonResult)
})
it('should render a button', () => {
assert.equal(wrappedRenderButtonResult.type(), 'button')
})
it('should call the correct method when clicked', () => {
assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 0)
wrappedRenderButtonResult.props().onClick()
assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 1)
assert.deepEqual(
mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args,
[mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei]
)
})
it('should call this.renderButtonContent with the correct args', () => {
assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 1)
const {
feeInPrimaryCurrency,
feeInSecondaryCurrency,
timeEstimate,
} = mockGasPriceButtonGroupProps.gasButtonInfo[0]
const {
showCheck,
className,
} = mockGasPriceButtonGroupProps
assert.deepEqual(
GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args,
[
{
feeInPrimaryCurrency,
feeInSecondaryCurrency,
timeEstimate,
},
{
showCheck,
className,
},
]
)
})
})
describe('renderButtonContent', () => {
it('should render a label if passed a labelKey', () => {
const renderButtonContentResult = wrapper.instance().renderButtonContent({
labelKey: 'mockLabelKey',
}, {
className: 'someClass',
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey')
})
it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => {
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
}, {
className: 'someClass',
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
assert.equal(wrappedRenderButtonContentResult.find('.someClass__primary-currency').text(), 'mockFeeInPrimaryCurrency')
})
it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => {
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
}, {
className: 'someClass',
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
assert.equal(wrappedRenderButtonContentResult.find('.someClass__secondary-currency').text(), 'mockFeeInSecondaryCurrency')
})
it('should render a timeEstimate if passed a timeEstimate', () => {
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({
timeEstimate: 'mockTimeEstimate',
}, {
className: 'someClass',
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
assert.equal(wrappedRenderButtonContentResult.find('.someClass__time-estimate').text(), 'mockTimeEstimate')
})
it('should render a check if showCheck is true', () => {
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {
className: 'someClass',
showCheck: true,
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.find('.fa-check').length, 1)
})
it('should render all elements if all args passed', () => {
const renderButtonContentResult = wrapper.instance().renderButtonContent({
labelKey: 'mockLabel',
feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
timeEstimate: 'mockTimeEstimate',
}, {
className: 'someClass',
showCheck: true,
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.children().length, 5)
})
it('should render no elements if all args passed', () => {
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.children().length, 0)
})
})
})

@ -0,0 +1,108 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import * as d3 from 'd3'
import {
generateChart,
getCoordinateData,
handleChartUpdate,
hideDataUI,
setTickPosition,
handleMouseMove,
} from './gas-price-chart.utils.js'
export default class GasPriceChart extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
gasPrices: PropTypes.array,
estimatedTimes: PropTypes.array,
gasPricesMax: PropTypes.number,
estimatedTimesMax: PropTypes.number,
currentPrice: PropTypes.number,
updateCustomGasPrice: PropTypes.func,
}
renderChart ({
currentPrice,
gasPrices,
estimatedTimes,
gasPricesMax,
estimatedTimesMax,
updateCustomGasPrice,
}) {
const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax)
setTimeout(function () {
setTickPosition('y', 0, -5, 8)
setTickPosition('y', 1, -3, -5)
setTickPosition('x', 0, 3)
setTickPosition('x', 1, 3, -8)
const { x: domainX } = getCoordinateData('.domain')
const { x: yAxisX } = getCoordinateData('.c3-axis-y-label')
const { x: tickX } = getCoordinateData('.c3-axis-x .tick')
d3.select('.c3-axis-x .tick').attr('transform', 'translate(' + (domainX - tickX) / 2 + ', 0)')
d3.select('.c3-axis-x-label').attr('transform', 'translate(0,-15)')
d3.select('.c3-axis-y-label').attr('transform', 'translate(' + (domainX - yAxisX - 12) + ', 2) rotate(-90)')
d3.select('.c3-xgrid-focus line').attr('y2', 98)
d3.select('.c3-chart').on('mouseout', () => {
hideDataUI(chart, '#overlayed-circle')
})
d3.select('.c3-chart').on('click', () => {
const { x: newGasPrice } = d3.select('#overlayed-circle').datum()
updateCustomGasPrice(newGasPrice)
})
const { x: chartXStart, width: chartWidth } = getCoordinateData('.c3-areas-data1')
handleChartUpdate({
chart,
gasPrices,
newPrice: currentPrice,
cssId: '#set-circle',
})
d3.select('.c3-chart').on('mousemove', function () {
handleMouseMove({
xMousePos: d3.event.clientX,
chartXStart,
chartWidth,
gasPrices,
estimatedTimes,
chart,
})
})
}, 0)
this.chart = chart
}
componentDidUpdate (prevProps) {
const { gasPrices, currentPrice: newPrice } = this.props
if (prevProps.currentPrice !== newPrice) {
handleChartUpdate({
chart: this.chart,
gasPrices,
newPrice,
cssId: '#set-circle',
})
}
}
componentDidMount () {
this.renderChart(this.props)
}
render () {
return (
<div className="gas-price-chart" id="container">
<div className="gas-price-chart__root" id="chart"></div>
</div>
)
}
}

@ -0,0 +1,354 @@
import * as d3 from 'd3'
import c3 from 'c3'
import BigNumber from 'bignumber.js'
const newBigSigDig = n => (new BigNumber(n.toPrecision(15)))
const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b))
const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus')
const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div')
export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) {
const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({
xMousePos,
chartXStart,
chartWidth,
gasPrices,
estimatedTimes,
})
if (currentPosValue === null && newTimeEstimate === null) {
hideDataUI(chart, '#overlayed-circle')
return
}
const indexOfNewCircle = estimatedTimes.length + 1
const dataUIObj = generateDataUIObj(currentPosValue, indexOfNewCircle, newTimeEstimate)
chart.internal.overlayPoint(dataUIObj, indexOfNewCircle)
chart.internal.showTooltip([dataUIObj], d3.select('.c3-areas-data1')._groups[0])
chart.internal.showXGridFocus([dataUIObj])
}
export function getCoordinateData (selector) {
const node = d3.select(selector).node()
return node ? node.getBoundingClientRect() : {}
}
export function generateDataUIObj (x, index, value) {
return {
x,
value,
index,
id: 'data1',
name: 'data1',
}
}
export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) {
const {
closestLowerValueIndex,
closestLowerValue,
closestHigherValueIndex,
closestHigherValue,
} = getAdjacentGasPrices({ gasPrices, priceToPosition: newPrice })
if (closestLowerValue && closestHigherValue) {
setSelectedCircle({
chart,
newPrice,
closestLowerValueIndex,
closestLowerValue,
closestHigherValueIndex,
closestHigherValue,
})
} else {
hideDataUI(chart, cssId)
}
}
export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) {
const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition)
const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition)
return {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue: gasPrices[closestHigherValueIndex],
closestLowerValue: gasPrices[closestLowerValueIndex],
}
}
export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) {
const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX))
const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated()
return newTimeEstimate.toNumber()
}
export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) {
const chartMouseXPos = bigNumMinus(xMousePos, chartXStart)
const posPercentile = bigNumDiv(chartMouseXPos, chartWidth)
const currentPosValue = (bigNumMinus(gasPrices[gasPrices.length - 1], gasPrices[0]))
.times(newBigSigDig(posPercentile))
.plus(newBigSigDig(gasPrices[0]))
.toNumber()
const {
closestLowerValueIndex,
closestLowerValue,
closestHigherValueIndex,
closestHigherValue,
} = getAdjacentGasPrices({ gasPrices, priceToPosition: currentPosValue })
return !closestHigherValue || !closestLowerValue
? {
currentPosValue: null,
newTimeEstimate: null,
}
: {
currentPosValue,
newTimeEstimate: extrapolateY({
higherY: estimatedTimes[closestHigherValueIndex],
lowerY: estimatedTimes[closestLowerValueIndex],
higherX: closestHigherValue,
lowerX: closestLowerValue,
xForExtrapolation: currentPosValue,
}),
}
}
export function hideDataUI (chart, dataNodeId) {
const overLayedCircle = d3.select(dataNodeId)
if (!overLayedCircle.empty()) {
overLayedCircle.remove()
}
d3.select('.c3-tooltip-container').style('display', 'none !important')
chart.internal.hideXGridFocus()
}
export function setTickPosition (axis, n, newPosition, secondNewPosition) {
const positionToShift = axis === 'y' ? 'x' : 'y'
const secondPositionToShift = axis === 'y' ? 'y' : 'x'
d3.select('#chart')
.select(`.c3-axis-${axis}`)
.selectAll('.tick')
.filter((d, i) => i === n)
.select('text')
.attr(positionToShift, 0)
.select('tspan')
.attr(positionToShift, newPosition)
.attr(secondPositionToShift, secondNewPosition || 0)
.style('visibility', 'visible')
}
export function appendOrUpdateCircle ({ data, itemIndex, cx, cy, cssId, appendOnly }) {
const circle = this.main
.select('.c3-selected-circles' + this.getTargetSelectorSuffix(data.id))
.selectAll(`.c3-selected-circle-${itemIndex}`)
if (appendOnly || circle.empty()) {
circle.data([data])
.enter().append('circle')
.attr('class', () => this.generateClass('c3-selected-circle', itemIndex))
.attr('id', cssId)
.attr('cx', cx)
.attr('cy', cy)
.attr('stroke', () => this.color(data))
.attr('r', 6)
} else {
circle.data([data])
.attr('cx', cx)
.attr('cy', cy)
}
}
export function setSelectedCircle ({
chart,
newPrice,
closestLowerValueIndex,
closestLowerValue,
closestHigherValueIndex,
closestHigherValue,
}) {
const numberOfValues = chart.internal.data.xs.data1.length
const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`)
let { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`)
let count = closestHigherValueIndex + 1
if (lowerX && higherX) {
while (lowerX === higherX) {
higherX = getCoordinateData(`.c3-circle-${count}`).x
higherY = getCoordinateData(`.c3-circle-${count}`).y
count++
}
}
const currentX = bigNumMinus(higherX, lowerX)
.times(bigNumMinus(newPrice, closestLowerValue))
.div(bigNumMinus(closestHigherValue, closestLowerValue))
.plus(newBigSigDig(lowerX))
const newTimeEstimate = extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX })
chart.internal.selectPoint(
generateDataUIObj(currentX.toNumber(), numberOfValues, newTimeEstimate),
numberOfValues
)
}
export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) {
const gasPricesMaxPadded = gasPricesMax + 1
const chart = c3.generate({
size: {
height: 165,
},
transition: {
duration: 0,
},
padding: {left: 20, right: 15, top: 6, bottom: 10},
data: {
x: 'x',
columns: [
['x', ...gasPrices],
['data1', ...estimatedTimes],
],
types: {
data1: 'area',
},
selection: {
enabled: false,
},
},
color: {
data1: '#259de5',
},
axis: {
x: {
min: gasPrices[0],
max: gasPricesMax,
tick: {
values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)],
outer: false,
format: function (val) { return val + ' GWEI' },
},
padding: {left: gasPricesMax / 50, right: gasPricesMax / 50},
label: {
text: 'Gas Price ($)',
position: 'outer-center',
},
},
y: {
padding: {top: 7, bottom: 7},
tick: {
values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)],
outer: false,
},
label: {
text: 'Confirmation time (sec)',
position: 'outer-middle',
},
min: 0,
},
},
legend: {
show: false,
},
grid: {
x: {},
lines: {
front: false,
},
},
point: {
focus: {
expand: {
enabled: false,
r: 3.5,
},
},
},
tooltip: {
format: {
title: (v) => v.toPrecision(4),
},
contents: function (d) {
const titleFormat = this.config.tooltip_format_title
let text
d.forEach(el => {
if (el && (el.value || el.value === 0) && !text) {
text = "<table class='" + 'custom-tooltip' + "'>" + "<tr><th colspan='2'>" + titleFormat(el.x) + '</th></tr>'
}
})
return text + '</table>' + "<div class='tooltip-arrow'></div>"
},
position: function (data) {
if (d3.select('#overlayed-circle').empty()) {
return { top: -100, left: -100 }
}
const { x: circleX, y: circleY, width: circleWidth } = getCoordinateData('#overlayed-circle')
const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-chart')
// TODO: Confirm the below constants work with all data sets and screen sizes
const flipTooltip = circleY - circleWidth < chartYStart + 5
d3
.select('.tooltip-arrow')
.style('margin-top', flipTooltip ? '-16px' : '4px')
return {
top: bigNumMinus(circleY, chartYStart).minus(19).plus(flipTooltip ? circleWidth + 38 : 0).toNumber(),
left: bigNumMinus(circleX, chartXStart).plus(newBigSigDig(circleWidth)).minus(bigNumDiv(gasPricesMaxPadded, 50)).toNumber(),
}
},
show: true,
},
})
chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) {
const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-areas-data1')
d3.select('#set-circle').remove()
appendOrUpdateCircle.bind(this)({
data,
itemIndex,
cx: () => bigNumMinus(data.x, chartXStart).plus(11).toNumber(),
cy: () => bigNumMinus(data.value, chartYStart).plus(10).toNumber(),
cssId: 'set-circle',
appendOnly: true,
})
}
chart.internal.overlayPoint = function (data, itemIndex) {
appendOrUpdateCircle.bind(this)({
data,
itemIndex,
cx: this.circleX.bind(this),
cy: this.circleY.bind(this),
cssId: 'overlayed-circle',
})
}
chart.internal.showTooltip = function (selectedData, element) {
const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0))
if (dataToShow.length) {
this.tooltip.html(
this.config.tooltip_contents.call(this, selectedData, this.axis.getXAxisTickFormat(), this.getYFormat(), this.color)
).style('display', 'flex')
// Get tooltip dimensions
const tWidth = this.tooltip.property('offsetWidth')
const tHeight = this.tooltip.property('offsetHeight')
const position = this.config.tooltip_position.call(this, dataToShow, tWidth, tHeight, element)
// Set tooltip
this.tooltip.style('top', position.top + 'px').style('left', position.left + 'px')
}
}
return chart
}

@ -0,0 +1 @@
export { default } from './gas-price-chart.component'

@ -0,0 +1,132 @@
.gas-price-chart {
display: flex;
position: relative;
justify-content: center;
&__root {
max-height: 154px;
max-width: 391px;
position: relative;
overflow: hidden;
@media screen and (max-width: $break-small) {
max-width: 326px;
}
}
.tick text, .c3-axis-x-label, .c3-axis-y-label {
font-family: Roboto;
font-style: normal;
font-weight: bold;
line-height: normal;
font-size: 8px;
text-align: center;
fill: #9A9CA6 !important;
}
.c3-tooltip-container {
display: flex;
justify-content: center !important;
align-items: flex-end !important;
}
.custom-tooltip {
background: rgba(0, 0, 0, 1);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 3px;
opacity: 1 !important;
height: 21px;
z-index: 1;
}
.tooltip-arrow {
background: black;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
opacity: 1 !important;
width: 9px;
height: 9px;
margin-top: 4px;
}
.custom-tooltip th {
font-family: Roboto;
font-style: normal;
font-weight: 500;
line-height: normal;
font-size: 10px;
text-align: center;
padding: 3px;
color: #FFFFFF;
}
.c3-circle {
visibility: hidden;
}
.c3-selected-circle, .c3-circle._expanded_ {
fill: #FFFFFF !important;
stroke-width: 2.4px !important;
stroke: #2d9fd9 !important;
/* visibility: visible; */
}
#set-circle {
fill: #313A5E !important;
stroke: #313A5E !important;
}
.c3-axis-x-label, .c3-axis-y-label {
font-weight: normal;
}
.tick text tspan {
visibility: hidden;
}
.c3-circle {
fill: #2d9fd9 !important;
}
.c3-line-data1 {
stroke: #2d9fd9 !important;
background: rgba(0,0,0,0) !important;
color: rgba(0,0,0,0) !important;
}
.c3 path {
fill: none;
}
.c3 path.c3-area-data1 {
opacity: 1;
fill: #e9edf1 !important;
}
.c3-xgrid-line line {
stroke: #B8B8B8 !important;
}
.c3-xgrid-focus {
stroke: #aaa;
}
.c3-axis-x .domain {
fill: none;
stroke: none;
}
.c3-axis-y .domain {
fill: none;
stroke: #C8CCD6;
}
.c3-event-rect {
cursor: pointer;
}
}
#chart {
background: #F8F9FB
}

@ -0,0 +1,218 @@
import React from 'react'
import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
import shallow from '../../../../../lib/shallow-with-context'
import * as d3 from 'd3'
function timeout (time) {
return new Promise((resolve, reject) => {
setTimeout(resolve, time)
})
}
const propsMethodSpies = {
updateCustomGasPrice: sinon.spy(),
}
const selectReturnSpies = {
empty: sinon.spy(),
remove: sinon.spy(),
style: sinon.spy(),
select: d3.select,
attr: sinon.spy(),
on: sinon.spy(),
datum: sinon.stub().returns({ x: 'mockX' }),
}
const mockSelectReturn = {
...d3.select('div'),
node: () => ({
getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }),
}),
...selectReturnSpies,
}
const gasPriceChartUtilsSpies = {
appendOrUpdateCircle: sinon.spy(),
generateChart: sinon.stub().returns({ mockChart: true }),
generateDataUIObj: sinon.spy(),
getAdjacentGasPrices: sinon.spy(),
getCoordinateData: sinon.stub().returns({ x: 'mockCoordinateX', width: 'mockWidth' }),
getNewXandTimeEstimate: sinon.spy(),
handleChartUpdate: sinon.spy(),
hideDataUI: sinon.spy(),
setSelectedCircle: sinon.spy(),
setTickPosition: sinon.spy(),
handleMouseMove: sinon.spy(),
}
const testProps = {
gasPrices: [1.5, 2.5, 4, 8],
estimatedTimes: [100, 80, 40, 10],
gasPricesMax: 9,
estimatedTimesMax: '100',
currentPrice: 6,
updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
}
const GasPriceChart = proxyquire('../gas-price-chart.component.js', {
'./gas-price-chart.utils.js': gasPriceChartUtilsSpies,
'd3': {
...d3,
select: function (...args) {
const result = d3.select(...args)
return result.empty()
? mockSelectReturn
: result
},
event: {
clientX: 'mockClientX',
},
},
}).default
sinon.spy(GasPriceChart.prototype, 'renderChart')
describe('GasPriceChart Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasPriceChart {...testProps} />)
})
describe('render()', () => {
it('should render', () => {
assert(wrapper.hasClass('gas-price-chart'))
})
it('should render the chart div', () => {
assert(wrapper.childAt(0).hasClass('gas-price-chart__root'))
assert.equal(wrapper.childAt(0).props().id, 'chart')
})
})
describe('componentDidMount', () => {
it('should call this.renderChart with the components props', () => {
assert(GasPriceChart.prototype.renderChart.callCount, 1)
wrapper.instance().componentDidMount()
assert(GasPriceChart.prototype.renderChart.callCount, 2)
assert.deepEqual(GasPriceChart.prototype.renderChart.getCall(1).args, [{...testProps}])
})
})
describe('componentDidUpdate', () => {
it('should call handleChartUpdate if props.currentPrice has changed', () => {
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
wrapper.instance().componentDidUpdate({ currentPrice: 7 })
assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 1)
})
it('should call handleChartUpdate with the correct props', () => {
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
wrapper.instance().componentDidUpdate({ currentPrice: 7 })
assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
chart: { mockChart: true },
gasPrices: [1.5, 2.5, 4, 8],
newPrice: 6,
cssId: '#set-circle',
}])
})
it('should not call handleChartUpdate if props.currentPrice has not changed', () => {
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
wrapper.instance().componentDidUpdate({ currentPrice: 6 })
assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 0)
})
})
describe('renderChart', () => {
it('should call setTickPosition 4 times, with the expected props', async () => {
await timeout(0)
gasPriceChartUtilsSpies.setTickPosition.resetHistory()
assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 0)
wrapper.instance().renderChart(testProps)
await timeout(0)
assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 4)
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(0).args, ['y', 0, -5, 8])
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(1).args, ['y', 1, -3, -5])
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3])
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8])
})
it('should call handleChartUpdate with the correct props', async () => {
await timeout(0)
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory()
wrapper.instance().renderChart(testProps)
await timeout(0)
assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{
chart: { mockChart: true },
gasPrices: [1.5, 2.5, 4, 8],
newPrice: 6,
cssId: '#set-circle',
}])
})
it('should add three events to the chart', async () => {
await timeout(0)
selectReturnSpies.on.resetHistory()
assert.equal(selectReturnSpies.on.callCount, 0)
wrapper.instance().renderChart(testProps)
await timeout(0)
assert.equal(selectReturnSpies.on.callCount, 3)
const firstOnEventArgs = selectReturnSpies.on.getCall(0).args
assert.equal(firstOnEventArgs[0], 'mouseout')
const secondOnEventArgs = selectReturnSpies.on.getCall(1).args
assert.equal(secondOnEventArgs[0], 'click')
const thirdOnEventArgs = selectReturnSpies.on.getCall(2).args
assert.equal(thirdOnEventArgs[0], 'mousemove')
})
it('should hide the data UI on mouseout', async () => {
await timeout(0)
selectReturnSpies.on.resetHistory()
wrapper.instance().renderChart(testProps)
gasPriceChartUtilsSpies.hideDataUI.resetHistory()
await timeout(0)
const mouseoutEventArgs = selectReturnSpies.on.getCall(0).args
assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 0)
mouseoutEventArgs[1]()
assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 1)
assert.deepEqual(gasPriceChartUtilsSpies.hideDataUI.getCall(0).args, [{ mockChart: true }, '#overlayed-circle'])
})
it('should updateCustomGasPrice on click', async () => {
await timeout(0)
selectReturnSpies.on.resetHistory()
wrapper.instance().renderChart(testProps)
propsMethodSpies.updateCustomGasPrice.resetHistory()
await timeout(0)
const mouseoutEventArgs = selectReturnSpies.on.getCall(1).args
assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 0)
mouseoutEventArgs[1]()
assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 1)
assert.equal(propsMethodSpies.updateCustomGasPrice.getCall(0).args[0], 'mockX')
})
it('should handle mousemove', async () => {
await timeout(0)
selectReturnSpies.on.resetHistory()
wrapper.instance().renderChart(testProps)
gasPriceChartUtilsSpies.handleMouseMove.resetHistory()
await timeout(0)
const mouseoutEventArgs = selectReturnSpies.on.getCall(2).args
assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 0)
mouseoutEventArgs[1]()
assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 1)
assert.deepEqual(gasPriceChartUtilsSpies.handleMouseMove.getCall(0).args, [{
xMousePos: 'mockClientX',
chartXStart: 'mockCoordinateX',
chartWidth: 'mockWidth',
gasPrices: testProps.gasPrices,
estimatedTimes: testProps.estimatedTimes,
chart: { mockChart: true },
}])
})
})
})

@ -0,0 +1,48 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class AdvancedTabContent extends Component {
static propTypes = {
onChange: PropTypes.func,
lowLabel: PropTypes.string,
highLabel: PropTypes.string,
value: PropTypes.number,
step: PropTypes.number,
max: PropTypes.number,
min: PropTypes.number,
}
render () {
const {
onChange,
lowLabel,
highLabel,
value,
step,
max,
min,
} = this.props
return (
<div className="gas-slider">
<input
className="gas-slider__input"
type="range"
step={step}
max={max}
min={min}
value={value}
id="gasSlider"
onChange={event => onChange(event.target.value)}
/>
<div className="gas-slider__bar">
<div className="gas-slider__colored"/>
</div>
<div className="gas-slider__labels">
<span>{lowLabel}</span>
<span>{highLabel}</span>
</div>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './gas-slider.component'

@ -0,0 +1,54 @@
.gas-slider {
position: relative;
width: 322px;
&__input {
width: 322px;
margin-left: -2px;
z-index: 2;
}
input[type=range] {
-webkit-appearance: none !important;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none !important;
height: 34px;
width: 34px;
background-color: $curious-blue;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
border-radius: 50%;
position: relative;
z-index: 10;
}
&__bar {
height: 6px;
width: 322px;
background: $alto;
display: flex;
justify-content: space-between;
position: absolute;
top: 16px;
z-index: 0;
border-radius: 4px;
}
&__colored {
height: 6px;
border-radius: 4px;
margin-left: 102px;
width: 322px;
z-index: 1;
background-color: $blizzard-blue;
}
&__labels {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-top: -6px;
color: $mid-gray;
}
}

@ -0,0 +1,14 @@
const selectors = {
getCurrentBlockTime,
getBasicGasEstimateLoadingStatus,
}
module.exports = selectors
function getCurrentBlockTime (state) {
return state.gas.currentBlockTime
}
function getBasicGasEstimateLoadingStatus (state) {
return state.gas.basicEstimateIsLoading
}

@ -0,0 +1,5 @@
@import './gas-slider/index';
@import './gas-modal-page-container/index';
@import './gas-price-chart/index';

@ -63,3 +63,13 @@
@import './sidebars/index';
@import './unit-input/index';
@import './gas-customization/gas-modal-page-container/index';
@import './gas-customization/gas-modal-page-container/index';
@import './gas-customization/gas-modal-page-container/index';
@import './gas-customization/index';
@import './gas-customization/gas-price-button-group/index';

@ -4,6 +4,7 @@ const inherits = require('util').inherits
const connect = require('react-redux').connect
const FadeModal = require('boron').FadeModal
const actions = require('../../actions')
const { resetCustomData: resetCustomGasData } = require('../../ducks/gas.duck')
const isMobileView = require('../../../lib/is-mobile-view')
const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
@ -17,18 +18,17 @@ const ExportPrivateKeyModal = require('./export-private-key-modal')
const NewAccountModal = require('./new-account-modal')
const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const QRScanner = require('./qr-scanner')
import ConfirmRemoveAccount from './confirm-remove-account'
import ConfirmResetAccount from './confirm-reset-account'
import TransactionConfirmed from './transaction-confirmed'
import ConfirmCustomizeGasModal from './customize-gas'
import CancelTransaction from './cancel-transaction'
import WelcomeBeta from './welcome-beta'
import RejectTransactions from './reject-transactions'
import ClearApprovedOrigins from './clear-approved-origins'
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
@ -295,7 +295,7 @@ const MODALS = {
CUSTOMIZE_GAS: {
contents: [
h(CustomizeGasModal),
h(ConfirmCustomizeGasModal),
],
mobileModalStyle: {
width: '100vw',
@ -307,35 +307,20 @@ const MODALS = {
margin: '0 auto',
},
laptopModalStyle: {
width: '720px',
height: '377px',
width: 'auto',
height: '0px',
top: '80px',
left: '0px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
position: 'relative',
},
contentStyle: {
borderRadius: '8px',
},
CONFIRM_CUSTOMIZE_GAS: {
contents: h(ConfirmCustomizeGasModal),
mobileModalStyle: {
width: '100vw',
height: '100vh',
top: '0',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '720px',
height: '377px',
top: '80px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
customOnHideOpts: {
action: resetCustomGasData,
args: [],
},
},
@ -412,8 +397,11 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
hideModal: (customOnHideOpts) => {
dispatch(actions.hideModal())
if (customOnHideOpts && customOnHideOpts.action) {
dispatch(customOnHideOpts.action(...customOnHideOpts.args))
}
},
hideWarning: () => {
dispatch(actions.hideWarning())
@ -445,7 +433,7 @@ Modal.prototype.render = function () {
if (modal.onHide) {
modal.onHide(this.props)
}
this.onHide()
this.onHide(modal.customOnHideOpts)
},
ref: (ref) => {
this.modalRef = ref
@ -467,11 +455,11 @@ Modal.prototype.componentWillReceiveProps = function (nextProps) {
}
}
Modal.prototype.onHide = function () {
Modal.prototype.onHide = function (customOnHideOpts) {
if (this.props.onHideCallback) {
this.props.onHideCallback()
}
this.props.hideModal()
this.props.hideModal(customOnHideOpts)
}
Modal.prototype.hide = function () {

@ -6,6 +6,7 @@
display: flex;
flex-flow: column;
border-radius: 8px;
overflow-y: auto;
&__header {
display: flex;
@ -194,10 +195,10 @@
.page-container {
height: 100%;
width: 100%;
overflow-y: auto;
background-color: $white;
border-radius: 0;
flex: 1;
overflow-y: auto;
}
}

@ -12,6 +12,7 @@ export default class PageContainerFooter extends Component {
submitText: PropTypes.string,
disabled: PropTypes.bool,
submitButtonType: PropTypes.string,
hideCancel: PropTypes.bool,
}
static contextTypes = {
@ -27,20 +28,21 @@ export default class PageContainerFooter extends Component {
submitText,
disabled,
submitButtonType,
hideCancel,
} = this.props
return (
<div className="page-container__footer">
<header>
<Button
{!hideCancel && <Button
type="default"
large
className="page-container__footer-button"
onClick={e => onCancel(e)}
>
{ cancelText || this.context.t('cancel') }
</Button>
</Button>}
<Button
type={submitButtonType || 'primary'}

@ -12,6 +12,7 @@ export default class PageContainerHeader extends Component {
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
tabs: PropTypes.node,
headerCloseText: PropTypes.string,
}
renderTabs () {
@ -41,7 +42,7 @@ export default class PageContainerHeader extends Component {
}
render () {
const { title, subtitle, onClose, tabs } = this.props
const { title, subtitle, onClose, tabs, headerCloseText } = this.props
return (
<div className={
@ -66,7 +67,9 @@ export default class PageContainerHeader extends Component {
}
{
onClose && <div
onClose && headerCloseText
? <div className="page-container__header-close-text" onClick={() => onClose()}>{ headerCloseText }</div>
: onClose && <div
className="page-container__header-close"
onClick={() => onClose()}
/>

@ -9,6 +9,7 @@ export default class PageContainer extends PureComponent {
// PageContainerHeader props
backButtonString: PropTypes.string,
backButtonStyles: PropTypes.object,
headerCloseText: PropTypes.string,
onBackButtonClick: PropTypes.func,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
@ -22,6 +23,7 @@ export default class PageContainer extends PureComponent {
// PageContainerFooter props
cancelText: PropTypes.string,
disabled: PropTypes.bool,
hideCancel: PropTypes.bool,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
submitText: PropTypes.string,
@ -58,7 +60,8 @@ export default class PageContainer extends PureComponent {
renderActiveTabContent () {
const { tabsComponent } = this.props
const { children } = tabsComponent.props
let { children } = tabsComponent.props
children = children.filter(child => child)
const { activeTabIndex } = this.state
return children[activeTabIndex]
@ -92,6 +95,8 @@ export default class PageContainer extends PureComponent {
onSubmit,
submitText,
disabled,
headerCloseText,
hideCancel,
} = this.props
return (
@ -105,18 +110,22 @@ export default class PageContainer extends PureComponent {
backButtonStyles={backButtonStyles}
backButtonString={backButtonString}
tabs={this.renderTabs()}
headerCloseText={headerCloseText}
/>
<div className="page-container__bottom">
<div className="page-container__content">
{ this.renderContent() }
</div>
<PageContainerFooter
onCancel={onCancel}
cancelText={cancelText}
hideCancel={hideCancel}
onSubmit={onSubmit}
submitText={submitText}
disabled={disabled}
/>
</div>
</div>
)
}
}

@ -29,7 +29,7 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
const mapStateToProps = (state, props) => {
const { toAddress: propsToAddress } = props
const { confirmTransaction, metamask } = state
const { confirmTransaction, metamask, gas } = state
const {
ethTransactionAmount,
ethTransactionFee,
@ -60,6 +60,12 @@ const mapStateToProps = (state, props) => {
unapprovedTxs,
} = metamask
const assetImage = assetImages[txParamsToAddress]
const {
customGasLimit,
customGasPrice,
} = gas
const { balance } = accounts[selectedAddress]
const { name: fromName } = identities[selectedAddress]
const toAddress = propsToAddress || txParamsToAddress
@ -106,6 +112,10 @@ const mapStateToProps = (state, props) => {
unapprovedTxs,
unapprovedTxCount,
currentNetworkUnapprovedTxs,
customGas: {
gasLimit: customGasLimit || txData.gasPrice,
gasPrice: customGasPrice || txData.gasLimit,
},
}
}
@ -117,7 +127,7 @@ const mapDispatchToProps = dispatch => {
return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
},
showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))
return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }))
},
updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
@ -192,7 +202,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
...ownProps,
showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
txData,
onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas),
validate: validateEditGas,
}),
cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),

@ -32,6 +32,7 @@ export default class ConfirmTransaction extends Component {
setTransactionToConfirm: PropTypes.func,
confirmTransaction: PropTypes.object,
clearConfirmTransaction: PropTypes.func,
fetchBasicGasAndTimeEstimates: PropTypes.func,
}
getParamsTransactionId () {
@ -45,6 +46,7 @@ export default class ConfirmTransaction extends Component {
send = {},
history,
confirmTransaction: { txData: { id: transactionId } = {} },
fetchBasicGasAndTimeEstimates,
} = this.props
if (!totalUnapprovedCount && !send.to) {
@ -53,6 +55,7 @@ export default class ConfirmTransaction extends Component {
}
if (!transactionId) {
fetchBasicGasAndTimeEstimates()
this.setTransactionToConfirm()
}
}

@ -5,6 +5,9 @@ import {
setTransactionToConfirm,
clearConfirmTransaction,
} from '../../../ducks/confirm-transaction.duck'
import {
fetchBasicGasAndTimeEstimates,
} from '../../../ducks/gas.duck'
import ConfirmTransaction from './confirm-transaction.component'
import { getTotalUnapprovedCount } from '../../../selectors'
import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
@ -24,6 +27,7 @@ const mapDispatchToProps = dispatch => {
return {
setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
}
}

@ -11,7 +11,7 @@ export default class GasFeeDisplay extends Component {
convertedCurrency: PropTypes.string,
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
onClick: PropTypes.func,
onReset: PropTypes.func,
};
static contextTypes = {
@ -19,7 +19,7 @@ export default class GasFeeDisplay extends Component {
};
render () {
const { gasTotal, onClick, gasLoadingError } = this.props
const { gasTotal, gasLoadingError, onReset } = this.props
return (
<div className="send-v2__gas-fee-display">
@ -46,11 +46,10 @@ export default class GasFeeDisplay extends Component {
</div>
}
<button
className="sliders-icon-container"
onClick={onClick}
disabled={!gasTotal && !gasLoadingError}
className="gas-fee-reset"
onClick={onReset}
>
<i className="fa fa-sliders sliders-icon" />
{ this.context.t('reset') }
</button>
</div>
)

@ -8,18 +8,20 @@ import sinon from 'sinon'
const propsMethodSpies = {
showCustomizeGasModal: sinon.spy(),
onReset: sinon.spy(),
}
describe('SendGasRow Component', function () {
describe('GasFeeDisplay Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasFeeDisplay
conversionRate={20}
gasTotal={'mockGasTotal'}
onClick={propsMethodSpies.showCustomizeGasModal}
primaryCurrency={'mockPrimaryCurrency'}
convertedCurrency={'mockConvertedCurrency'}
showGasButtonGroup={propsMethodSpies.showCustomizeGasModal}
onReset={propsMethodSpies.onReset}
/>, {context: {t: str => str + '_t'}})
})
@ -41,13 +43,19 @@ describe('SendGasRow Component', function () {
assert.equal(value, 'mockGasTotal')
})
it('should render the Button with the correct props', () => {
it('should render the reset button with the correct props', () => {
const {
onClick,
className,
} = wrapper.find('button').props()
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
assert.equal(className, 'gas-fee-reset')
assert.equal(propsMethodSpies.onReset.callCount, 0)
onClick()
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
assert.equal(propsMethodSpies.onReset.callCount, 1)
})
it('should render the reset button with the correct text', () => {
assert.equal(wrapper.find('button').text(), 'reset_t')
})
})
})

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group'
export default class SendGasRow extends Component {
@ -12,6 +13,9 @@ export default class SendGasRow extends Component {
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object,
gasButtonGroupShown: PropTypes.bool,
resetGasButtons: PropTypes.func,
}
static contextTypes = {
@ -26,21 +30,37 @@ export default class SendGasRow extends Component {
gasTotal,
gasFeeError,
showCustomizeGasModal,
gasPriceButtonGroupProps,
gasButtonGroupShown,
resetGasButtons,
} = this.props
return (
<SendRowWrapper
label={`${this.context.t('gasFee')}:`}
label={`${this.context.t('transactionFee')}:`}
showError={gasFeeError}
errorType={'gasFee'}
>
<GasFeeDisplay
{gasButtonGroupShown
? <div>
<GasPriceButtonGroup
className="gas-price-button-group--small"
showCheck={false}
{...gasPriceButtonGroupProps}
/>
<div className="advanced-gas-options-btn" onClick={() => showCustomizeGasModal()}>
{ this.context.t('advancedOptions') }
</div>
</div>
: <GasFeeDisplay
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
gasLoadingError={gasLoadingError}
gasTotal={gasTotal}
onReset={resetGasButtons}
onClick={() => showCustomizeGasModal()}
/>
/>}
</SendRowWrapper>
)
}

@ -3,25 +3,76 @@ import {
getConversionRate,
getCurrentCurrency,
getGasTotal,
getGasPrice,
} from '../../send.selectors.js'
import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js'
import { showModal } from '../../../../actions'
import {
getBasicGasEstimateLoadingStatus,
getRenderableEstimateDataForSmallButtonsFromGWEI,
getDefaultActiveButtonIndex,
} from '../../../../selectors/custom-gas'
import {
showGasButtonGroup,
} from '../../../../ducks/send.duck'
import {
resetCustomData,
} from '../../../../ducks/gas.duck'
import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
import { showModal, setGasPrice } from '../../../../actions'
import SendGasRow from './send-gas-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
function mapStateToProps (state) {
const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, getGasPrice(state))
return {
conversionRate: getConversionRate(state),
convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state),
gasFeeError: gasFeeIsInError(state),
gasLoadingError: getGasLoadingError(state),
gasPriceButtonGroupProps: {
buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
defaultActiveButtonIndex: 1,
newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null,
gasButtonInfo,
},
gasButtonGroupShown: getGasButtonGroupShown(state),
}
}
function mapDispatchToProps (dispatch) {
return {
showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })),
showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
setGasPrice: newPrice => dispatch(setGasPrice(newPrice)),
showGasButtonGroup: () => dispatch(showGasButtonGroup()),
resetCustomData: () => dispatch(resetCustomData()),
}
}
function mergeProps (stateProps, dispatchProps, ownProps) {
const { gasPriceButtonGroupProps } = stateProps
const { gasButtonInfo } = gasPriceButtonGroupProps
const {
setGasPrice: dispatchSetGasPrice,
showGasButtonGroup: dispatchShowGasButtonGroup,
resetCustomData: dispatchResetCustomData,
...otherDispatchProps
} = dispatchProps
return {
...stateProps,
...otherDispatchProps,
...ownProps,
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
handleGasPriceSelection: dispatchSetGasPrice,
},
resetGasButtons: () => {
dispatchResetCustomData()
dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei)
dispatchShowGasButtonGroup()
},
}
}

@ -1,6 +1,7 @@
const selectors = {
gasFeeIsInError,
getGasLoadingError,
getGasButtonGroupShown,
}
module.exports = selectors
@ -12,3 +13,7 @@ function getGasLoadingError (state) {
function gasFeeIsInError (state) {
return Boolean(state.send.errors.gasFee)
}
function getGasButtonGroupShown (state) {
return state.send.gasButtonGroupShown
}

@ -6,9 +6,11 @@ import SendGasRow from '../send-gas-row.component.js'
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
import GasPriceButtonGroup from '../../../../gas-customization/gas-price-button-group'
const propsMethodSpies = {
showCustomizeGasModal: sinon.spy(),
resetGasButtons: sinon.spy(),
}
describe('SendGasRow Component', function () {
@ -21,12 +23,18 @@ describe('SendGasRow Component', function () {
gasFeeError={'mockGasFeeError'}
gasLoadingError={false}
gasTotal={'mockGasTotal'}
gasButtonGroupShown={false}
showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
resetGasButtons={propsMethodSpies.resetGasButtons}
gasPriceButtonGroupProps={{
someGasPriceButtonGroupProp: 'foo',
anotherGasPriceButtonGroupProp: 'bar',
}}
/>, { context: { t: str => str + '_t' } })
})
afterEach(() => {
propsMethodSpies.showCustomizeGasModal.resetHistory()
propsMethodSpies.resetGasButtons.resetHistory()
})
describe('render', () => {
@ -41,7 +49,7 @@ describe('SendGasRow Component', function () {
errorType,
} = wrapper.find(SendRowWrapper).props()
assert.equal(label, 'gasFee_t:')
assert.equal(label, 'transactionFee_t:')
assert.equal(showError, 'mockGasFeeError')
assert.equal(errorType, 'gasFee')
})
@ -56,14 +64,40 @@ describe('SendGasRow Component', function () {
convertedCurrency,
gasLoadingError,
gasTotal,
onClick,
onReset,
} = wrapper.find(SendRowWrapper).childAt(0).props()
assert.equal(conversionRate, 20)
assert.equal(convertedCurrency, 'mockConvertedCurrency')
assert.equal(gasLoadingError, false)
assert.equal(gasTotal, 'mockGasTotal')
assert.equal(propsMethodSpies.resetGasButtons.callCount, 0)
onReset()
assert.equal(propsMethodSpies.resetGasButtons.callCount, 1)
})
it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => {
wrapper.setProps({ gasButtonGroupShown: true })
const rendered = wrapper.find(SendRowWrapper).childAt(0)
assert.equal(rendered.children().length, 2)
const gasPriceButtonGroup = rendered.childAt(0)
assert(gasPriceButtonGroup.is(GasPriceButtonGroup))
assert(gasPriceButtonGroup.hasClass('gas-price-button-group--small'))
assert.equal(gasPriceButtonGroup.props().showCheck, false)
assert.equal(gasPriceButtonGroup.props().someGasPriceButtonGroupProp, 'foo')
assert.equal(gasPriceButtonGroup.props().anotherGasPriceButtonGroupProp, 'bar')
})
it('should render an advanced options button if gasButtonGroupShown is true', () => {
wrapper.setProps({ gasButtonGroupShown: true })
const rendered = wrapper.find(SendRowWrapper).childAt(0)
assert.equal(rendered.children().length, 2)
const advancedOptionsButton = rendered.childAt(1)
assert.equal(advancedOptionsButton.text(), 'advancedOptions_t')
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
onClick()
advancedOptionsButton.props().onClick()
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
})
})

@ -4,16 +4,27 @@ import sinon from 'sinon'
let mapStateToProps
let mapDispatchToProps
let mergeProps
const actionSpies = {
showModal: sinon.spy(),
setGasPrice: sinon.spy(),
}
const sendDuckSpies = {
showGasButtonGroup: sinon.spy(),
}
const gasDuckSpies = {
resetCustomData: sinon.spy(),
}
proxyquire('../send-gas-row.container.js', {
'react-redux': {
connect: (ms, md) => {
connect: (ms, md, mp) => {
mapStateToProps = ms
mapDispatchToProps = md
mergeProps = mp
return () => ({})
},
},
@ -21,12 +32,21 @@ proxyquire('../send-gas-row.container.js', {
getConversionRate: (s) => `mockConversionRate:${s}`,
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
getGasPrice: (s) => `mockGasPrice:${s}`,
},
'./send-gas-row.selectors.js': {
getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`,
},
'../../../../actions': actionSpies,
'../../../../selectors/custom-gas': {
getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`,
getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`,
getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length,
},
'../../../../ducks/send.duck': sendDuckSpies,
'../../../../ducks/gas.duck': gasDuckSpies,
})
describe('send-gas-row container', () => {
@ -40,6 +60,13 @@ describe('send-gas-row container', () => {
gasTotal: 'mockGasTotal:mockState',
gasFeeError: 'mockGasFeeError:mockState',
gasLoadingError: 'mockGasLoadingError:mockState',
gasPriceButtonGroupProps: {
buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`,
defaultActiveButtonIndex: 1,
newActiveButtonIndex: 49,
gasButtonInfo: `mockGasButtonInfo:mockState`,
},
gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`,
})
})
@ -60,11 +87,74 @@ describe('send-gas-row container', () => {
assert(dispatchSpy.calledOnce)
assert.deepEqual(
actionSpies.showModal.getCall(0).args[0],
{ name: 'CUSTOMIZE_GAS' }
{ name: 'CUSTOMIZE_GAS', hideBasic: true }
)
})
})
describe('setGasPrice()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setGasPrice('mockNewPrice')
assert(dispatchSpy.calledOnce)
assert(actionSpies.setGasPrice.calledOnce)
assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice')
})
})
describe('showGasButtonGroup()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.showGasButtonGroup()
assert(dispatchSpy.calledOnce)
assert(sendDuckSpies.showGasButtonGroup.calledOnce)
})
})
describe('resetCustomData()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.resetCustomData()
assert(dispatchSpy.calledOnce)
assert(gasDuckSpies.resetCustomData.calledOnce)
})
})
})
describe('mergeProps', () => {
let stateProps
let dispatchProps
let ownProps
beforeEach(() => {
stateProps = {
gasPriceButtonGroupProps: {
someGasPriceButtonGroupProp: 'foo',
anotherGasPriceButtonGroupProp: 'bar',
},
someOtherStateProp: 'baz',
}
dispatchProps = {
setGasPrice: sinon.spy(),
someOtherDispatchProp: sinon.spy(),
}
ownProps = { someOwnProp: 123 }
})
it('should return the expected props when isConfirm is true', () => {
const result = mergeProps(stateProps, dispatchProps, ownProps)
assert.equal(result.someOtherStateProp, 'baz')
assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
assert.equal(result.someOwnProp, 123)
assert.equal(dispatchProps.setGasPrice.callCount, 0)
result.gasPriceButtonGroupProps.handleGasPriceSelection()
assert.equal(dispatchProps.setGasPrice.callCount, 1)
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
result.someOtherDispatchProp()
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
})
})
})

@ -2,6 +2,7 @@ import assert from 'assert'
import {
gasFeeIsInError,
getGasLoadingError,
getGasButtonGroupShown,
} from '../send-gas-row.selectors.js'
describe('send-gas-row selectors', () => {
@ -46,4 +47,16 @@ describe('send-gas-row selectors', () => {
})
})
describe('getGasButtonGroupShown()', () => {
it('should return send.gasButtonGroupShown', () => {
const state = {
send: {
gasButtonGroupShown: 'foobar',
},
}
assert.equal(getGasButtonGroupShown(state), 'foobar')
})
})
})

@ -35,6 +35,7 @@ export default class SendTransactionScreen extends PersistentForm {
selectedToken: PropTypes.object,
tokenBalance: PropTypes.string,
tokenContract: PropTypes.object,
fetchBasicGasEstimates: PropTypes.func,
updateAndSetGasTotal: PropTypes.func,
updateSendErrors: PropTypes.func,
updateSendTokenBalance: PropTypes.func,
@ -73,10 +74,10 @@ export default class SendTransactionScreen extends PersistentForm {
selectedAddress,
selectedToken = {},
to: currentToAddress,
updateAndSetGasTotal,
updateAndSetGasLimit,
} = this.props
updateAndSetGasTotal({
updateAndSetGasLimit({
blockGasLimit,
editingTransactionId,
gasLimit,
@ -162,6 +163,13 @@ export default class SendTransactionScreen extends PersistentForm {
}
}
componentDidMount () {
this.props.fetchBasicGasEstimates()
.then(() => {
this.updateGas()
})
}
componentWillMount () {
const {
from: { address },
@ -169,12 +177,12 @@ export default class SendTransactionScreen extends PersistentForm {
tokenContract,
updateSendTokenBalance,
} = this.props
updateSendTokenBalance({
selectedToken,
tokenContract,
address,
})
this.updateGas()
// Show QR Scanner modal if ?scan=true
if (window.location.search === '?scan=true') {

@ -36,6 +36,9 @@ import {
resetSendState,
updateSendErrors,
} from '../../ducks/send.duck'
import {
fetchBasicGasEstimates,
} from '../../ducks/gas.duck'
import {
calcGasTotal,
} from './send.utils.js'
@ -76,7 +79,7 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
updateAndSetGasTotal: ({
updateAndSetGasLimit: ({
blockGasLimit,
editingTransactionId,
gasLimit,
@ -89,7 +92,7 @@ function mapDispatchToProps (dispatch) {
data,
}) => {
!editingTransactionId
? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data }))
? dispatch(updateGasData({ gasPrice, recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data }))
: dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
},
updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {
@ -104,5 +107,6 @@ function mapDispatchToProps (dispatch) {
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
}
}

@ -8,7 +8,11 @@ const {
} = require('../../selectors')
const {
estimateGasPriceFromRecentBlocks,
calcGasTotal,
} = require('./send.utils')
import {
getFastPriceEstimateInHexWEI,
} from '../../selectors/custom-gas'
const selectors = {
accountsWithSendEtherInfoSelector,
@ -131,11 +135,11 @@ function getForceGasMin (state) {
}
function getGasLimit (state) {
return state.metamask.send.gasLimit
return state.metamask.send.gasLimit || '0'
}
function getGasPrice (state) {
return state.metamask.send.gasPrice
return state.metamask.send.gasPrice || getFastPriceEstimateInHexWEI(state)
}
function getGasPriceFromRecentBlocks (state) {
@ -143,7 +147,7 @@ function getGasPriceFromRecentBlocks (state) {
}
function getGasTotal (state) {
return state.metamask.send.gasTotal
return calcGasTotal(getGasLimit(state), getGasPrice(state))
}
function getPrimaryCurrency (state) {

@ -3,16 +3,23 @@ import assert from 'assert'
import proxyquire from 'proxyquire'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import timeout from '../../../../lib/test-timeout'
import SendHeader from '../send-header/send-header.container'
import SendContent from '../send-content/send-content.component'
import SendFooter from '../send-footer/send-footer.container'
const mockBasicGasEstimates = {
blockTime: 'mockBlockTime',
}
const propsMethodSpies = {
updateAndSetGasTotal: sinon.spy(),
updateAndSetGasLimit: sinon.spy(),
updateSendErrors: sinon.spy(),
updateSendTokenBalance: sinon.spy(),
resetSendState: sinon.spy(),
fetchBasicGasEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)),
fetchGasEstimates: sinon.spy(),
}
const utilsMethodStubs = {
getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
@ -37,6 +44,8 @@ describe('Send Component', function () {
blockGasLimit={'mockBlockGasLimit'}
conversionRate={10}
editingTransactionId={'mockEditingTransactionId'}
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
from={ { address: 'mockAddress', balance: 'mockBalance' } }
gasLimit={'mockGasLimit'}
gasPrice={'mockGasPrice'}
@ -50,7 +59,7 @@ describe('Send Component', function () {
showHexData={true}
tokenBalance={'mockTokenBalance'}
tokenContract={'mockTokenContract'}
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
updateAndSetGasLimit={propsMethodSpies.updateAndSetGasLimit}
updateSendErrors={propsMethodSpies.updateSendErrors}
updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
resetSendState={propsMethodSpies.resetSendState}
@ -63,7 +72,8 @@ describe('Send Component', function () {
utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
utilsMethodStubs.getAmountErrorObject.resetHistory()
utilsMethodStubs.getGasFeeErrorObject.resetHistory()
propsMethodSpies.updateAndSetGasTotal.resetHistory()
propsMethodSpies.fetchBasicGasEstimates.resetHistory()
propsMethodSpies.updateAndSetGasLimit.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
propsMethodSpies.updateSendTokenBalance.resetHistory()
})
@ -72,12 +82,20 @@ describe('Send Component', function () {
assert(SendTransactionScreen.prototype.componentDidMount.calledOnce)
})
describe('componentWillMount', () => {
it('should call this.updateGas', () => {
describe('componentDidMount', () => {
it('should call props.fetchBasicGasAndTimeEstimates', () => {
propsMethodSpies.fetchBasicGasEstimates.resetHistory()
assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0)
wrapper.instance().componentDidMount()
assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 1)
})
it('should call this.updateGas', async () => {
SendTransactionScreen.prototype.updateGas.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
wrapper.instance().componentWillMount()
wrapper.instance().componentDidMount()
await timeout(250)
assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
})
})
@ -271,12 +289,12 @@ describe('Send Component', function () {
})
describe('updateGas', () => {
it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => {
propsMethodSpies.updateAndSetGasLimit.resetHistory()
wrapper.instance().updateGas()
assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
assert.equal(propsMethodSpies.updateAndSetGasLimit.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0],
propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0],
{
blockGasLimit: 'mockBlockGasLimit',
editingTransactionId: 'mockEditingTransactionId',
@ -292,20 +310,20 @@ describe('Send Component', function () {
)
})
it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
it('should call updateAndSetGasLimit with the correct params if a to prop is passed', () => {
propsMethodSpies.updateAndSetGasLimit.resetHistory()
wrapper.setProps({ to: 'someAddress' })
wrapper.instance().updateGas()
assert.equal(
propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to,
propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to,
'someaddress',
)
})
it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
it('should call updateAndSetGasLimit with to set to lowercase if passed', () => {
propsMethodSpies.updateAndSetGasLimit.resetHistory()
wrapper.instance().updateGas({ to: '0xABC' })
assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc')
assert.equal(propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, '0xabc')
})
})

@ -94,7 +94,7 @@ describe('send container', () => {
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
})
describe('updateAndSetGasTotal()', () => {
describe('updateAndSetGasLimit()', () => {
const mockProps = {
blockGasLimit: 'mockBlockGasLimit',
editingTransactionId: '0x2',
@ -109,7 +109,7 @@ describe('send container', () => {
}
it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
mapDispatchToPropsObject.updateAndSetGasTotal(mockProps)
mapDispatchToPropsObject.updateAndSetGasLimit(mockProps)
assert(dispatchSpy.calledOnce)
assert.equal(
actionSpies.setGasTotal.getCall(0).args[0],
@ -118,14 +118,14 @@ describe('send container', () => {
})
it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
mapDispatchToPropsObject.updateAndSetGasTotal(
const { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps
mapDispatchToPropsObject.updateAndSetGasLimit(
Object.assign({}, mockProps, {editingTransactionId: false})
)
assert(dispatchSpy.calledOnce)
assert.deepEqual(
actionSpies.updateGasData.getCall(0).args[0],
{ selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data }
{ gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data }
)
})
})

@ -237,7 +237,7 @@ describe('send selectors', () => {
it('should return the send.gasTotal', () => {
assert.equal(
getGasTotal(mockState),
'0xb451dc41b578'
'a9ff56'
)
})
})

@ -1,3 +1,5 @@
@import './sidebar-content';
.sidebar-right-enter {
transition: transform 300ms ease-in-out;
transform: translateX(-100%);
@ -58,6 +60,11 @@
width: 408px;
left: calc(100% - 408px);
}
@media screen and (max-width: $break-small) {
width: 100%;
left: 0%;
}
}
.sidebar-overlay {

@ -0,0 +1,112 @@
.sidebar-left {
display: flex;
.gas-modal-page-container {
display: flex;
.page-container {
flex: 1;
max-width: 100%;
&__content {
display: flex;
overflow-y: initial;
}
@media screen and (max-width: $break-small) {
max-width: 344px;
min-height: auto;
}
@media screen and (min-width: $break-small) {
max-height: none;
}
}
.gas-price-chart {
margin-left: 10px;
&__root {
max-height: 160px !important;
}
}
.page-container__bottom {
display: flex;
flex-direction: column;
flex-flow: space-between;
height: 100%;
}
.page-container__content {
overflow-y: inherit;
}
.basic-tab-content {
height: auto;
margin-bottom: 0px;
border-bottom: 1px solid #d2d8dd;
flex: 1 1 70%;
@media screen and (max-width: $break-small) {
padding-left: 14px;
padding-bottom: 21px;
}
.gas-price-button-group--alt {
@media screen and (max-width: $break-small) {
max-width: 318px;
&__time-estimate {
font-size: 12px;
}
}
}
}
.advanced-tab {
@media screen and (min-width: $break-small) {
flex: 1 1 70%;
}
&__fee-chart {
height: 320px;
@media screen and (max-width: $break-small) {
height: initial;
}
}
&__fee-chart__speed-buttons {
bottom: 77px;
@media screen and (max-width: $break-small) {
display: none;
}
}
}
.gas-modal-content {
display: flex;
flex-direction: column;
width: 100%;
&__info-row-wrapper {
display: flex;
@media screen and (min-width: $break-small) {
flex: 1 1 30%;
}
}
&__info-row {
height: 170px;
@media screen and (max-width: $break-small) {
height: initial;
display: flex;
justify-content: center;
}
}
}
}
}

@ -3,14 +3,17 @@ import PropTypes from 'prop-types'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import WalletView from '../wallet-view'
import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
import CustomizeGas from '../gas-customization/gas-modal-page-container/'
export default class Sidebar extends Component {
static propTypes = {
sidebarOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
sidebarShouldClose: PropTypes.bool,
transitionName: PropTypes.string,
type: PropTypes.string,
sidebarProps: PropTypes.object,
};
renderOverlay () {
@ -18,19 +21,27 @@ export default class Sidebar extends Component {
}
renderSidebarContent () {
const { type } = this.props
const { type, sidebarProps = {} } = this.props
const { transaction = {} } = sidebarProps
switch (type) {
case WALLET_VIEW_SIDEBAR:
return <WalletView responsiveDisplayClassname={'sidebar-right' } />
case 'customize-gas':
return <div className={'sidebar-left'}><CustomizeGas transaction={transaction} /></div>
default:
return null
}
}
componentDidUpdate (prevProps) {
if (!prevProps.sidebarShouldClose && this.props.sidebarShouldClose) {
this.props.hideSidebar()
}
}
render () {
const { transitionName, sidebarOpen } = this.props
const { transitionName, sidebarOpen, sidebarShouldClose } = this.props
return (
<div>
@ -39,9 +50,9 @@ export default class Sidebar extends Component {
transitionEnterTimeout={300}
transitionLeaveTimeout={200}
>
{ sidebarOpen ? this.renderSidebarContent() : null }
{ sidebarOpen && !sidebarShouldClose ? this.renderSidebarContent() : null }
</ReactCSSTransitionGroup>
{ sidebarOpen ? this.renderOverlay() : null }
{ sidebarOpen && !sidebarShouldClose ? this.renderOverlay() : null }
</div>
)
}

@ -6,6 +6,7 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import Sidebar from '../sidebar.component.js'
import WalletView from '../../wallet-view'
import CustomizeGas from '../../gas-customization/gas-modal-page-container/'
const propsMethodSpies = {
hideSidebar: sinon.spy(),
@ -59,6 +60,14 @@ describe('Sidebar Component', function () {
assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
})
it('should render sidebar content with the correct props', () => {
wrapper.setProps({ type: 'customize-gas' })
renderSidebarContent = wrapper.instance().renderSidebarContent()
const renderedSidebarContent = shallow(renderSidebarContent)
assert(renderedSidebarContent.hasClass('sidebar-left'))
assert(renderedSidebarContent.childAt(0).is(CustomizeGas))
})
it('should not render with an unrecognized type', () => {
wrapper.setProps({ type: 'foobar' })
renderSidebarContent = wrapper.instance().renderSidebarContent()

@ -26,6 +26,7 @@ export default class TransactionListItemDetails extends PureComponent {
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
global.platform.openWindow({ url: etherscanUrl })
this.setState({ showTransactionDetails: true })
}

@ -27,6 +27,8 @@ export default class TransactionListItem extends PureComponent {
tokenData: PropTypes.object,
transaction: PropTypes.object,
value: PropTypes.string,
fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
}
state = {
@ -69,9 +71,12 @@ export default class TransactionListItem extends PureComponent {
}
resubmit () {
const { transaction: { id }, retryTransaction, history } = this.props
return retryTransaction(id)
.then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
const { transaction, retryTransaction, fetchBasicGasAndTimeEstimates, fetchGasEstimates } = this.props
fetchBasicGasAndTimeEstimates().then(basicEstimates => {
fetchGasEstimates(basicEstimates.blockTime)
}).then(() => {
retryTransaction(transaction)
})
}
renderPrimaryCurrency () {

@ -3,10 +3,16 @@ import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction, showModal } from '../../actions'
import { setSelectedToken, showModal, showSidebar } from '../../actions'
import { hexToDecimal } from '../../helpers/conversions.util'
import { getTokenData } from '../../helpers/transactions.util'
import { formatDate } from '../../util'
import {
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
setCustomGasPrice,
setCustomGasLimit,
} from '../../ducks/gas.duck'
const mapStateToProps = (state, ownProps) => {
const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
@ -23,8 +29,18 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
retryTransaction: (transaction) => {
dispatch(setCustomGasPrice(transaction.txParams.gasPrice))
dispatch(setCustomGasLimit(transaction.txParams.gas))
dispatch(showSidebar({
transitionName: 'sidebar-left',
type: 'customize-gas',
props: { transaction },
}))
},
showCancelModal: (transactionId, originalGasPrice) => {
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
},

@ -1,51 +0,0 @@
.gas-slider {
position: relative;
width: 313px;
&__input {
width: 317px;
margin-left: -2px;
z-index: 2;
}
input[type=range] {
-webkit-appearance: none !important;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none !important;
height: 26px;
width: 26px;
border: 2px solid #B8B8B8;
background-color: #FFFFFF;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
border-radius: 50%;
position: relative;
z-index: 10;
}
&__bar {
height: 6px;
width: 313px;
background: $alto;
display: flex;
justify-content: space-between;
position: absolute;
top: 11px;
z-index: 0;
}
&__low, &__high {
height: 6px;
width: 49px;
z-index: 1;
}
&__low {
background-color: $crimson;
}
&__high {
background-color: $caribbean-green;
}
}

@ -216,6 +216,7 @@ $wallet-view-bg: $alabaster;
.main-container-wrapper {
height: 100%;
width: 100%;
}
}

@ -684,6 +684,7 @@
display: flex;
align-items: center;
}
}
&__sliders-icon-container {
@ -917,6 +918,15 @@
display: none;
}
}
}
.advanced-gas-options-btn {
display: flex;
justify-content: flex-end;
font-size: 14px;
color: #2f9ae0;
cursor: pointer;
}
.sliders-icon-container {
@ -935,6 +945,23 @@
font-size: 1em;
}
.gas-fee-reset {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
border-radius: 4px;
background-color: #fff;
position: absolute;
right: 12px;
top: 14px;
cursor: pointer;
font-size: 1em;
font-size: 14px;
color: #2f9ae0;
font-family: Roboto;
}
.sliders-icon {
color: $curious-blue;
}

@ -18,6 +18,10 @@ body {
height: 100%;
margin: 0;
padding: 0;
@media screen and (max-width: $break-small) {
overflow-y: overlay;
}
}
html {

@ -56,6 +56,9 @@ $zumthor: #edf7ff;
$ecstasy: #f7861c;
$linen: #fdf4f4;
$oslo-gray: #8C8E94;
$polar: #fafcfe;
$blizzard-blue: #bfdef3;
$mischka: #dddee9;
/*
Z-Indicies

@ -0,0 +1,468 @@
import { clone, uniqBy, flatten } from 'ramda'
import BigNumber from 'bignumber.js'
import {
loadLocalStorageData,
saveLocalStorageData,
} from '../../lib/local-storage-helpers'
// Actions
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
customData: {
price: null,
limit: '0x5208',
},
basicEstimates: {
average: null,
fastestWait: null,
fastWait: null,
fast: null,
safeLowWait: null,
blockNum: null,
avgWait: null,
blockTime: null,
speed: null,
fastest: null,
safeLow: null,
},
basicEstimateIsLoading: true,
gasEstimatesLoading: true,
priceAndTimeEstimates: [],
basicPriceAndTimeEstimates: [],
priceAndTimeEstimatesLastRetrieved: 0,
basicPriceAndTimeEstimatesLastRetrieved: 0,
errors: {},
}
// Reducer
export default function reducer ({ gas: gasState = initState }, action = {}) {
const newState = clone(gasState)
switch (action.type) {
case BASIC_GAS_ESTIMATE_LOADING_STARTED:
return {
...newState,
basicEstimateIsLoading: true,
}
case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
return {
...newState,
basicEstimateIsLoading: false,
}
case GAS_ESTIMATE_LOADING_STARTED:
return {
...newState,
gasEstimatesLoading: true,
}
case GAS_ESTIMATE_LOADING_FINISHED:
return {
...newState,
gasEstimatesLoading: false,
}
case SET_BASIC_GAS_ESTIMATE_DATA:
return {
...newState,
basicEstimates: action.value,
}
case SET_CUSTOM_GAS_PRICE:
return {
...newState,
customData: {
...newState.customData,
price: action.value,
},
}
case SET_CUSTOM_GAS_LIMIT:
return {
...newState,
customData: {
...newState.customData,
limit: action.value,
},
}
case SET_CUSTOM_GAS_TOTAL:
return {
...newState,
customData: {
...newState.customData,
total: action.value,
},
}
case SET_PRICE_AND_TIME_ESTIMATES:
return {
...newState,
priceAndTimeEstimates: action.value,
}
case SET_CUSTOM_GAS_ERRORS:
return {
...newState,
errors: {
...newState.errors,
...action.value,
},
}
case SET_API_ESTIMATES_LAST_RETRIEVED:
return {
...newState,
priceAndTimeEstimatesLastRetrieved: action.value,
}
case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED:
return {
...newState,
basicPriceAndTimeEstimatesLastRetrieved: action.value,
}
case RESET_CUSTOM_DATA:
return {
...newState,
customData: clone(initState.customData),
}
case RESET_CUSTOM_GAS_STATE:
return clone(initState)
default:
return newState
}
}
// Action Creators
export function basicGasEstimatesLoadingStarted () {
return {
type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
}
}
export function basicGasEstimatesLoadingFinished () {
return {
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
}
}
export function gasEstimatesLoadingStarted () {
return {
type: GAS_ESTIMATE_LOADING_STARTED,
}
}
export function gasEstimatesLoadingFinished () {
return {
type: GAS_ESTIMATE_LOADING_FINISHED,
}
}
export function fetchBasicGasEstimates () {
return (dispatch) => {
dispatch(basicGasEstimatesLoadingStarted())
return fetch('https://dev.blockscale.net/api/gasexpress.json', {
'headers': {},
'referrer': 'https://dev.blockscale.net/api/',
'referrerPolicy': 'no-referrer-when-downgrade',
'body': null,
'method': 'GET',
'mode': 'cors'}
)
.then(r => r.json())
.then(({
safeLow,
standard: average,
fast,
fastest,
block_time: blockTime,
blockNum,
}) => {
const basicEstimates = {
safeLow,
average,
fast,
fastest,
blockTime,
blockNum,
}
dispatch(setBasicGasEstimateData(basicEstimates))
dispatch(basicGasEstimatesLoadingFinished())
return basicEstimates
})
}
}
export function fetchBasicGasAndTimeEstimates () {
return (dispatch, getState) => {
const {
basicPriceAndTimeEstimatesLastRetrieved,
basicPriceAndTimeEstimates,
} = getState().gas
const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || 0
dispatch(basicGasEstimatesLoadingStarted())
const promiseToFetch = Date.now() - timeLastRetrieved > 75000
? fetch('https://ethgasstation.info/json/ethgasAPI.json', {
'headers': {},
'referrer': 'http://ethgasstation.info/json/',
'referrerPolicy': 'no-referrer-when-downgrade',
'body': null,
'method': 'GET',
'mode': 'cors'}
)
.then(r => r.json())
.then(({
average: averageTimes10,
avgWait,
block_time: blockTime,
blockNum,
fast: fastTimes10,
fastest: fastestTimes10,
fastestWait,
fastWait,
safeLow: safeLowTimes10,
safeLowWait,
speed,
}) => {
const [average, fast, fastest, safeLow] = [
averageTimes10,
fastTimes10,
fastestTimes10,
safeLowTimes10,
].map(price => (new BigNumber(price)).div(10).toNumber())
const basicEstimates = {
average,
avgWait,
blockTime,
blockNum,
fast,
fastest,
fastestWait,
fastWait,
safeLow,
safeLowWait,
speed,
}
const timeRetrieved = Date.now()
dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved))
saveLocalStorageData(timeRetrieved, 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED')
saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES')
return basicEstimates
})
: Promise.resolve(basicPriceAndTimeEstimates.length
? basicPriceAndTimeEstimates
: loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES')
)
return promiseToFetch.then(basicEstimates => {
dispatch(setBasicGasEstimateData(basicEstimates))
dispatch(basicGasEstimatesLoadingFinished())
return basicEstimates
})
}
}
function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) {
higherY = new BigNumber(higherY, 10)
lowerY = new BigNumber(lowerY, 10)
higherX = new BigNumber(higherX, 10)
lowerX = new BigNumber(lowerX, 10)
xForExtrapolation = new BigNumber(xForExtrapolation, 10)
const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX))
const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated()
return Number(newTimeEstimate.toPrecision(10))
}
function getRandomArbitrary (min, max) {
min = new BigNumber(min, 10)
max = new BigNumber(max, 10)
const random = new BigNumber(String(Math.random()), 10)
return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10)
}
function calcMedian (list) {
const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2
return medianPos === Math.floor(medianPos)
? (list[medianPos - 1] + list[medianPos]) / 2
: list[Math.floor(medianPos)]
}
function quartiles (data) {
const lowerHalf = data.slice(0, Math.floor(data.length / 2))
const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1))
const median = calcMedian(data)
const lowerQuartile = calcMedian(lowerHalf)
const upperQuartile = calcMedian(upperHalf)
return {
median,
lowerQuartile,
upperQuartile,
}
}
function inliersByIQR (data, prop) {
const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d))
const IQR = upperQuartile - lowerQuartile
const lowerBound = lowerQuartile - 1.5 * IQR
const upperBound = upperQuartile + 1.5 * IQR
return data.filter(d => {
const value = prop ? d[prop] : d
return value >= lowerBound && value <= upperBound
})
}
export function fetchGasEstimates (blockTime) {
return (dispatch, getState) => {
const {
priceAndTimeEstimatesLastRetrieved,
priceAndTimeEstimates,
} = getState().gas
const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || 0
dispatch(gasEstimatesLoadingStarted())
const promiseToFetch = Date.now() - timeLastRetrieved > 75000
? fetch('https://ethgasstation.info/json/predictTable.json', {
'headers': {},
'referrer': 'http://ethgasstation.info/json/',
'referrerPolicy': 'no-referrer-when-downgrade',
'body': null,
'method': 'GET',
'mode': 'cors'}
)
.then(r => r.json())
.then(r => {
const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice }))
const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes)
const withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => {
const next = arr[i + 1]
if (!next) {
return [{ expectedWait, gasprice }]
} else {
const supplementalPrice = getRandomArbitrary(gasprice, next.gasprice)
const supplementalTime = extrapolateY({
higherY: next.expectedWait,
lowerY: expectedWait,
higherX: next.gasprice,
lowerX: gasprice,
xForExtrapolation: supplementalPrice,
})
const supplementalPrice2 = getRandomArbitrary(supplementalPrice, next.gasprice)
const supplementalTime2 = extrapolateY({
higherY: next.expectedWait,
lowerY: supplementalTime,
higherX: next.gasprice,
lowerX: supplementalPrice,
xForExtrapolation: supplementalPrice2,
})
return [
{ expectedWait, gasprice },
{ expectedWait: supplementalTime, gasprice: supplementalPrice },
{ expectedWait: supplementalTime2, gasprice: supplementalPrice2 },
]
}
}))
const withOutliersRemoved = inliersByIQR(withSupplementalTimeEstimates.slice(0).reverse(), 'expectedWait').reverse()
const timeMappedToSeconds = withOutliersRemoved.map(({ expectedWait, gasprice }) => {
const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toNumber()
return {
expectedTime,
gasprice: (new BigNumber(gasprice, 10).toNumber()),
}
})
const timeRetrieved = Date.now()
dispatch(setApiEstimatesLastRetrieved(timeRetrieved))
saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED')
saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES')
return timeMappedToSeconds
})
: Promise.resolve(priceAndTimeEstimates.length
? priceAndTimeEstimates
: loadLocalStorageData('GAS_API_ESTIMATES')
)
return promiseToFetch.then(estimates => {
dispatch(setPricesAndTimeEstimates(estimates))
dispatch(gasEstimatesLoadingFinished())
})
}
}
export function setBasicGasEstimateData (basicGasEstimateData) {
return {
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: basicGasEstimateData,
}
}
export function setPricesAndTimeEstimates (estimatedPricesAndTimes) {
return {
type: SET_PRICE_AND_TIME_ESTIMATES,
value: estimatedPricesAndTimes,
}
}
export function setCustomGasPrice (newPrice) {
return {
type: SET_CUSTOM_GAS_PRICE,
value: newPrice,
}
}
export function setCustomGasLimit (newLimit) {
return {
type: SET_CUSTOM_GAS_LIMIT,
value: newLimit,
}
}
export function setCustomGasTotal (newTotal) {
return {
type: SET_CUSTOM_GAS_TOTAL,
value: newTotal,
}
}
export function setCustomGasErrors (newErrors) {
return {
type: SET_CUSTOM_GAS_ERRORS,
value: newErrors,
}
}
export function setApiEstimatesLastRetrieved (retrievalTime) {
return {
type: SET_API_ESTIMATES_LAST_RETRIEVED,
value: retrievalTime,
}
}
export function setBasicApiEstimatesLastRetrieved (retrievalTime) {
return {
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
value: retrievalTime,
}
}
export function resetCustomGasState () {
return { type: RESET_CUSTOM_GAS_STATE }
}
export function resetCustomData () {
return { type: RESET_CUSTOM_DATA }
}

File diff suppressed because one or more lines are too long

@ -7,11 +7,14 @@ const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'
const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
fromDropdownOpen: false,
toDropdownOpen: false,
gasButtonGroupShown: true,
errors: {},
}
@ -43,6 +46,14 @@ export default function reducer ({ send: sendState = initState }, action = {}) {
...action.value,
},
})
case SHOW_GAS_BUTTON_GROUP:
return extend(newState, {
gasButtonGroupShown: true,
})
case HIDE_GAS_BUTTON_GROUP:
return extend(newState, {
gasButtonGroupShown: false,
})
case RESET_SEND_STATE:
return extend({}, initState)
default:
@ -67,6 +78,14 @@ export function closeToDropdown () {
return { type: CLOSE_TO_DROPDOWN }
}
export function showGasButtonGroup () {
return { type: SHOW_GAS_BUTTON_GROUP }
}
export function hideGasButtonGroup () {
return { type: HIDE_GAS_BUTTON_GROUP }
}
export function updateSendErrors (errorObject) {
return {
type: UPDATE_SEND_ERRORS,

@ -0,0 +1,544 @@
import assert from 'assert'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
const GasDuck = proxyquire('../gas.duck.js', {
'../../lib/local-storage-helpers': {
loadLocalStorageData: sinon.spy(),
saveLocalStorageData: sinon.spy(),
},
})
const {
basicGasEstimatesLoadingStarted,
basicGasEstimatesLoadingFinished,
setBasicGasEstimateData,
setCustomGasPrice,
setCustomGasLimit,
setCustomGasTotal,
setCustomGasErrors,
resetCustomGasState,
fetchBasicGasAndTimeEstimates,
gasEstimatesLoadingStarted,
gasEstimatesLoadingFinished,
setPricesAndTimeEstimates,
fetchGasEstimates,
setApiEstimatesLastRetrieved,
} = GasDuck
const GasReducer = GasDuck.default
describe('Gas Duck', () => {
let tempFetch
let tempDateNow
const mockEthGasApiResponse = {
average: 20,
avgWait: 'mockAvgWait',
block_time: 'mockBlock_time',
blockNum: 'mockBlockNum',
fast: 30,
fastest: 40,
fastestWait: 'mockFastestWait',
fastWait: 'mockFastWait',
safeLow: 10,
safeLowWait: 'mockSafeLowWait',
speed: 'mockSpeed',
}
const mockPredictTableResponse = [
{ expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' },
{ expectedTime: 200, expectedWait: 20, gasprice: 0.5, somethingElse: 'foobar' },
{ expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' },
{ expectedTime: 75, expectedWait: 7.5, gasprice: 1.5, somethingElse: 'foobar' },
{ expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' },
{ expectedTime: 35, expectedWait: 4.5, gasprice: 3, somethingElse: 'foobar' },
{ expectedTime: 34, expectedWait: 4.4, gasprice: 3.1, somethingElse: 'foobar' },
{ expectedTime: 25, expectedWait: 4.2, gasprice: 3.5, somethingElse: 'foobar' },
{ expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' },
{ expectedTime: 19, expectedWait: 3.9, gasprice: 4.1, somethingElse: 'foobar' },
{ expectedTime: 15, expectedWait: 3, gasprice: 7, somethingElse: 'foobar' },
{ expectedTime: 14, expectedWait: 2.9, gasprice: 7.1, somethingElse: 'foobar' },
{ expectedTime: 12, expectedWait: 2.5, gasprice: 8, somethingElse: 'foobar' },
{ expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' },
{ expectedTime: 9, expectedWait: 1.9, gasprice: 10.1, somethingElse: 'foobar' },
{ expectedTime: 5, expectedWait: 1, gasprice: 15, somethingElse: 'foobar' },
{ expectedTime: 4, expectedWait: 0.9, gasprice: 15.1, somethingElse: 'foobar' },
{ expectedTime: 2, expectedWait: 0.8, gasprice: 17, somethingElse: 'foobar' },
{ expectedTime: 1.1, expectedWait: 0.6, gasprice: 19.9, somethingElse: 'foobar' },
{ expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' },
]
const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => {
const dataToResolve = url.match(/ethgasAPI/)
? mockEthGasApiResponse
: mockPredictTableResponse
resolve({
json: () => new Promise(resolve => resolve(dataToResolve)),
})
}))
beforeEach(() => {
tempFetch = global.fetch
tempDateNow = global.Date.now
global.fetch = fetchStub
global.Date.now = () => 2000000
})
afterEach(() => {
global.fetch = tempFetch
global.Date.now = tempDateNow
})
const mockState = {
gas: {
mockProp: 123,
},
}
const initState = {
customData: {
price: null,
limit: '0x5208',
},
basicEstimates: {
average: null,
fastestWait: null,
fastWait: null,
fast: null,
safeLowWait: null,
blockNum: null,
avgWait: null,
blockTime: null,
speed: null,
fastest: null,
safeLow: null,
},
basicEstimateIsLoading: true,
errors: {},
gasEstimatesLoading: true,
priceAndTimeEstimates: [],
priceAndTimeEstimatesLastRetrieved: 0,
basicPriceAndTimeEstimates: [],
basicPriceAndTimeEstimatesLastRetrieved: 0,
}
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED'
const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED'
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED'
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE'
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'
const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS'
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL'
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES'
const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED'
const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED'
describe('GasReducer()', () => {
it('should initialize state', () => {
assert.deepEqual(
GasReducer({}),
initState
)
})
it('should return state unchanged if it does not match a dispatched actions type', () => {
assert.deepEqual(
GasReducer(mockState, {
type: 'someOtherAction',
value: 'someValue',
}),
Object.assign({}, mockState.gas)
)
})
it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: BASIC_GAS_ESTIMATE_LOADING_STARTED,
}),
Object.assign({basicEstimateIsLoading: true}, mockState.gas)
)
})
it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
}),
Object.assign({basicEstimateIsLoading: false}, mockState.gas)
)
})
it('should set gasEstimatesLoading to true when receiving a GAS_ESTIMATE_LOADING_STARTED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: GAS_ESTIMATE_LOADING_STARTED,
}),
Object.assign({gasEstimatesLoading: true}, mockState.gas)
)
})
it('should set gasEstimatesLoading to false when receiving a GAS_ESTIMATE_LOADING_FINISHED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: GAS_ESTIMATE_LOADING_FINISHED,
}),
Object.assign({gasEstimatesLoading: false}, mockState.gas)
)
})
it('should return a new object (and not just modify the existing state object)', () => {
assert.deepEqual(GasReducer(mockState), mockState.gas)
assert.notEqual(GasReducer(mockState), mockState.gas)
})
it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: { someProp: 'someData123' },
}),
Object.assign({basicEstimates: {someProp: 'someData123'} }, mockState.gas)
)
})
it('should set priceAndTimeEstimates when receiving a SET_PRICE_AND_TIME_ESTIMATES action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_PRICE_AND_TIME_ESTIMATES,
value: { someProp: 'someData123' },
}),
Object.assign({priceAndTimeEstimates: {someProp: 'someData123'} }, mockState.gas)
)
})
it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_CUSTOM_GAS_PRICE,
value: 4321,
}),
Object.assign({customData: {price: 4321} }, mockState.gas)
)
})
it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_CUSTOM_GAS_LIMIT,
value: 9876,
}),
Object.assign({customData: {limit: 9876} }, mockState.gas)
)
})
it('should set customData.total when receiving a SET_CUSTOM_GAS_TOTAL action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_CUSTOM_GAS_TOTAL,
value: 10000,
}),
Object.assign({customData: {total: 10000} }, mockState.gas)
)
})
it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_API_ESTIMATES_LAST_RETRIEVED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_API_ESTIMATES_LAST_RETRIEVED,
value: 1500000000000,
}),
Object.assign({ priceAndTimeEstimatesLastRetrieved: 1500000000000 }, mockState.gas)
)
})
it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_BASIC_API_ESTIMATES_LAST_RETRIEVED action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED,
value: 1700000000000,
}),
Object.assign({ basicPriceAndTimeEstimatesLastRetrieved: 1700000000000 }, mockState.gas)
)
})
it('should set errors when receiving a SET_CUSTOM_GAS_ERRORS action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: SET_CUSTOM_GAS_ERRORS,
value: { someError: 'error_error' },
}),
Object.assign({errors: {someError: 'error_error'} }, mockState.gas)
)
})
it('should return the initial state in response to a RESET_CUSTOM_GAS_STATE action', () => {
assert.deepEqual(
GasReducer(mockState, {
type: RESET_CUSTOM_GAS_STATE,
}),
Object.assign({}, initState)
)
})
})
describe('basicGasEstimatesLoadingStarted', () => {
it('should create the correct action', () => {
assert.deepEqual(
basicGasEstimatesLoadingStarted(),
{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED }
)
})
})
describe('basicGasEstimatesLoadingFinished', () => {
it('should create the correct action', () => {
assert.deepEqual(
basicGasEstimatesLoadingFinished(),
{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }
)
})
})
describe('fetchBasicGasAndTimeEstimates', () => {
const mockDistpatch = sinon.spy()
it('should call fetch with the expected params', async () => {
await fetchBasicGasAndTimeEstimates()(mockDistpatch, () => ({ gas: Object.assign(
{},
initState,
{ basicPriceAndTimeEstimatesLastRetrieved: 1000000 }
) }))
assert.deepEqual(
mockDistpatch.getCall(0).args,
[{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ]
)
assert.deepEqual(
global.fetch.getCall(0).args,
[
'https://ethgasstation.info/json/ethgasAPI.json',
{
'headers': {},
'referrer': 'http://ethgasstation.info/json/',
'referrerPolicy': 'no-referrer-when-downgrade',
'body': null,
'method': 'GET',
'mode': 'cors',
},
]
)
assert.deepEqual(
mockDistpatch.getCall(1).args,
[{ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ]
)
assert.deepEqual(
mockDistpatch.getCall(2).args,
[{
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: {
average: 2,
avgWait: 'mockAvgWait',
blockTime: 'mockBlock_time',
blockNum: 'mockBlockNum',
fast: 3,
fastest: 4,
fastestWait: 'mockFastestWait',
fastWait: 'mockFastWait',
safeLow: 1,
safeLowWait: 'mockSafeLowWait',
speed: 'mockSpeed',
},
}]
)
assert.deepEqual(
mockDistpatch.getCall(3).args,
[{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }]
)
})
})
describe('fetchGasEstimates', () => {
const mockDistpatch = sinon.spy()
beforeEach(() => {
mockDistpatch.resetHistory()
})
it('should call fetch with the expected params', async () => {
global.fetch.resetHistory()
await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
{},
initState,
{ priceAndTimeEstimatesLastRetrieved: 1000000 }
) }))
assert.deepEqual(
mockDistpatch.getCall(0).args,
[{ type: GAS_ESTIMATE_LOADING_STARTED} ]
)
assert.deepEqual(
global.fetch.getCall(0).args,
[
'https://ethgasstation.info/json/predictTable.json',
{
'headers': {},
'referrer': 'http://ethgasstation.info/json/',
'referrerPolicy': 'no-referrer-when-downgrade',
'body': null,
'method': 'GET',
'mode': 'cors',
},
]
)
assert.deepEqual(
mockDistpatch.getCall(1).args,
[{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }]
)
const { type: thirdDispatchCallType, value: priceAndTimeEstimateResult } = mockDistpatch.getCall(2).args[0]
assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES)
assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2)
assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100))
assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime))
assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice))
assert.deepEqual(
mockDistpatch.getCall(3).args,
[{ type: GAS_ESTIMATE_LOADING_FINISHED }]
)
})
it('should not call fetch if the estimates were retrieved < 75000 ms ago', async () => {
global.fetch.resetHistory()
await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign(
{},
initState,
{
priceAndTimeEstimatesLastRetrieved: Date.now(),
priceAndTimeEstimates: [{
expectedTime: '10',
expectedWait: 2,
gasprice: 50,
}],
}
) }))
assert.deepEqual(
mockDistpatch.getCall(0).args,
[{ type: GAS_ESTIMATE_LOADING_STARTED} ]
)
assert.equal(global.fetch.callCount, 0)
assert.deepEqual(
mockDistpatch.getCall(1).args,
[{
type: SET_PRICE_AND_TIME_ESTIMATES,
value: [
{
expectedTime: '10',
expectedWait: 2,
gasprice: 50,
},
],
}]
)
assert.deepEqual(
mockDistpatch.getCall(2).args,
[{ type: GAS_ESTIMATE_LOADING_FINISHED }]
)
})
})
describe('gasEstimatesLoadingStarted', () => {
it('should create the correct action', () => {
assert.deepEqual(
gasEstimatesLoadingStarted(),
{ type: GAS_ESTIMATE_LOADING_STARTED }
)
})
})
describe('gasEstimatesLoadingFinished', () => {
it('should create the correct action', () => {
assert.deepEqual(
gasEstimatesLoadingFinished(),
{ type: GAS_ESTIMATE_LOADING_FINISHED }
)
})
})
describe('setPricesAndTimeEstimates', () => {
it('should create the correct action', () => {
assert.deepEqual(
setPricesAndTimeEstimates('mockPricesAndTimeEstimates'),
{ type: SET_PRICE_AND_TIME_ESTIMATES, value: 'mockPricesAndTimeEstimates' }
)
})
})
describe('setBasicGasEstimateData', () => {
it('should create the correct action', () => {
assert.deepEqual(
setBasicGasEstimateData('mockBasicEstimatData'),
{ type: SET_BASIC_GAS_ESTIMATE_DATA, value: 'mockBasicEstimatData' }
)
})
})
describe('setCustomGasPrice', () => {
it('should create the correct action', () => {
assert.deepEqual(
setCustomGasPrice('mockCustomGasPrice'),
{ type: SET_CUSTOM_GAS_PRICE, value: 'mockCustomGasPrice' }
)
})
})
describe('setCustomGasLimit', () => {
it('should create the correct action', () => {
assert.deepEqual(
setCustomGasLimit('mockCustomGasLimit'),
{ type: SET_CUSTOM_GAS_LIMIT, value: 'mockCustomGasLimit' }
)
})
})
describe('setCustomGasTotal', () => {
it('should create the correct action', () => {
assert.deepEqual(
setCustomGasTotal('mockCustomGasTotal'),
{ type: SET_CUSTOM_GAS_TOTAL, value: 'mockCustomGasTotal' }
)
})
})
describe('setCustomGasErrors', () => {
it('should create the correct action', () => {
assert.deepEqual(
setCustomGasErrors('mockErrorObject'),
{ type: SET_CUSTOM_GAS_ERRORS, value: 'mockErrorObject' }
)
})
})
describe('setApiEstimatesLastRetrieved', () => {
it('should create the correct action', () => {
assert.deepEqual(
setApiEstimatesLastRetrieved(1234),
{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 1234 }
)
})
})
describe('resetCustomGasState', () => {
it('should create the correct action', () => {
assert.deepEqual(
resetCustomGasState(),
{ type: RESET_CUSTOM_GAS_STATE }
)
})
})
})

@ -6,6 +6,8 @@ import SendReducer, {
openToDropdown,
closeToDropdown,
updateSendErrors,
showGasButtonGroup,
hideGasButtonGroup,
} from '../send.duck.js'
describe('Send Duck', () => {
@ -18,6 +20,7 @@ describe('Send Duck', () => {
fromDropdownOpen: false,
toDropdownOpen: false,
errors: {},
gasButtonGroupShown: true,
}
const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN'
const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'
@ -25,6 +28,8 @@ describe('Send Duck', () => {
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'
const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'
describe('SendReducer()', () => {
it('should initialize state', () => {
@ -85,6 +90,24 @@ describe('Send Duck', () => {
)
})
it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => {
assert.deepEqual(
SendReducer(Object.assign({}, mockState, { gasButtonGroupShown: false }), {
type: SHOW_GAS_BUTTON_GROUP,
}),
Object.assign({gasButtonGroupShown: true}, mockState.send)
)
})
it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => {
assert.deepEqual(
SendReducer(mockState, {
type: HIDE_GAS_BUTTON_GROUP,
}),
Object.assign({gasButtonGroupShown: false}, mockState.send)
)
})
it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => {
const modifiedMockState = Object.assign({}, mockState, {
send: {
@ -145,6 +168,20 @@ describe('Send Duck', () => {
)
})
describe('showGasButtonGroup', () => {
assert.deepEqual(
showGasButtonGroup(),
{ type: SHOW_GAS_BUTTON_GROUP }
)
})
describe('hideGasButtonGroup', () => {
assert.deepEqual(
hideGasButtonGroup(),
{ type: HIDE_GAS_BUTTON_GROUP }
)
})
describe('updateSendErrors', () => {
assert.deepEqual(
updateSendErrors('mockErrorObject'),

@ -95,7 +95,7 @@ export function formatCurrency (value, currencyCode) {
const upperCaseCurrencyCode = currencyCode.toUpperCase()
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode })
? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode, style: 'currency' })
: value
}

@ -1,6 +1,6 @@
import ethUtil from 'ethereumjs-util'
import { conversionUtil } from '../conversion-util'
import { ETH, GWEI, WEI } from '../constants/common'
import { conversionUtil, addCurrencies } from '../conversion-util'
export function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16))
@ -82,3 +82,41 @@ export function getWeiHexFromDecimalValue ({
toDenomination: WEI,
})
}
export function addHexWEIsToDec (aHexWEI, bHexWEI) {
return addCurrencies(aHexWEI, bHexWEI, {
aBase: 16,
bBase: 16,
fromDenomination: 'WEI',
numberOfDecimals: 6,
})
}
export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) {
return conversionUtil(ethTotal, {
fromNumericBase: 'dec',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: convertedCurrency,
numberOfDecimals: 2,
conversionRate,
})
}
export function decGWEIToHexWEI (decGWEI) {
return conversionUtil(decGWEI, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
fromDenomination: 'GWEI',
toDenomination: 'WEI',
})
}
export function hexWEIToDecGWEI (decGWEI) {
return conversionUtil(decGWEI, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
toDenomination: 'GWEI',
})
}

@ -0,0 +1,3 @@
export function formatETHFee (ethFee) {
return ethFee + ' ETH'
}

@ -10,6 +10,7 @@ const reduceApp = require('./reducers/app')
const reduceLocale = require('./reducers/locale')
const reduceSend = require('./ducks/send.duck').default
import reduceConfirmTransaction from './ducks/confirm-transaction.duck'
import reduceGas from './ducks/gas.duck'
window.METAMASK_CACHED_LOG_STATE = null
@ -49,6 +50,8 @@ function rootReducer (state, action) {
state.confirmTransaction = reduceConfirmTransaction(state, action)
state.gas = reduceGas(state, action)
window.METAMASK_CACHED_LOG_STATE = state
return state
}

@ -52,6 +52,7 @@ function reduceApp (state, action) {
isOpen: false,
transitionName: '',
type: '',
props: {},
},
alertOpen: false,
alertMessage: null,

@ -35,6 +35,7 @@ const selectors = {
getTotalUnapprovedCount,
preferencesSelector,
getMetaMaskAccounts,
getCurrentEthBalance,
}
module.exports = selectors
@ -137,6 +138,10 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
function getCurrentEthBalance (state) {
return getCurrentAccountWithSendEtherInfo(state).balance
}
function getGasIsLoading (state) {
return state.appState.gasIsLoading
}

@ -0,0 +1,270 @@
import { pipe, partialRight } from 'ramda'
import {
conversionUtil,
multiplyCurrencies,
} from '../conversion-util'
import {
getCurrentCurrency,
} from '../selectors'
import {
formatCurrency,
} from '../helpers/confirm-transaction/util'
import {
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
} from '../helpers/conversions.util'
import {
formatETHFee,
} from '../helpers/formatters'
import {
calcGasTotal,
} from '../components/send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
const selectors = {
formatTimeEstimate,
getAveragePriceEstimateInHexWEI,
getFastPriceEstimateInHexWEI,
getBasicGasEstimateLoadingStatus,
getBasicGasEstimateBlockTime,
getCustomGasErrors,
getCustomGasLimit,
getCustomGasPrice,
getCustomGasTotal,
getDefaultActiveButtonIndex,
getEstimatedGasPrices,
getEstimatedGasTimes,
getGasEstimatesLoadingStatus,
getPriceAndTimeEstimates,
getRenderableBasicEstimateData,
getRenderableEstimateDataForSmallButtonsFromGWEI,
priceEstimateToWei,
}
module.exports = selectors
const NUMBER_OF_DECIMALS_SM_BTNS = 5
function getCustomGasErrors (state) {
return state.gas.errors
}
function getCustomGasLimit (state) {
return state.gas.customData.limit
}
function getCustomGasPrice (state) {
return state.gas.customData.price
}
function getCustomGasTotal (state) {
return state.gas.customData.total
}
function getBasicGasEstimateLoadingStatus (state) {
return state.gas.basicEstimateIsLoading
}
function getGasEstimatesLoadingStatus (state) {
return state.gas.gasEstimatesLoading
}
function getPriceAndTimeEstimates (state) {
return state.gas.priceAndTimeEstimates
}
function getEstimatedGasPrices (state) {
return getPriceAndTimeEstimates(state).map(({ gasprice }) => gasprice)
}
function getEstimatedGasTimes (state) {
return getPriceAndTimeEstimates(state).map(({ expectedTime }) => expectedTime)
}
function getAveragePriceEstimateInHexWEI (state) {
const averagePriceEstimate = state.gas.basicEstimates.average
return getGasPriceInHexWei(averagePriceEstimate || '0x0')
}
function getFastPriceEstimateInHexWEI (state) {
const fastPriceEstimate = state.gas.basicEstimates.fast
return getGasPriceInHexWei(fastPriceEstimate || '0x0')
}
function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) {
return gasButtonInfo.findIndex(({ priceInHexWei }) => {
return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice)
})
}
function getBasicGasEstimateBlockTime (state) {
return state.gas.basicEstimates.blockTime
}
function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) {
return conversionUtil(calcGasTotal(gasLimit, estimate), {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'GWEI',
numberOfDecimals,
})
}
function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) {
return pipe(
x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]),
formatETHFee
)(estimate, gasLimit)
}
function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) {
return pipe(
x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
partialRight(basicPriceEstimateToETHTotal, [gasLimit]),
partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]),
partialRight(formatCurrency, [convertedCurrency])
)(estimate, gasLimit, convertedCurrency, conversionRate)
}
function getTimeEstimateInSeconds (blockWaitEstimate) {
return multiplyCurrencies(blockWaitEstimate, 60, {
toNumericBase: 'dec',
multiplicandBase: 10,
multiplierBase: 10,
numberOfDecimals: 1,
})
}
function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) {
const minutes = Math.floor(totalSeconds / 60)
const seconds = Math.floor(totalSeconds % 60)
if (!minutes && !seconds) {
return '...'
}
let symbol = '~'
if (greaterThanMax) {
symbol = '< '
} else if (lessThanMin) {
symbol = '> '
}
const formattedMin = `${minutes ? minutes + ' min' : ''}`
const formattedSec = `${seconds ? seconds + ' sec' : ''}`
const formattedCombined = formattedMin && formattedSec
? `${symbol}${formattedMin} ${formattedSec}`
: symbol + [formattedMin, formattedSec].find(t => t)
return formattedCombined
}
function getRenderableTimeEstimate (blockWaitEstimate) {
return pipe(
getTimeEstimateInSeconds,
formatTimeEstimate
)(blockWaitEstimate)
}
function priceEstimateToWei (priceEstimate) {
return conversionUtil(priceEstimate, {
fromNumericBase: 'hex',
toNumericBase: 'hex',
fromDenomination: 'GWEI',
toDenomination: 'WEI',
numberOfDecimals: 9,
})
}
function getGasPriceInHexWei (price) {
return pipe(
x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
priceEstimateToWei,
addHexPrefix
)(price)
}
function getRenderableBasicEstimateData (state) {
if (getBasicGasEstimateLoadingStatus(state)) {
return []
}
const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state)
const conversionRate = state.metamask.conversionRate
const currentCurrency = getCurrentCurrency(state)
const {
gas: {
basicEstimates: {
safeLow,
fast,
fastest,
safeLowWait,
fastestWait,
fastWait,
},
},
} = state
return [
{
labelKey: 'fastest',
feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate),
feeInSecondaryCurrency: getRenderableEthFee(fastest, gasLimit),
timeEstimate: fastestWait && getRenderableTimeEstimate(fastestWait),
priceInHexWei: getGasPriceInHexWei(fastest),
},
{
labelKey: 'fast',
feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate),
feeInSecondaryCurrency: getRenderableEthFee(fast, gasLimit),
timeEstimate: fastWait && getRenderableTimeEstimate(fastWait),
priceInHexWei: getGasPriceInHexWei(fast),
},
{
labelKey: 'slow',
feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate),
feeInSecondaryCurrency: getRenderableEthFee(safeLow, gasLimit),
timeEstimate: safeLowWait && getRenderableTimeEstimate(safeLowWait),
priceInHexWei: getGasPriceInHexWei(safeLow),
},
]
}
function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {
if (getBasicGasEstimateLoadingStatus(state)) {
return []
}
const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state)
const conversionRate = state.metamask.conversionRate
const currentCurrency = getCurrentCurrency(state)
const {
gas: {
basicEstimates: {
safeLow,
fast,
fastest,
},
},
} = state
return [
{
labelKey: 'fastest',
feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate),
feeInPrimaryCurrency: getRenderableEthFee(fastest, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
priceInHexWei: getGasPriceInHexWei(fastest, true),
},
{
labelKey: 'fast',
feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate),
feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
priceInHexWei: getGasPriceInHexWei(fast, true),
},
{
labelKey: 'slow',
feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate),
feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true),
priceInHexWei: getGasPriceInHexWei(safeLow, true),
},
]
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save