Fix merge conflicts. Modify send workflow

feature/default_network_editable
Alexander Tseung 7 years ago
commit 35875863d2
  1. 78
      .circleci/config.yml
  2. 4
      .eslintrc
  3. 2
      app/_locales/de/messages.json
  4. 3
      app/_locales/en/messages.json
  5. 2
      app/_locales/es/messages.json
  6. 2
      app/_locales/hn/messages.json
  7. 2
      app/_locales/nl/messages.json
  8. 418
      app/_locales/ru/messages.json
  9. 2
      app/_locales/sl/messages.json
  10. 2
      app/_locales/th/messages.json
  11. 2
      app/_locales/zh_TW/messages.json
  12. 5
      app/scripts/controllers/transactions.js
  13. 11
      app/scripts/lib/tx-gas-utils.js
  14. 52
      development/metamaskbot-build-announce.js
  15. 128
      development/states/tx-list-items.js
  16. 101
      development/verify-locale-strings.js
  17. 9
      docs/translating-guide.md
  18. 6
      gulpfile.js
  19. 1
      old-ui/app/app.js
  20. 1
      old-ui/app/components/buy-button-subview.js
  21. 1
      old-ui/app/components/qr-code.js
  22. 4
      old-ui/app/components/range-slider.js
  23. 7
      old-ui/app/components/transaction-list-item.js
  24. 2
      old-ui/app/config.js
  25. 3399
      package-lock.json
  26. 14
      package.json
  27. 6
      test/e2e/metamask.spec.js
  28. 61
      test/integration/lib/tx-list-items.js
  29. 18
      test/screens/func.js
  30. 230
      test/screens/new-ui.js
  31. 6
      test/unit/tx-controller-test.js
  32. 24
      test/unit/tx-gas-util-test.js
  33. 5
      ui/app/actions.js
  34. 5
      ui/app/app.js
  35. 3
      ui/app/components/customize-gas-modal/index.js
  36. 8
      ui/app/components/dropdowns/network-dropdown.js
  37. 5
      ui/app/components/identicon.js
  38. 29
      ui/app/components/pages/home.js
  39. 110
      ui/app/components/pending-tx/confirm-send-ether.js
  40. 107
      ui/app/components/pending-tx/confirm-send-token.js
  41. 12
      ui/app/components/send/send-utils.js
  42. 21
      ui/app/components/tx-list-item.js
  43. 5
      ui/app/components/tx-list.js
  44. 25
      ui/app/conf-tx.js
  45. 36
      ui/app/css/itcss/components/confirm.scss
  46. 17
      ui/app/send-v2.js
  47. 10
      ui/index.js
  48. 1196
      yarn.lock

@ -17,8 +17,17 @@ workflows:
- prep-deps-npm - prep-deps-npm
- test-e2e: - test-e2e:
requires: requires:
- prep-deps-npm
- prep-build
- job-screens:
requires:
- prep-deps-npm
- prep-build - prep-build
- job-announce:
requires:
- prep-deps-npm - prep-deps-npm
- prep-build
- job-screens
- test-unit: - test-unit:
requires: requires:
- prep-deps-npm - prep-deps-npm
@ -45,6 +54,7 @@ workflows:
- test-lint - test-lint
- test-unit - test-unit
- test-e2e - test-e2e
- job-screens
- test-integration-mascara-chrome - test-integration-mascara-chrome
- test-integration-mascara-firefox - test-integration-mascara-firefox
- test-integration-flat-chrome - test-integration-flat-chrome
@ -98,32 +108,7 @@ jobs:
key: build-cache-{{ .Revision }} key: build-cache-{{ .Revision }}
paths: paths:
- dist - dist
- store_artifacts: - builds
path: dist/mascara
destination: builds/mascara
- store_artifacts:
path: builds
destination: builds
- run:
name: build:announce
command: |
CIRCLE_PR_NUMBER="${CIRCLE_PR_NUMBER:-${CIRCLE_PULL_REQUEST##*/}}"
SHORT_SHA1=$(echo $CIRCLE_SHA1 | cut -c 1-7)
BUILD_LINK_BASE="https://$CIRCLE_BUILD_NUM-42009758-gh.circle-artifacts.com/0/builds"
VERSION=$(node -p 'require("./dist/chrome/manifest.json").version')
MASCARA="$BUILD_LINK_BASE/mascara/home.html"
CHROME="$BUILD_LINK_BASE/metamask-chrome-$VERSION.zip"
FIREFOX="$BUILD_LINK_BASE/metamask-firefox-$VERSION.zip"
OPERA="$BUILD_LINK_BASE/metamask-opera-$VERSION.zip"
EDGE="$BUILD_LINK_BASE/metamask-edge-$VERSION.zip"
COMMENT_MAIN="Builds ready [$SHORT_SHA1]: [mascara][mascara], [chrome][chrome], [firefox][firefox], [edge][edge], [opera][opera]"
COMMENT_LINKS="[mascara]:$MASCARA\n[chrome]:$CHROME\n[firefox]:$FIREFOX\n[opera]:$OPERA\n[edge]:$EDGE\n"
COMMENT_BODY="$COMMENT_MAIN\n\n$COMMENT_LINKS"
JSON_PAYLOAD="{\"body\":\"$COMMENT_BODY\"}"
POST_COMMENT_URI="https://api.github.com/repos/metamask/metamask-extension/issues/$CIRCLE_PR_NUMBER/comments"
echo "Announcement:\n$COMMENT_BODY"
echo "Posting to $POST_COMMENT_URI"
curl -d "$JSON_PAYLOAD" -H "Authorization: token $GITHUB_COMMENT_TOKEN" $POST_COMMENT_URI
prep-scss: prep-scss:
docker: docker:
@ -171,6 +156,47 @@ jobs:
path: test-artifacts path: test-artifacts
destination: test-artifacts destination: test-artifacts
job-screens:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: Test
command: npm run test:screens
- save_cache:
key: job-screens-{{ .Revision }}
paths:
- test-artifacts
job-announce:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
key: job-screens-{{ .Revision }}
- store_artifacts:
path: dist/mascara
destination: builds/mascara
- store_artifacts:
path: builds
destination: builds
- store_artifacts:
path: test-artifacts
destination: test-artifacts
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js
test-unit: test-unit:
docker: docker:
- image: circleci/node:8-browsers - image: circleci/node:8-browsers

@ -29,7 +29,8 @@
"plugins": [ "plugins": [
"mocha", "mocha",
"chai", "chai",
"react" "react",
"json"
], ],
"globals": { "globals": {
@ -41,6 +42,7 @@
}, },
"rules": { "rules": {
"no-restricted-globals": ["error", "event"],
"accessor-pairs": 2, "accessor-pairs": 2,
"arrow-spacing": [2, { "before": true, "after": true }], "arrow-spacing": [2, { "before": true, "after": true }],
"block-spacing": [2, "always"], "block-spacing": [2, "always"],

@ -232,7 +232,7 @@
"done": { "done": {
"message": "Fertig" "message": "Fertig"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Statelogs herunterladen" "message": "Statelogs herunterladen"
}, },
"dropped": { "dropped": {

@ -826,6 +826,9 @@
"transactions": { "transactions": {
"message": "transactions" "message": "transactions"
}, },
"transactionError": {
"message": "Transaction Error. Exception thrown in contract code."
},
"transactionMemo": { "transactionMemo": {
"message": "Transaction memo (optional)" "message": "Transaction memo (optional)"
}, },

@ -247,7 +247,7 @@
"done": { "done": {
"message": "Completo" "message": "Completo"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Descargar logs de estado" "message": "Descargar logs de estado"
}, },
"dropped": { "dropped": {

@ -223,7 +223,7 @@
"done": { "done": {
"message": "सपनन" "message": "सपनन"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "रय लग डउनलड कर" "message": "रय लग डउनलड कर"
}, },
"edit": { "edit": {

@ -223,7 +223,7 @@
"done": { "done": {
"message": "Gedaan" "message": "Gedaan"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Staatslogboeken downloaden" "message": "Staatslogboeken downloaden"
}, },
"edit": { "edit": {

@ -3,13 +3,13 @@
"message": "Принять" "message": "Принять"
}, },
"account": { "account": {
"message": "Аккаунт" "message": "Счет"
}, },
"accountDetails": { "accountDetails": {
"message": "Детали Аккаунта" "message": "Детали счета"
}, },
"accountName": { "accountName": {
"message": "Имя Пользователя" "message": "Название счета"
}, },
"address": { "address": {
"message": "Адрес" "message": "Адрес"
@ -21,13 +21,13 @@
"message": "Добавить токен" "message": "Добавить токен"
}, },
"addTokens": { "addTokens": {
"message": "Добавить Токены" "message": "Добавить токены"
}, },
"amount": { "amount": {
"message": "Количество" "message": "Сумма"
}, },
"amountPlusGas": { "amountPlusGas": {
"message": "Количество + газ" "message": "Сумма + газ"
}, },
"appDescription": { "appDescription": {
"message": "Расширение браузера для Ethereum", "message": "Расширение браузера для Ethereum",
@ -37,11 +37,14 @@
"message": "MetaMask", "message": "MetaMask",
"description": "The name of the application" "description": "The name of the application"
}, },
"approved": {
"message": "Одобрена"
},
"attemptingConnect": { "attemptingConnect": {
"message": "Попытка подключиться к блокчейн сети." "message": "Попытка подключиться к блокчейн сети."
}, },
"attributions": { "attributions": {
"message": "Опознания" "message": "Атрибуция"
}, },
"available": { "available": {
"message": "Доступный" "message": "Доступный"
@ -53,13 +56,13 @@
"message": "Баланс:" "message": "Баланс:"
}, },
"balances": { "balances": {
"message": "Ваши балансы" "message": "Ваш баланс"
}, },
"balanceIsInsufficientGas": { "balanceIsInsufficientGas": {
"message": "Недостаточный баланс для текущего объема газа" "message": "Недостаточный баланс для текущего объема газа"
}, },
"beta": { "beta": {
"message": "БЕТА" "message": "BETA"
}, },
"betweenMinAndMax": { "betweenMinAndMax": {
"message": "должно быть больше или равно $1 и меньше или равно $2.", "message": "должно быть больше или равно $1 и меньше или равно $2.",
@ -69,10 +72,10 @@
"message": "Использовать Blockies Identicon" "message": "Использовать Blockies Identicon"
}, },
"borrowDharma": { "borrowDharma": {
"message": "Заимствовать с Dharma (бета)" "message": "Взять в долг на Dharma (Beta)"
}, },
"builtInCalifornia": { "builtInCalifornia": {
"message": "MetaMask спроектирован и построен в Калифорнии." "message": "MetaMask спроектирован и разработан в Калифорнии."
}, },
"buy": { "buy": {
"message": "Купить" "message": "Купить"
@ -81,7 +84,10 @@
"message": "Купить на Coinbase" "message": "Купить на Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase - самый популярный в мире способ купить и продать биткойн, ethereum и litecoin." "message": "Биржа Coinbase – это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin."
},
"ok": {
"message": "ОК"
}, },
"cancel": { "cancel": {
"message": "Отмена" "message": "Отмена"
@ -95,14 +101,17 @@
"confirm": { "confirm": {
"message": "Подтвердить" "message": "Подтвердить"
}, },
"confirmed": {
"message": "Подтверждена"
},
"confirmContract": { "confirmContract": {
"message": "Подтвердить Контракт" "message": "Подтвердить контракт"
}, },
"confirmPassword": { "confirmPassword": {
"message": "Подтвердите Пароль" "message": "Подтвердите пароль"
}, },
"confirmTransaction": { "confirmTransaction": {
"message": "Подтвердить Транзакцию" "message": "Подтвердить транзакцию"
}, },
"continue": { "continue": {
"message": "Продолжить" "message": "Продолжить"
@ -114,7 +123,7 @@
"message": "Развертывание контракта" "message": "Развертывание контракта"
}, },
"conversionProgress": { "conversionProgress": {
"message": "Выполняется конверсия" "message": "Выполняется конвертация"
}, },
"copiedButton": { "copiedButton": {
"message": "Скопировано" "message": "Скопировано"
@ -126,7 +135,7 @@
"message": "Скопировано!" "message": "Скопировано!"
}, },
"copiedSafe": { "copiedSafe": {
"message": "Я скопировал его где-то в безопасности" "message": "Я скопировал это в безопасное место"
}, },
"copy": { "copy": {
"message": "Скопировать" "message": "Скопировать"
@ -138,29 +147,32 @@
"message": " Скопировать " "message": " Скопировать "
}, },
"copyPrivateKey": { "copyPrivateKey": {
"message": "Это ваш личный ключ (нажмите, чтобы скопировать)" "message": "Это ваш закрытый ключ (нажмите, чтобы скопировать)"
}, },
"create": { "create": {
"message": "Создать" "message": "Создать"
}, },
"createAccount": { "createAccount": {
"message": "Регистрация" "message": "Создать счет"
}, },
"createDen": { "createDen": {
"message": "Создать" "message": "Создать"
}, },
"crypto": { "crypto": {
"message": "Крипто", "message": "Криптовалюта",
"description": "Exchange type (cryptocurrencies)" "description": "Exchange type (cryptocurrencies)"
}, },
"currentConversion": { "currentConversion": {
"message": "Текущая конверсия" "message": "Текущая конвертация"
}, },
"currentNetwork": { "currentNetwork": {
"message": "Текущая сеть" "message": "Текущая сеть"
}, },
"customGas": { "customGas": {
"message": "Настроить Газ" "message": "Настроить газ"
},
"customToken": {
"message": "Пользовательский токен"
}, },
"customize": { "customize": {
"message": "Настроить" "message": "Настроить"
@ -169,112 +181,115 @@
"message": "Пользовательский RPC" "message": "Пользовательский RPC"
}, },
"decimalsMustZerotoTen": { "decimalsMustZerotoTen": {
"message": "Десятичные числа должны быть не менее 0, и не более 36." "message": "Количество десятичных разрядов должно быть минимум 0 и максимум 36."
}, },
"decimal": { "decimal": {
"message": "Десятичные значения точности" "message": "Количество десятичных разрядов"
}, },
"defaultNetwork": { "defaultNetwork": {
"message": "Сеть по умолчанию для транзакций Ether - это Main Net." "message": "Основная сеть Ethereum – это сеть по умолчанию для Ether транзакций."
}, },
"denExplainer": { "denExplainer": {
"message": "Ваш DEN - это ваше зашифрованное паролем хранилище в MetaMask." "message": "DEN – это зашифрованное паролем хранилище внутри MetaMask."
}, },
"deposit": { "deposit": {
"message": "Депозит" "message": "Пополнить"
}, },
"depositBTC": { "depositBTC": {
"message": "Депозит BTC по адресу:" "message": "Отправьте ваш BTC на адрес ниже:"
}, },
"depositCoin": { "depositCoin": {
"message": "Депозит $1 по указанному ниже адресу", "message": "Отправьте ваш $1 на адрес ниже",
"description": "Tells the user what coin they have selected to deposit with shapeshift" "description": "Tells the user what coin they have selected to deposit with shapeshift"
}, },
"depositEth": { "depositEth": {
"message": "Депозит Eth" "message": "Пополнить Eth"
}, },
"depositEther": { "depositEther": {
"message": "Депозит Эфир" "message": "Пополнить Ether"
}, },
"depositFiat": { "depositFiat": {
"message": "Депозит с деньгами" "message": "Пополнить деньгами"
}, },
"depositFromAccount": { "depositFromAccount": {
"message": "Депозит с другого счета" "message": "Пополнить с другого счета"
}, },
"depositShapeShift": { "depositShapeShift": {
"message": "Депозит с ShapeShift" "message": "Пополнить через ShapeShift"
}, },
"depositShapeShiftExplainer": { "depositShapeShiftExplainer": {
"message": "Если у вас есть другие крипторесурсы, вы можете торговать и вносить Эфир непосредственно в кошелек MetaMask. Нет необходимости в аккаунте." "message": "Если у вас есть другие криптовалюты, вы можете торговать и пополнять Ether напрямую в ваш MetaMask кошелек. Нет необходимости в счете."
}, },
"details": { "details": {
"message": "Детали" "message": "Детали"
}, },
"directDeposit": { "directDeposit": {
"message": "Прямой Депозит" "message": "Прямое пополнение"
}, },
"directDepositEther": { "directDepositEther": {
"message": "Прямой Депозит Эфира" "message": "Прямое пополнение Ether"
}, },
"directDepositEtherExplainer": { "directDepositEtherExplainer": {
"message": "Если у вас уже есть Эфир, самый быстрый способ получить Эфир в вашем новом кошельке это прямым депозитом." "message": "Если у вас уже есть Ether, то самый быстрый способ получить Ether в ваш новый кошелек – это прямое пополнение."
}, },
"done": { "done": {
"message": "Готово" "message": "Готово"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Загрузить логи статус" "message": "Скачать журнал состояния"
},
"dropped": {
"message": "Отброшена"
}, },
"edit": { "edit": {
"message": "Редактировать" "message": "Редактировать"
}, },
"editAccountName": { "editAccountName": {
"message": "Изменить Имя Аккаунта" "message": "Редактировать название счета"
}, },
"emailUs": { "emailUs": {
"message": "Свяжитесь с нами по электронной почте!" "message": "Свяжитесь с нами по электронной почте!"
}, },
"encryptNewDen": { "encryptNewDen": {
"message": "Шифруйте новый DEN" "message": "Зашифровать ваш новый DEN"
}, },
"enterPassword": { "enterPassword": {
"message": "Введите пароль" "message": "Введите пароль"
}, },
"enterPasswordConfirm": { "enterPasswordConfirm": {
"message": "Введите свой пароль для подтверждения" "message": "Введите ваш пароль для подтверждения"
}, },
"etherscanView": { "etherscanView": {
"message": "Просмотреть аккаунт на Etherscan" "message": "Просмотреть счет на Etherscan"
}, },
"exchangeRate": { "exchangeRate": {
"message": "Обменный Курс" "message": "Обменный курс"
}, },
"exportPrivateKey": { "exportPrivateKey": {
"message": "Экспорт закрытого ключа" "message": "Экспортировать закрытый ключ"
}, },
"exportPrivateKeyWarning": { "exportPrivateKeyWarning": {
"message": "Экспорт секретных ключей на свой страх и риск." "message": "Вы экспортируете закрытые ключи на свой страх и риск."
}, },
"failed": { "failed": {
"message": "Не смогли" "message": "Неудачна"
}, },
"fiat": { "fiat": {
"message": "Бумажные деньги", "message": "Валюта",
"description": "Exchange type" "description": "Exchange type"
}, },
"fileImportFail": { "fileImportFail": {
"message": "Ошибка импорта файлов? Кликните сюда!", "message": "Не работает импорт файла? Нажмите тут!",
"description": "Helps user import their account from a JSON file" "description": "Helps user import their account from a JSON file"
}, },
"followTwitter": { "followTwitter": {
"message": "Следуйте за нами на Twitter" "message": "Читайте нас в Twitter"
}, },
"from": { "from": {
"message": "Из" "message": "Отправитель"
}, },
"fromToSame": { "fromToSame": {
"message": "От и до адреса не могут быть одинаковым" "message": "Адрес отправителя и получателя не могут быть одинаковыми"
}, },
"fromShapeShift": { "fromShapeShift": {
"message": "Из ShapeShift" "message": "Из ShapeShift"
@ -284,37 +299,37 @@
"description": "Short indication of gas cost" "description": "Short indication of gas cost"
}, },
"gasFee": { "gasFee": {
"message": "Плата за Газ" "message": "Комиссия за газ"
}, },
"gasLimit": { "gasLimit": {
"message": "Газовый Предел" "message": "Лимит газа"
}, },
"gasLimitCalculation": { "gasLimitCalculation": {
"message": "Мы рассчитываем предполагаемый предел газа на основе коэффициентов успешности сети." "message": "Мы расчитываем предлагаемый лимит газа на основании успешных ставок в сети."
}, },
"gasLimitRequired": { "gasLimitRequired": {
"message": "Требуется ограничение на Газ" "message": "Установите лимит газа"
}, },
"gasLimitTooLow": { "gasLimitTooLow": {
"message": "Предел газа должен быть не менее 21000" "message": "Лимит газа должен быть как минимум 21000"
}, },
"generatingSeed": { "generatingSeed": {
"message": "Создание Семян ..." "message": "Генерируем фразу..."
}, },
"gasPrice": { "gasPrice": {
"message": "Цена на Газ (GWEI)" "message": "Цена за газ (GWEI)"
}, },
"gasPriceCalculation": { "gasPriceCalculation": {
"message": "Мы вычисляем предлагаемые цены на газ на основе коэффициентов успеха сети." "message": "Мы расчитываем предлагаемые цены за газ на основании успешных ставок в сети."
}, },
"gasPriceRequired": { "gasPriceRequired": {
"message": "Требуется цена на Газ" "message": "Установите стоимость газа"
}, },
"getEther": { "getEther": {
"message": "Получить Эфир" "message": "Получить Ether"
}, },
"getEtherFromFaucet": { "getEtherFromFaucet": {
"message": "Получите Эфир из крана $1", "message": "Получить Ether из крана для $1",
"description": "Displays network name for Ether faucet" "description": "Displays network name for Ether faucet"
}, },
"greaterThanMin": { "greaterThanMin": {
@ -322,14 +337,14 @@
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"here": { "here": {
"message": "здесь", "message": "тут",
"description": "as in -click here- for more information (goes with troubleTokenBalances)" "description": "as in -click here- for more information (goes with troubleTokenBalances)"
}, },
"hereList": { "hereList": {
"message": "Вот список!!!!" "message": "Вот список!!!!"
}, },
"hide": { "hide": {
"message": прятать" "message": крыть"
}, },
"hideToken": { "hideToken": {
"message": "Скрыть токен" "message": "Скрыть токен"
@ -338,33 +353,33 @@
"message": "Скрыть токен?" "message": "Скрыть токен?"
}, },
"howToDeposit": { "howToDeposit": {
"message": "Как бы вы хотели поместить Эфир?" "message": "Как бы вы хотели пополнить Ether?"
}, },
"holdEther": { "holdEther": {
"message": "Это позволяет вам использовать эфир и токены и служит мостом для децентрализованных приложений." "message": "Позволяет вам хранить ether и токены и служит в качестве моста в децентрализированные приложения."
}, },
"import": { "import": {
"message": "Импортировать", "message": "Импортировать",
"description": "Button to import an account from a selected file" "description": "Button to import an account from a selected file"
}, },
"importAccount": { "importAccount": {
"message": "Импорт Аккаунта" "message": "Импортировать счет"
}, },
"importAccountMsg": { "importAccountMsg": {
"message": " Импортированные аккаунты не будут связаны с вашей первоначально созданным аккаунтом MetaMask. Подробнее о импортированных аккаунтах " "message":" Импортированные счета не будут ассоциированы с вашей ключевой фразой, созданной MetaMask. Узнать больше про импорт счетов "
}, },
"importAnAccount": { "importAnAccount": {
"message": "Импортировать аккаунт" "message": "Импортировать аккаунт"
}, },
"importDen": { "importDen": {
"message": "Импорт существующих DEN" "message": "Импортировать существующий DEN"
}, },
"imported": { "imported": {
"message": "Импортирован", "message": "Импортирован",
"description": "status showing that an account has been fully loaded into the keyring" "description": "status showing that an account has been fully loaded into the keyring"
}, },
"infoHelp": { "infoHelp": {
"message": "Информация и Помощь" "message": "Информация и помощь"
}, },
"insufficientFunds": { "insufficientFunds": {
"message": "Недостаточно средств." "message": "Недостаточно средств."
@ -373,35 +388,44 @@
"message": "Недостаточно токенов." "message": "Недостаточно токенов."
}, },
"invalidAddress": { "invalidAddress": {
"message": "Недействительный адрес" "message": "Неверный адрес"
}, },
"invalidAddressRecipient": { "invalidAddressRecipient": {
"message": "Недопустимый адрес получателя." "message": "Неверный адрес получателя"
}, },
"invalidGasParams": { "invalidGasParams": {
"message": "Недопустимые параметры Газа" "message": "Неверные параметры газа"
}, },
"invalidInput": { "invalidInput": {
"message": "Неправильный ввод." "message": "Неверный ввод."
}, },
"invalidRequest": { "invalidRequest": {
"message": "Неверный Запрос" "message": "Неверный запрос"
}, },
"invalidRPC": { "invalidRPC": {
"message": "Недопустимый URI RPC" "message": "Неверный RPC URI"
}, },
"jsonFail": { "jsonFail": {
"message": "Что-то пошло не так. Убедитесь, что ваш файл JSON правильно отформатирован." "message": "Что-то пошло не так. Убедитесь, что ваш JSON файл правильно отформатирован."
}, },
"jsonFile": { "jsonFile": {
"message": "Файл JSON", "message": "JSON файл",
"description": "format for importing an account" "description": "format for importing an account"
}, },
"keepTrackTokens": {
"message": "Следите за купленными вами токенами с помощью аккаунта MetaMask."
},
"kovan": { "kovan": {
"message": "Kovan тестовая сеть" "message": "Тестовая сеть Kovan"
}, },
"knowledgeDataBase": { "knowledgeDataBase": {
"message": "Посетите нашу базу знаний" "message": "Посмотрите нашу Базу Знаний"
},
"max": {
"message": "Максимум"
},
"learnMore": {
"message": "Узнать больше."
}, },
"lessThanMax": { "lessThanMax": {
"message": "должно быть меньше или равно $1.", "message": "должно быть меньше или равно $1.",
@ -410,8 +434,11 @@
"likeToAddTokens": { "likeToAddTokens": {
"message": "Вы хотите добавить эти токены?" "message": "Вы хотите добавить эти токены?"
}, },
"links": {
"message": "Ссылки"
},
"limit": { "limit": {
"message": "Предел" "message": "Лимит"
}, },
"loading": { "loading": {
"message": "Загрузка..." "message": "Загрузка..."
@ -420,19 +447,19 @@
"message": "Загрузка токенов..." "message": "Загрузка токенов..."
}, },
"localhost": { "localhost": {
"message": "Локальный адрес 8545" "message": "Localhost 8545"
}, },
"login": { "login": {
"message": "Авторизоваться" "message": "Вход"
}, },
"logout": { "logout": {
"message": "Выйти" "message": "Выход"
}, },
"loose": { "loose": {
"message": "Рыхлый" "message": "Несвязанный"
}, },
"loweCaseWords": { "loweCaseWords": {
"message": "семенные слова имеют только символы нижнего регистра" "message": "ключевая фраза может содержать только символы нижнего регистра"
}, },
"mainnet": { "mainnet": {
"message": "Основная сеть Ethereum" "message": "Основная сеть Ethereum"
@ -441,19 +468,19 @@
"message": "Сообщение" "message": "Сообщение"
}, },
"metamaskDescription": { "metamaskDescription": {
"message": "MetaMask - это безопасное хранилище для Ethereum." "message": "MetaMask – безопасный кошелек для Ethereum."
}, },
"min": { "min": {
"message": "Минимум" "message": "Минимум"
}, },
"myAccounts": { "myAccounts": {
"message": "Мои Аккаунты" "message": "Мои счета"
}, },
"mustSelectOne": { "mustSelectOne": {
"message": "Необходимо выбрать не менее 1 токена." "message": "Необходимо выбрать как минимум 1 токен."
}, },
"needEtherInWallet": { "needEtherInWallet": {
"message": "Чтобы взаимодействовать с децентрализованными приложениями с помощью MetaMask, вам понадобится Эфир в вашем кошельке." "message": "Для взаимодействия с децентрализованными приложениями с помощью MetaMask нужен Ether в вашем кошельке."
}, },
"needImportFile": { "needImportFile": {
"message": "Вы должны выбрать файл для импорта.", "message": "Вы должны выбрать файл для импорта.",
@ -464,60 +491,60 @@
"description": "Password and file needed to import an account" "description": "Password and file needed to import an account"
}, },
"negativeETH": { "negativeETH": {
"message": "Невозможно отправить отрицательные количества ETH." "message": "Невозможно отправить отрицательную сумму ETH."
}, },
"networks": { "networks": {
"message": "Сети" "message": "Сети"
}, },
"newAccount": { "newAccount": {
"message": "Новый Аккаунт" "message": "Новый счет"
}, },
"newAccountNumberName": { "newAccountNumberName": {
"message": "Аккаунт $1", "message": "Счет $1",
"description": "Default name of next account to be created on create account screen" "description": "Default name of next account to be created on create account screen"
}, },
"newContract": { "newContract": {
"message": "Новый Контракт" "message": "Новый контракт"
}, },
"newPassword": { "newPassword": {
"message": "Новый пароль (мин. 8 символов)" "message": "Новый пароль (мин. 8 символов)"
}, },
"newRecipient": { "newRecipient": {
"message": "Новый Получатель" "message": "Новый получатель"
}, },
"newRPC": { "newRPC": {
"message": "Новый URL-адрес RPC" "message": "Новый RPC URL"
}, },
"next": { "next": {
"message": "Далее" "message": "Далее"
}, },
"noAddressForName": { "noAddressForName": {
"message": "Для этого имени не задан адрес." "message": "Дла этого названия не установлен адрес."
}, },
"noDeposits": { "noDeposits": {
"message": "Не было получено никаких депозитов" "message": "Пополнения не получены"
}, },
"noTransactionHistory": { "noTransactionHistory": {
"message": "Нет истории транзакций." "message": "Нет истории транзакций."
}, },
"noTransactions": { "noTransactions": {
"message": "Нет Транзакций" "message": "Нет транзакций"
}, },
"notStarted": { "notStarted": {
"message": "Не Начался" "message": "Не запущен"
}, },
"oldUI": { "oldUI": {
"message": "Старый Интерфейс" "message": "Старая версия интерфейса"
}, },
"oldUIMessage": { "oldUIMessage": {
"message": "Вы вернулись к старому интерфейсу. Вы можете вернуться к новому с помощью опции в раскрывающемся меню в правом верхнем углу." "message": "Вы вернулись к старой версии интерфейса пользователя. Вы можете переключиться на новую с помощью опции выпадающего меню в правом верхнем углу."
}, },
"or": { "or": {
"message": "или", "message": "или",
"description": "choice between creating or importing a new account" "description": "choice between creating or importing a new account"
}, },
"passwordCorrect": { "passwordCorrect": {
"message": "Убедитесь, что ваш пароль правильный." "message": "Убедитесь, что ваш пароль верный."
}, },
"passwordMismatch": { "passwordMismatch": {
"message": "пароли не совпадают", "message": "пароли не совпадают",
@ -528,27 +555,30 @@
"description": "in password creation process, the password is not long enough to be secure" "description": "in password creation process, the password is not long enough to be secure"
}, },
"pastePrivateKey": { "pastePrivateKey": {
"message": "Вставьте свою личную строку:", "message": "Вставьте ваш закрытый ключ тут:",
"description": "For importing an account from a private key" "description": "For importing an account from a private key"
}, },
"pasteSeed": { "pasteSeed": {
"message": "Вставьте здесь свою семенную фразу!" "message": "Вставьте вашу ключевую фразу!"
}, },
"personalAddressDetected": { "personalAddressDetected": {
"message": "Персональный адрес обнаружен. Введите адрес контракта токена." "message": "Обнаружен персональный адрес. Введите адрес контракта токена."
}, },
"pleaseReviewTransaction": { "pleaseReviewTransaction": {
"message": "Проверьте транзакцию." "message": "Проверьте транзакцию."
}, },
"popularTokens": {
"message": "Популярные токены"
},
"privacyMsg": { "privacyMsg": {
"message": "Политика Конфиденциальности" "message": "Политика конфиденциальности"
}, },
"privateKey": { "privateKey": {
"message": "Закрытый ключ", "message": "Закрытый ключ",
"description": "select this type of file to use to import an account" "description": "select this type of file to use to import an account"
}, },
"privateKeyWarning": { "privateKeyWarning": {
"message": "Предупреждение: никогда не раскрывайте этот ключ. Любой, у кого есть ваши личные ключи, может украсть любые активы, хранящиеся в вашем аккаунте." "message": "Предупреждение: Никогда не раскрывайте этот ключ. Любой, у кого есть ваши закрытые ключи, может украсть любые активы, хранящиеся на счету."
}, },
"privateNetwork": { "privateNetwork": {
"message": "Частная сеть" "message": "Частная сеть"
@ -557,126 +587,165 @@
"message": "Показать QR-код" "message": "Показать QR-код"
}, },
"readdToken": { "readdToken": {
"message": "Вы можете добавить этот токен в будущем, перейдя в “Добавить токен” в меню параметров вашего аккаунта." "message": "Вы можете в будущем добавить обратно этот токен, выбрав пункт меню “Добавить токен”."
}, },
"readMore": { "readMore": {
"message": "Подробнее читайте здесь." "message": "Узнать больше тут."
}, },
"readMore2": { "readMore2": {
"message": "Прочитайте больше." "message": "Узнать больше."
}, },
"receive": { "receive": {
"message": "Получить" "message": "Получить"
}, },
"recipientAddress": { "recipientAddress": {
"message": "Адрес Получателя" "message": "Адрес получателя"
}, },
"refundAddress": { "refundAddress": {
"message": "Ваш Адрес Возврата" "message": "Ваш адрес для возврата средств"
}, },
"rejected": { "rejected": {
"message": "Отклонено" "message": "Отклонена"
}, },
"resetAccount": { "resetAccount": {
"message": "Сбросить аккаунт" "message": "Сбросить аккаунт"
}, },
"restoreFromSeed": { "restoreFromSeed": {
"message": "Восстановить от семенной фразы" "message": "Восстановить из ключевой фразы"
},
"restoreVault": {
"message": "Восстановить кошелек"
}, },
"required": { "required": {
"message": "Необходимо" "message": "Обязательное поле"
}, },
"retryWithMoreGas": { "retryWithMoreGas": {
"message": "Повторите попытку с более высокой ценой на газ здесь" "message": "Повторите попытку с большей ценой за газRetry with a higher gas price here"
},
"walletSeed": {
"message": "Ключевая фраза кошелька"
}, },
"revealSeedWords": { "revealSeedWords": {
"message": "Раскрыть семенные слова" "message": "Показать ключевую фразу"
}, },
"revealSeedWordsWarning": { "revealSeedWordsWarning": {
"message": "Не восстанавливайте семенные слова в общественном месте! Эти слова могут использоваться для кражи всех ваших аккаунтах." "message": "Не восстанавливайте ключевую фразу в общественном месте! Она может быть использована для кражи всех ваших счетов."
}, },
"revert": { "revert": {
"message": "Откат" "message": "Восстановить"
}, },
"rinkeby": { "rinkeby": {
"message": "Rinkeby тестовая сеть" "message": "Тестовая сеть Rinkeby"
}, },
"ropsten": { "ropsten": {
"message": "Ropsten тестовая сеть" "message": "Тестовая сеть Ropsten"
},
"currentRpc": {
"message": "Current RPC"
},
"connectingToMainnet": {
"message": "Соединение с основной сетью Ethereum"
},
"connectingToRopsten": {
"message": "Соединение с тестовой сетью Ropsten"
},
"connectingToKovan": {
"message": "Соединение с тестовой сетью Kovan"
},
"connectingToRinkeby": {
"message": "Соединение с тестовой сетью Rinkeby"
},
"connectingToUnknown": {
"message": "Соединение с неизвестной сетью"
}, },
"sampleAccountName": { "sampleAccountName": {
"message": "Например, Мой новый аккаунт", "message": "Например, Мой новый счет",
"description": "Help user understand concept of adding a human-readable name to their account" "description": "Help user understand concept of adding a human-readable name to their account"
}, },
"save": { "save": {
"message": "Сохранить" "message": "Сохранить"
}, },
"saveAsFile": { "saveAsFile": {
"message": "Сохранить как Файл", "message": "Сохранить в виде файла",
"description": "Account export process" "description": "Account export process"
}, },
"saveSeedAsFile": { "saveSeedAsFile": {
"message": "Сохранить Семенные Слова Как Файл" "message": "Сохранить ключевую фразу в виде файла"
}, },
"search": { "search": {
"message": "Поиск" "message": "Поиск"
}, },
"secretPhrase": { "secretPhrase": {
"message": "Введите свою секретную двенадцатисловную фразу здесь, чтобы восстановить хранилище." "message": "Введите вашу ключевую фразу из 12 слов, чтобы восстановить кошелек."
},
"newPassword8Chars": {
"message": "Новый пароль (мин. 8 символов)"
}, },
"seedPhraseReq": { "seedPhraseReq": {
"message": "семенные фразы длиной 12 слов" "message": "ключевые фразы имеют длину 12 слов"
}, },
"select": { "select": {
"message": "Выбрать" "message": "Выбрать"
}, },
"selectCurrency": { "selectCurrency": {
"message": "Выберите Валюту" "message": "Выберите валюту"
}, },
"selectService": { "selectService": {
"message": "Выберите Сервис" "message": "Выберите сервис"
}, },
"selectType": { "selectType": {
"message": "Выберите Тип" "message": "Выберите тип"
}, },
"send": { "send": {
"message": "Послать" "message": "Отправить"
}, },
"sendETH": { "sendETH": {
"message": "Отправить ETH" "message": "Отправить ETH"
}, },
"sendTokens": { "sendTokens": {
"message": "Отправить Токены" "message": "Отправить токены"
},
"onlySendToEtherAddress": {
"message": "Отправляйте ETH только на Ethereum адреса."
},
"searchTokens": {
"message": "Поиск токенов"
}, },
"sendTokensAnywhere": { "sendTokensAnywhere": {
"message": "Отправить Токены кому-либо с аккаунтом Ethereum" "message": "Отправить токены любому, у кого есть счет Ethereum"
}, },
"settings": { "settings": {
"message": "Настройки" "message": "Настройки"
}, },
"info": {
"message": "Информация"
},
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "Купить с помощью Shapeshift" "message": "Купить через Shapeshift"
}, },
"showPrivateKeys": { "showPrivateKeys": {
"message": "Показать приватные ключи" "message": "Показать закрытые ключи"
}, },
"showQRCode": { "showQRCode": {
"message": "Показать QR-код" "message": "Показать QR-код"
}, },
"sign": { "sign": {
"message": "Знак" "message": "Подпись"
},
"signed": {
"message": "Подписана"
}, },
"signMessage": { "signMessage": {
"message": "Нодписать сообщение" "message": "Подписать сообщение"
}, },
"signNotice": { "signNotice": {
"message": "Подписание этого сообщения может иметь \nопасные побочные эффекты. Только подписывайте сообщения \nс сайтов, которым вы полностью доверяете своим аккаунтом. Этот опасный метод будет удален в будущей версии." "message": "Подпись этого сообщения может иметь \nопасные побочные эффекты. Подписывайте только сообщения \nс сайтов, которым вы полностью доверяете свой аккаунт. Этот опасный метод будет удален в будущей версии."
}, },
"sigRequest": { "sigRequest": {
"message": "Запрос на подпись" "message": "Запрос подписи"
}, },
"sigRequested": { "sigRequested": {
"message": "Подпись Запрошена" "message": "Подпись запрошена"
}, },
"spaceBetween": { "spaceBetween": {
"message": "между словами может быть только пробел" "message": "между словами может быть только пробел"
@ -685,53 +754,59 @@
"message": "Статус" "message": "Статус"
}, },
"stateLogs": { "stateLogs": {
"message": "Логи Статуса" "message": "Журнал состояния"
}, },
"stateLogsDescription": { "stateLogsDescription": {
"message": "Логи статуса содержат ваши общедоступные адреса и отправленные транзакции." "message": "Журнал состояния содержит ваши публичные адреса счетов и совершенные транзакции."
},
"stateLogError": {
"message": "Ошибка при получении журнала состояния."
}, },
"submit": { "submit": {
"message": "Отправить" "message": "Отправить"
}, },
"submitted": {
"message": "Отправлена"
},
"supportCenter": { "supportCenter": {
"message": "Посетите наш Центр поддержки" "message": ерейти в наш Центр поддержки"
}, },
"symbolBetweenZeroTen": { "symbolBetweenZeroTen": {
"message": "Символ должен быть от 0 до 10 символов." "message": "Символ должен быть от 0 до 10 символов."
}, },
"takesTooLong": { "takesTooLong": {
"message": "Занимает слишком долго?" "message": "Слишком долго?"
}, },
"terms": { "terms": {
"message": "Условия Эксплуатации" "message": "Условия пользования"
}, },
"testFaucet": { "testFaucet": {
"message": "Тестовый Кран" "message": "Тестовый кран"
}, },
"to": { "to": {
"message": "К" "message": "Получатель: "
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "$1 в ETH через ShapeShift", "message": "$1 в ETH через ShapeShift",
"description": "system will fill in deposit type in start of message" "description": "system will fill in deposit type in start of message"
}, },
"tokenAddress": { "tokenAddress": {
"message": "Адрес Токена" "message": "Адрес токена"
}, },
"tokenAlreadyAdded": { "tokenAlreadyAdded": {
"message": "Токен уже добавлен." "message": "Токен уже был добавлен."
}, },
"tokenBalance": { "tokenBalance": {
"message": "Баланс Вашых Tокенов:" "message": "Баланс ваших токенов:"
}, },
"tokenSelection": { "tokenSelection": {
"message": "Поиск токенов или выбор из нашего списка популярных токенов." "message": "Поищите токен или выберите из нашего списка популярных токенов."
}, },
"tokenSymbol": { "tokenSymbol": {
"message": "Символ Токена" "message": "Символ токена"
}, },
"tokenWarning1": { "tokenWarning1": {
"message": "Следите за токенами, которые вы купили с помощью аккаунта MetaMask. Если вы купили токены, используя другой аккаунт, эти токены здесь не появятся." "message": "Отслеживаются токены, купленные на счет в MetaMask. Если вы купили токены, используя другой счет, такие токены не будут тут отображены."
}, },
"total": { "total": {
"message": "Всего" "message": "Всего"
@ -740,35 +815,38 @@
"message": "транзакции" "message": "транзакции"
}, },
"transactionMemo": { "transactionMemo": {
"message": "Транзакционная записка (необязательно)" "message": "Транзакционные данные (необязательный)"
}, },
"transactionNumber": { "transactionNumber": {
"message": "Номер Транзакции" "message": "Номер транзакции"
}, },
"transfers": { "transfers": {
"message": "Переводы" "message": "Переводы"
}, },
"troubleTokenBalances": { "troubleTokenBalances": {
"message": "У нас были проблемы с загрузкой ваших токенов. Вы можете просмотреть их ", "message": "Возникли проблемы при загрузке балансов токенов. Вы можете посмотреть их ",
"description": "Followed by a link (here) to view token balances" "description": "Followed by a link (here) to view token balances"
}, },
"twelveWords": { "twelveWords": {
"message": "Эти 12 слов - единственный способ восстановить ваши учетные записи MetaMask.\nСохраните их где-нибудь в безопасности и в тайне." "message": "Эти 12 слов являются единственной возможностью восстановить ваши счета в MetaMask.\nСохраните из в надежном секретном месте."
}, },
"typePassword": { "typePassword": {
"message": "Введите Пароль" "message": "Введите пароль"
}, },
"uiWelcome": { "uiWelcome": {
"message": "Добро пожаловать в новый интерфейс (бета-версия)" "message": "Новый интерфейс (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Теперь вы используете новый интерфейс Metamask. Осмотритесь, попробуйте новые функции, такие как отправку токенов, и сообщите нам, есть ли у вас какие-либо проблемы." "message": "Теперь вы используете новый интерфейс пользователя MetaMask. Осмотритесь, попробуйте новые функции, например, отправить токены и, если возникнут проблемы, сообщите нам."
},
"unapproved": {
"message": "Не одобрена"
}, },
"unavailable": { "unavailable": {
"message": "Недоступен" "message": "Недоступный"
}, },
"unknown": { "unknown": {
"message": "Неизвестный" "message": "Неизвестно"
}, },
"unknownNetwork": { "unknownNetwork": {
"message": "Неизвестная частная сеть" "message": "Неизвестная частная сеть"
@ -787,19 +865,19 @@
"message": "Используется различными клиентами" "message": "Используется различными клиентами"
}, },
"useOldUI": { "useOldUI": {
"message": "Использовать старый интерфейс" "message": "Использовать старый интерфейс пользователя"
}, },
"validFileImport": { "validFileImport": {
"message": ы должны выбрать действительный файл для импорта." "message": ам нужно выбрать правильный файл для импорта."
}, },
"vaultCreated": { "vaultCreated": {
"message": "Создано хранилище" "message": "Кошелек был создан"
}, },
"viewAccount": { "viewAccount": {
"message": "Посмотреть аккаунт" "message": "Посмотреть счет"
}, },
"visitWebSite": { "visitWebSite": {
"message": осетите наш сайт" "message": ерейти на наш сайт"
}, },
"warning": { "warning": {
"message": "Предупреждение" "message": "Предупреждение"
@ -811,7 +889,7 @@
"message": "Что это?" "message": "Что это?"
}, },
"yourSigRequested": { "yourSigRequested": {
"message": "Ваша подпись запрашивается" "message": "Запрашивается ваша подпись"
}, },
"youSign": { "youSign": {
"message": "Вы подписываете" "message": "Вы подписываете"

@ -223,7 +223,7 @@
"done": { "done": {
"message": "Končano" "message": "Končano"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Prenesi state dnevnike" "message": "Prenesi state dnevnike"
}, },
"edit": { "edit": {

@ -223,7 +223,7 @@
"done": { "done": {
"message": "เสรจสน" "message": "เสรจสน"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "ดาวนโหลดลอกสถานะ" "message": "ดาวนโหลดลอกสถานะ"
}, },
"edit": { "edit": {

@ -235,7 +235,7 @@
"done": { "done": {
"message": "完成" "message": "完成"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "下載狀態紀錄" "message": "下載狀態紀錄"
}, },
"dropped": { "dropped": {

@ -187,12 +187,12 @@ module.exports = class TransactionController extends EventEmitter {
// validate // validate
await this.txGasUtil.validateTxParams(txParams) await this.txGasUtil.validateTxParams(txParams)
// construct txMeta // construct txMeta
const txMeta = this.txStateManager.generateTxMeta({txParams}) let txMeta = this.txStateManager.generateTxMeta({txParams})
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
try { try {
await this.addTxDefaults(txMeta) txMeta = await this.addTxDefaults(txMeta)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -215,6 +215,7 @@ module.exports = class TransactionController extends EventEmitter {
} }
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
if (txParams.to === null) delete txParams.to
// set gasLimit // set gasLimit
return await this.txGasUtil.analyzeGasUsage(txMeta) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }

@ -52,7 +52,9 @@ module.exports = class TxGasUtil {
// if recipient has no code, gas is 21k max: // if recipient has no code, gas is 21k max:
const recipient = txParams.to const recipient = txParams.to
const hasRecipient = Boolean(recipient) const hasRecipient = Boolean(recipient)
const code = await this.query.getCode(recipient) let code
if (recipient) code = await this.query.getCode(recipient)
if (hasRecipient && (!code || code === '0x')) { if (hasRecipient && (!code || code === '0x')) {
txParams.gas = SIMPLE_GAS_COST txParams.gas = SIMPLE_GAS_COST
txMeta.simpleSend = true // Prevents buffer addition txMeta.simpleSend = true // Prevents buffer addition
@ -100,6 +102,7 @@ module.exports = class TxGasUtil {
} }
async validateTxParams (txParams) { async validateTxParams (txParams) {
this.validateFrom(txParams)
this.validateRecipient(txParams) this.validateRecipient(txParams)
if ('value' in txParams) { if ('value' in txParams) {
const value = txParams.value.toString() const value = txParams.value.toString()
@ -112,6 +115,12 @@ module.exports = class TxGasUtil {
} }
} }
} }
validateFrom (txParams) {
if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
validateRecipient (txParams) { validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) { if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) { if (txParams.data) {

@ -0,0 +1,52 @@
#!/usr/bin/env node
const request = require('request-promise')
const { version } = require('../dist/chrome/manifest.json')
const GITHUB_COMMENT_TOKEN = process.env.GITHUB_COMMENT_TOKEN
console.log('GITHUB_COMMENT_TOKEN', GITHUB_COMMENT_TOKEN)
const CIRCLE_PULL_REQUEST = process.env.CIRCLE_PULL_REQUEST
console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST)
const CIRCLE_SHA1 = process.env.CIRCLE_SHA1
console.log('CIRCLE_SHA1', CIRCLE_SHA1)
const CIRCLE_BUILD_NUM = process.env.CIRCLE_BUILD_NUM
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM)
const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop()
const SHORT_SHA1 = CIRCLE_SHA1.slice(0,7)
const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0`
const MASCARA = `${BUILD_LINK_BASE}/builds/mascara/home.html`
const CHROME = `${BUILD_LINK_BASE}/builds/metamask-chrome-${version}.zip`
const FIREFOX = `${BUILD_LINK_BASE}/builds/metamask-firefox-${version}.zip`
const EDGE = `${BUILD_LINK_BASE}/builds/metamask-edge-${version}.zip`
const OPERA = `${BUILD_LINK_BASE}/builds/metamask-opera-${version}.zip`
const WALKTHROUGH = `${BUILD_LINK_BASE}/test-artifacts/screens/walkthrough%20%28en%29.gif`
const commentBody = `
<details>
<summary>
Builds ready [${SHORT_SHA1}]:
<a href="${MASCARA}">mascara</a>,
<a href="${CHROME}">chrome</a>,
<a href="${FIREFOX}">firefox</a>,
<a href="${EDGE}">edge</a>,
<a href="${OPERA}">opera</a>
</summary>
<image src="${WALKTHROUGH}">
</details>
`
const JSON_PAYLOAD = JSON.stringify({ body: commentBody })
const POST_COMMENT_URI = `https://api.github.com/repos/metamask/metamask-extension/issues/${CIRCLE_PR_NUMBER}/comments`
console.log(`Announcement:\n${commentBody}`)
console.log(`Posting to: ${POST_COMMENT_URI}`)
request({
method: 'POST',
uri: POST_COMMENT_URI,
body: JSON_PAYLOAD,
headers: {
'User-Agent': 'metamaskbot',
'Authorization': `token ${GITHUB_COMMENT_TOKEN}`,
},
})

File diff suppressed because one or more lines are too long

@ -10,87 +10,88 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
var fs = require('fs') const fs = require('fs')
var path = require('path') const path = require('path')
const localeIndex = require('../app/_locales/index.json')
console.log('Locale Verification') console.log('Locale Verification')
var locale = process.argv[2] const specifiedLocale = process.argv[2]
if (!locale || locale == '') { if (specifiedLocale) {
console.log('Must enter a locale as argument. exitting') console.log(`Verifying selected locale "${specifiedLocale}":\n\n`)
process.exit(1) const locale = localeIndex.find(localeMeta => localeMeta.code === specifiedLocale)
verifyLocale({ localeMeta })
} else {
console.log('Verifying all locales:\n\n')
localeIndex.forEach(localeMeta => {
verifyLocale({ localeMeta })
console.log('\n')
})
} }
console.log("verifying for locale " + locale)
localeFilePath = path.join(process.cwd(), 'app', '_locales', locale, 'messages.json')
function verifyLocale({ localeMeta }) {
const localeCode = localeMeta.code
const localeName = localeMeta.name
try { try {
localeObj = JSON.parse(fs.readFileSync(localeFilePath, 'utf8')); const localeFilePath = path.join(process.cwd(), 'app', '_locales', localeCode, 'messages.json')
targetLocale = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'));
} catch (e) { } catch (e) {
if (e.code == 'ENOENT') { if (e.code == 'ENOENT') {
console.log('Locale file not found') console.log('Locale file not found')
} else { } else {
console.log('Error opening your locale file: ', e) console.log(`Error opening your locale ("${localeCode}") file: `, e)
} }
process.exit(1) process.exit(1)
} }
englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json')
try { try {
englishObj = JSON.parse(fs.readFileSync(englishFilePath, 'utf8')); const englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json')
englishLocale = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'));
} catch (e) { } catch (e) {
if(e.code == 'ENOENT') { if(e.code == 'ENOENT') {
console.log("English File not found") console.log('English File not found')
} else { } else {
console.log("Error opening english locale file: ", e) console.log('Error opening english locale file: ', e)
} }
process.exit(1) process.exit(1)
} }
console.log('\tverifying whether all your locale strings are contained in the english one') // console.log(' verifying whether all your locale ("${localeCode}") strings are contained in the english one')
const extraItems = compareLocalesForMissingItems({ base: targetLocale, subject: englishLocale })
// console.log('\n verifying whether your locale ("${localeCode}") contains all english strings')
const missingItems = compareLocalesForMissingItems({ base: englishLocale, subject: targetLocale })
var counter = 0 const englishEntryCount = Object.keys(englishLocale).length
var foundErrorA = false const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount
var notFound = [];
Object.keys(localeObj).forEach(function(key){ console.log(`Status of **${localeName} (${localeCode})** ${coveragePercent.toFixed(2)}% coverage:`)
if (!englishObj[key]) {
foundErrorA = true
notFound.push(key)
}
counter++
})
if (foundErrorA) { if (extraItems.length) {
console.log('\nThe following string(s) is(are) not found in the english locale:') console.log('\nMissing from english locale:')
notFound.forEach(function(key) { extraItems.forEach(function(key) {
console.log(key) console.log(` - [ ] ${key}`)
}) })
} else { } else {
console.log('\tall ' + counter +' strings declared in your locale were found in the english one') // console.log(` all ${counter} strings declared in your locale ("${localeCode}") were found in the english one`)
} }
console.log('\n\tverifying whether your locale contains all english strings') if (missingItems.length) {
console.log(`\nMissing:`)
var counter = 0 missingItems.forEach(function(key) {
var foundErrorB = false console.log(` - [ ] ${key}`)
var notFound = [];
Object.keys(englishObj).forEach(function(key){
if (!localeObj[key]) {
foundErrorB = true
notFound.push(key)
}
counter++
})
if (foundErrorB) {
console.log('\nThe following string(s) is(are) not found in the your locale:')
notFound.forEach(function(key) {
console.log(key)
}) })
} else { } else {
console.log('\tall ' + counter +' english strings were found in your locale!') // console.log(` all ${counter} english strings were found in your locale ("${localeCode}")!`)
}
if (!extraItems.length && !missingItems.length) {
console.log('Full coverage : )')
}
} }
if (!foundErrorA && !foundErrorB) { function compareLocalesForMissingItems({ base, subject }) {
console.log('You are good to go') return Object.keys(base).filter((key) => !subject[key])
} }

@ -6,9 +6,12 @@ The MetaMask browser extension supports new translations added in the form of ne
## Adding a new Language ## Adding a new Language
Each supported language is represented by a folder in `app/_locales` whose name is that language's subtag ([look up a language subtag using this tool](https://r12a.github.io/app-subtags/)). - Each supported language is represented by a folder in `app/_locales` whose name is that language's subtag (example: `app/_locales/es/`). (look up a language subtag using the [r12a "Find" tool](https://r12a.github.io/app-subtags/) or this [wikipedia list](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)).
- Inside that folder there should be a `messages.json`.
- An easy way to start your translation is to first **make a copy** of `app/_locales/en/messages.json` (the english translation), and then **translate the `message` key** for each in-app message.
- **The `description` key** is just to add context for what the translation is about, it **does not need to be translated**.
- Add the language to the [locales index](https://github.com/MetaMask/metamask-extension/blob/master/app/_locales/index.json) `app/_locales/index.json`
Inside that folder there should be a `messages.json` file that follows the specified format. An easy way to start your translation is to first duplicate `app/_locales/en/messages.json` (the english translation), and then update the `message` key for each in-app message.
That's it! When MetaMask is loaded on a computer with that language set as the system language, they will see your translation instead of the default one. That's it! When MetaMask is loaded on a computer with that language set as the system language, they will see your translation instead of the default one.
@ -20,7 +23,7 @@ To automatically see if you are missing any phrases to translate, we have a scri
node development/verify-locale-strings.js $YOUR_LOCALE node development/verify-locale-strings.js $YOUR_LOCALE
``` ```
Where `$YOUR_LOCALE` is your [locale string](https://r12a.github.io/app-subtags/), i.e. the name of your language folder. Where `$YOUR_LOCALE` is your locale string (example: `es`), i.e. the name of your language folder.
To verify that your translation works in the app, you will need to [build a local copy](https://github.com/MetaMask/metamask-extension#building-locally) of MetaMask. You will need to change your browser language, your operating system language, and restart your browser (sorry it's so much work!). To verify that your translation works in the app, you will need to [build a local copy](https://github.com/MetaMask/metamask-extension#building-locally) of MetaMask. You will need to change your browser language, your operating system language, and restart your browser (sorry it's so much work!).

@ -207,9 +207,11 @@ gulp.task('dev:copy',
// lint js // lint js
const lintTargets = ['app/**/*.json', 'app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'old-ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']
gulp.task('lint', function () { gulp.task('lint', function () {
// Ignoring node_modules, dist/firefox, and docs folders: // Ignoring node_modules, dist/firefox, and docs folders:
return gulp.src(['app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']) return gulp.src(lintTargets)
.pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
// eslint.format() outputs the lint results to the console. // eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs). // Alternatively use eslint.formatEach() (see Docs).
@ -220,7 +222,7 @@ gulp.task('lint', function () {
}); });
gulp.task('lint:fix', function () { gulp.task('lint:fix', function () {
return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']) return gulp.src(lintTargets)
.pipe(eslint(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true}))) .pipe(eslint(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true})))
.pipe(eslint.format()) .pipe(eslint.format())
.pipe(eslint.failAfterError()) .pipe(eslint.failAfterError())

@ -581,7 +581,6 @@ App.prototype.renderPrimary = function () {
case 'qr': case 'qr':
log.debug('rendering show qr screen') log.debug('rendering show qr screen')
console.log(`QrView`, QrView);
return h('div', { return h('div', {
style: { style: {
position: 'absolute', position: 'absolute',

@ -247,7 +247,6 @@ BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') { if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage(false)) this.props.dispatch(actions.showConfTxPage(false))
} else { } else {
console.log(`actions.goHome`, actions.goHome);
this.props.dispatch(actions.goHome()) this.props.dispatch(actions.goHome())
} }
} }

@ -25,7 +25,6 @@ function QrCodeView () {
QrCodeView.prototype.render = function () { QrCodeView.prototype.render = function () {
const props = this.props const props = this.props
const Qr = props.Qr const Qr = props.Qr
console.log(`QrCodeView Qr`, Qr);
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
const qrImage = qrCode(4, 'M') const qrImage = qrCode(4, 'M')
qrImage.addData(address) qrImage.addData(address)

@ -35,7 +35,7 @@ RangeSlider.prototype.render = function () {
step: increment, step: increment,
style: range, style: range,
value: state.value || defaultValue, value: state.value || defaultValue,
onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, onChange: mirrorInput ? this.mirrorInputs.bind(this) : onInput,
}), }),
// Mirrored input for range // Mirrored input for range
@ -47,7 +47,7 @@ RangeSlider.prototype.render = function () {
value: state.value || defaultValue, value: state.value || defaultValue,
step: increment, step: increment,
style: input, style: input,
onChange: this.mirrorInputs.bind(this, event), onChange: this.mirrorInputs.bind(this),
}) : null, }) : null,
]) ])
) )

@ -30,7 +30,12 @@ function TransactionListItem () {
TransactionListItem.prototype.showRetryButton = function () { TransactionListItem.prototype.showRetryButton = function () {
const { transaction = {}, transactions } = this.props const { transaction = {}, transactions } = this.props
const { status, submittedTime, txParams } = transaction const { submittedTime, txParams } = transaction
if (!txParams) {
return false
}
const currentNonce = txParams.nonce const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce) const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted') const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')

@ -42,7 +42,7 @@ ConfigScreen.prototype.render = function () {
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => { onClick: () => {
state.dispatch(actions.goHome()) state.dispatch(actions.goHome())
}, },
}), }),

3399
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -12,7 +12,10 @@
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss", "test:integration:build": "gulp build:scss",
"test:e2e": "METAMASK_ENV=test mocha test/e2e/metamask.spec --recursive", "test:e2e": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run'",
"test:e2e:run": "mocha test/e2e/metamask.spec --recursive",
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
"test:screens:run": "node test/screens/new-ui.js",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload", "test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi", "test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js", "test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
@ -27,6 +30,7 @@
"test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales", "test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales",
"test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js", "test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js",
"test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js", "test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js",
"ganache:start": "ganache-cli -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'",
"sentry": "export RELEASE=`cat app/manifest.json| jq -r .version` && npm run sentry:release && npm run sentry:upload", "sentry": "export RELEASE=`cat app/manifest.json| jq -r .version` && npm run sentry:release && npm run sentry:upload",
"sentry:release": "npm run sentry:release:new && npm run sentry:release:clean", "sentry:release": "npm run sentry:release:new && npm run sentry:release:clean",
"sentry:release:new": "sentry-cli releases --org 'metamask' --project 'metamask' new $RELEASE", "sentry:release:new": "sentry-cli releases --org 'metamask' --project 'metamask' new $RELEASE",
@ -140,7 +144,6 @@
"number-to-bn": "^1.7.0", "number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0", "obj-multiplex": "^1.0.0",
"obs-store": "^3.0.0", "obs-store": "^3.0.0",
"once": "^1.3.3",
"percentile": "^1.2.0", "percentile": "^1.2.0",
"pify": "^3.0.0", "pify": "^3.0.0",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
@ -216,10 +219,13 @@
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5", "enzyme-adapter-react-15": "^1.0.5",
"eslint-plugin-chai": "0.0.1", "eslint-plugin-chai": "0.0.1",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.2.7", "eth-json-rpc-middleware": "^1.2.7",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.0.0", "gulp-babel": "^7.0.0",
"gulp-eslint": "^4.0.0", "gulp-eslint": "^4.0.0",
@ -234,6 +240,7 @@
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^5.0.0", "gulp-watch": "^5.0.0",
"gulp-zip": "^4.0.0", "gulp-zip": "^4.0.0",
"image-size": "^0.6.2",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsdom": "^11.2.0", "jsdom": "^11.2.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
@ -252,6 +259,7 @@
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"nyc": "^11.0.3", "nyc": "^11.0.3",
"open": "0.0.5", "open": "0.0.5",
"png-file-stream": "^1.0.0",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"qs": "^6.2.0", "qs": "^6.2.0",
"qunitjs": "^2.4.1", "qunitjs": "^2.4.1",
@ -259,7 +267,9 @@
"react-test-renderer": "^15.6.2", "react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"redux-test-utils": "^0.2.2", "redux-test-utils": "^0.2.2",
"rimraf": "^2.6.2",
"selenium-webdriver": "^3.5.0", "selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0", "sinon": "^5.0.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1", "tape": "^4.5.1",

@ -38,6 +38,8 @@ describe('Metamask popup page', function () {
const tabs = await driver.getAllWindowHandles() const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0]) await driver.switchTo().window(tabs[0])
await delay(300) await delay(300)
await setProviderType('localhost')
await delay(300)
}) })
it('should match title', async () => { it('should match title', async () => {
@ -124,6 +126,10 @@ describe('Metamask popup page', function () {
}) })
}) })
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) { async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}` const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure` const filepathBase = `${artifactDir}/test-failure`

@ -0,0 +1,61 @@
const reactTriggerChange = require('../../lib/react-trigger-change')
const {
timeout,
queryAsync,
findAsync,
} = require('../../lib/util')
QUnit.module('tx list items')
QUnit.test('renders list items successfully', (assert) => {
const done = assert.async()
runTxListItemsTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
done()
})
})
async function runTxListItemsTest(assert, done) {
console.log('*** start runTxListItemsTest')
const selectState = await queryAsync($, 'select')
selectState.val('tx list items')
reactTriggerChange(selectState[0])
const metamaskLogo = await queryAsync($, '.left-menu-wrapper')
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()
const txListItems = await queryAsync($, '.tx-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const unapprovedTx = txListItems[0]
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-link')
assert.equal(retryTxLink[0].textContent, 'Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4]
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5]
const shapeShiftTxStatus = await findAsync($(shapeShiftTx), '.flex-column div:eq(1)')
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6]
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
assert.equal(confirmedTokenTxAddress[0].textContent, '0xe7884118...81a9', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7]
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
}

@ -0,0 +1,18 @@
require('chromedriver')
const webdriver = require('selenium-webdriver')
exports.delay = function delay (time) {
return new Promise(resolve => setTimeout(resolve, time))
}
exports.buildWebDriver = function buildWebDriver (extPath) {
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {
args: [`load-extension=${extPath}`],
},
})
.forBrowser('chrome')
.build()
}

@ -0,0 +1,230 @@
const path = require('path')
const fs = require('fs')
const pify = require('pify')
const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const webdriver = require('selenium-webdriver')
const endOfStream = require('end-of-stream')
const GIFEncoder = require('gifencoder')
const pngFileStream = require('png-file-stream')
const sizeOfPng = require('image-size/lib/types/png')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
const localesIndex = require('../../app/_locales/index.json')
let driver
captureAllScreens().catch((err) => {
try {
console.error(err)
verboseReportOnFailure()
driver.quit()
} catch (err) {
console.error(err)
}
process.exit(1)
})
async function captureAllScreens() {
let screenshotCount = 0
// common names
let button
let tabs
let element
await cleanScreenShotDir()
// setup selenium and install extension
const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath)
await driver.get('chrome://extensions-frame')
const elems = await driver.findElements(By.css('.extension-list-item-wrapper'))
const extensionId = await elems[1].getAttribute('id')
await driver.get(`chrome-extension://${extensionId}/home.html`)
await delay(500)
tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(1000)
await setProviderType('localhost')
await delay(300)
// click try new ui
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-row.flex-center.flex-grow > p')).click()
await delay(300)
// close metamask homepage and extra home.html
tabs = await driver.getAllWindowHandles()
// metamask homepage is opened on prod, not dev
if (tabs.length > 2) {
await driver.switchTo().window(tabs[2])
driver.close()
}
await driver.switchTo().window(tabs[1])
driver.close()
await driver.switchTo().window(tabs[0])
await delay(300)
await captureLanguageScreenShots('welcome-new-ui')
// setup account
await delay(1000)
await driver.findElement(By.css('body')).click()
await delay(300)
await captureLanguageScreenShots('welcome')
await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password')
const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)'))
const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
await delay(500)
await captureLanguageScreenShots('choose-password-filled')
await driver.findElement(By.css('button')).click()
await delay(500)
await captureLanguageScreenShots('unique account image')
await driver.findElement(By.css('button')).click()
await delay(500)
await captureLanguageScreenShots('privacy note')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('terms')
await delay(300)
element = driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
await captureLanguageScreenShots('terms-scrolled')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase')
await driver.findElement(By.css('.backup-phrase__reveal-button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase - reveal')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('confirm secret backup phrase')
// finish up
console.log('building gif...')
await generateGif()
await driver.quit()
return
//
// await button.click()
// await delay(700)
// this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
// await captureScreenShot('seed phrase')
//
// const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
// await continueAfterSeedPhrase.click()
// await delay(300)
// await captureScreenShot('main screen')
//
// await driver.findElement(By.css('.sandwich-expando')).click()
// await delay(500)
// await captureScreenShot('menu')
// await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
// await captureScreenShot('main screen')
// it('should accept account password after lock', async () => {
// await delay(500)
// await driver.findElement(By.id('password-box')).sendKeys('123456789')
// await driver.findElement(By.css('button')).click()
// await delay(500)
// })
//
// it('should show QR code option', async () => {
// await delay(300)
// await driver.findElement(By.css('.fa-ellipsis-h')).click()
// await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
// await delay(300)
// })
//
// it('should show the account address', async () => {
// this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
// await driver.findElement(By.css('.fa-arrow-left')).click()
// await delay(500)
// })
async function captureLanguageScreenShots(label) {
const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
// take english shot
await captureScreenShot(`${label} (en)`)
for (let localeMeta of nonEnglishLocales) {
// set locale and take shot
await setLocale(localeMeta.code)
await delay(300)
await captureScreenShot(`${label} (${localeMeta.code})`)
}
// return locale to english
await setLocale('en')
await delay(300)
}
async function setLocale(code) {
await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
}
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
// cleanup
await driver.quit()
async function cleanScreenShotDir() {
await pify(rimraf)(`./test-artifacts/screens/`)
}
async function captureScreenShot(label) {
const shotIndex = screenshotCount.toString().padStart(4, '0')
screenshotCount++
const artifactDir = `./test-artifacts/screens/`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
}
async function generateGif(){
// calculate screenshot size
const screenshot = await driver.takeScreenshot()
const pngBuffer = Buffer.from(screenshot, 'base64')
const size = sizeOfPng.calculate(pngBuffer)
// read only the english pngs into gif
const encoder = new GIFEncoder(size.width, size.height)
const stream = pngFileStream('./test-artifacts/screens/* (en).png')
.pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
.pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
// wait for end
await pify(endOfStream)(stream)
}
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}

@ -162,7 +162,7 @@ describe('Transaction Controller', function () {
describe('#addUnapprovedTransaction', function () { describe('#addUnapprovedTransaction', function () {
it('should add an unapproved transaction and return a valid txMeta', function (done) { it('should add an unapproved transaction and return a valid txMeta', function (done) {
txController.addUnapprovedTransaction({}) txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' })
.then((txMeta) => { .then((txMeta) => {
assert(('id' in txMeta), 'should have a id') assert(('id' in txMeta), 'should have a id')
assert(('time' in txMeta), 'should have a time stamp') assert(('time' in txMeta), 'should have a time stamp')
@ -182,7 +182,7 @@ describe('Transaction Controller', function () {
assert(txMetaFromEmit, 'txMeta is falsey') assert(txMetaFromEmit, 'txMeta is falsey')
done() done()
}) })
txController.addUnapprovedTransaction({}) txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' })
.catch(done) .catch(done)
}) })
@ -213,6 +213,7 @@ describe('Transaction Controller', function () {
describe('#validateTxParams', function () { describe('#validateTxParams', function () {
it('does not throw for positive values', function (done) { it('does not throw for positive values', function (done) {
var sample = { var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01', value: '0x01',
} }
txController.txGasUtil.validateTxParams(sample).then(() => { txController.txGasUtil.validateTxParams(sample).then(() => {
@ -222,6 +223,7 @@ describe('Transaction Controller', function () {
it('returns error for negative values', function (done) { it('returns error for negative values', function (done) {
var sample = { var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01', value: '-0x01',
} }
txController.txGasUtil.validateTxParams(sample) txController.txGasUtil.validateTxParams(sample)

@ -29,4 +29,28 @@ describe('Tx Gas Util', function () {
} }
assert.throws(() => { txGasUtil.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address') assert.throws(() => { txGasUtil.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
}) })
it('should error when from is not a hex string', function () {
// where from is undefined
const txParams = {}
assert.throws(() => { txGasUtil.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is array
txParams.from = []
assert.throws(() => { txGasUtil.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a object
txParams.from = {}
assert.throws(() => { txGasUtil.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txGasUtil.validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txGasUtil.validateFrom(txParams)
})
}) })

@ -1374,7 +1374,7 @@ function retryTransaction (txId) {
function setProviderType (type) { function setProviderType (type) {
return (dispatch) => { return (dispatch) => {
log.debug(`background.setProviderType`) log.debug(`background.setProviderType`, type)
background.setProviderType(type, (err, result) => { background.setProviderType(type, (err, result) => {
if (err) { if (err) {
log.error(err) log.error(err)
@ -1395,13 +1395,14 @@ function updateProviderType (type) {
} }
function setRpcTarget (newRpc) { function setRpcTarget (newRpc) {
log.debug(`background.setRpcTarget: ${newRpc}`)
return (dispatch) => { return (dispatch) => {
log.debug(`background.setRpcTarget: ${newRpc}`)
background.setCustomRpc(newRpc, (err, result) => { background.setCustomRpc(newRpc, (err, result) => {
if (err) { if (err) {
log.error(err) log.error(err)
return dispatch(self.displayWarning('Had a problem changing networks!')) return dispatch(self.displayWarning('Had a problem changing networks!'))
} }
dispatch(actions.setSelectedToken())
}) })
} }
} }

@ -77,7 +77,6 @@ class App extends Component {
component: RevealSeedPage, component: RevealSeedPage,
mascaraComponent: MascaraSeedScreen, mascaraComponent: MascaraSeedScreen,
}), }),
// h(Initialized, { path: CONFIRM_SEED_ROUTE, exact, component: MascaraConfirmSeedScreen }),
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }), h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
h(Initialized, { path: SETTINGS_ROUTE, component: Settings }), h(Initialized, { path: SETTINGS_ROUTE, component: Settings }),
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
@ -214,7 +213,6 @@ class App extends Component {
networkDropdownOpen, networkDropdownOpen,
showNetworkDropdown, showNetworkDropdown,
hideNetworkDropdown, hideNetworkDropdown,
currentView,
isInitialized, isInitialized,
welcomeScreenSeen, welcomeScreenSeen,
isPopup, isPopup,
@ -276,7 +274,7 @@ class App extends Component {
h(NetworkIndicator, { h(NetworkIndicator, {
network, network,
provider, provider,
disabled: currentView.name === 'confTx', disabled: this.props.location.pathname === CONFIRM_TRANSACTION_ROUTE,
onClick: (event) => { onClick: (event) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -395,6 +393,7 @@ App.propTypes = {
showNetworkDropdown: PropTypes.func, showNetworkDropdown: PropTypes.func,
hideNetworkDropdown: PropTypes.func, hideNetworkDropdown: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
location: PropTypes.object,
dispatch: PropTypes.func, dispatch: PropTypes.func,
toggleAccountMenu: PropTypes.func, toggleAccountMenu: PropTypes.func,
selectedAddress: PropTypes.string, selectedAddress: PropTypes.string,

@ -65,6 +65,7 @@ function mapDispatchToProps (dispatch) {
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)), updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)),
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
} }
} }
@ -112,6 +113,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
selectedToken, selectedToken,
balance, balance,
updateSendAmount, updateSendAmount,
updateSendErrors,
} = this.props } = this.props
if (maxModeOn && !selectedToken) { if (maxModeOn && !selectedToken) {
@ -126,6 +128,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateGasPrice(ethUtil.addHexPrefix(gasPrice)) updateGasPrice(ethUtil.addHexPrefix(gasPrice))
updateGasLimit(ethUtil.addHexPrefix(gasLimit)) updateGasLimit(ethUtil.addHexPrefix(gasLimit))
updateGasTotal(ethUtil.addHexPrefix(gasTotal)) updateGasTotal(ethUtil.addHexPrefix(gasTotal))
updateSendErrors({ insufficientFunds: false })
hideModal() hideModal()
} }

@ -203,18 +203,18 @@ NetworkDropdown.prototype.render = function () {
{ {
key: 'default', key: 'default',
closeMenu: () => this.props.hideNetworkDropdown(), closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget('http://localhost:8545'), onClick: () => props.setProviderType('localhost'),
style: dropdownMenuItemStyle, style: dropdownMenuItemStyle,
}, },
[ [
activeNetwork === 'http://localhost:8545' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), providerType === 'localhost' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, { h(NetworkDropdownIcon, {
isSelected: activeNetwork === 'http://localhost:8545', isSelected: providerType === 'localhost',
innerBorder: '1px solid #9b9b9b', innerBorder: '1px solid #9b9b9b',
}), }),
h('span.network-name-item', { h('span.network-name-item', {
style: { style: {
color: activeNetwork === 'http://localhost:8545' ? '#ffffff' : '#9b9b9b', color: providerType === 'localhost' ? '#ffffff' : '#9b9b9b',
}, },
}, this.context.t('localhost')), }, this.context.t('localhost')),
] ]

@ -105,9 +105,8 @@ IdenticonComponent.prototype.componentDidUpdate = function () {
function _generateBlockie (container, address, diameter) { function _generateBlockie (container, address, diameter) {
const img = new Image() const img = new Image()
img.src = toDataUrl(address) img.src = toDataUrl(address)
const dia = !diameter || diameter < 50 ? 50 : diameter img.height = diameter
img.height = dia * 1.25 img.width = diameter
img.width = dia * 1.25
container.appendChild(img) container.appendChild(img)
} }

@ -27,21 +27,6 @@ const {
} = require('../../routes') } = require('../../routes')
class Home extends Component { class Home extends Component {
componentDidMount () {
const {
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedTypedMessagesCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
if (Object.keys(unapprovedTxs).length ||
unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () { render () {
log.debug('rendering primary') log.debug('rendering primary')
const { const {
@ -51,6 +36,10 @@ class Home extends Component {
currentView, currentView,
activeAddress, activeAddress,
seedWords, seedWords,
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedTypedMessagesCount = 0,
} = this.props } = this.props
// notices // notices
@ -81,6 +70,16 @@ class Home extends Component {
}) })
} }
// unapprovedTxs and unapproved messages
if (Object.keys(unapprovedTxs).length ||
unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
return h(Redirect, {
to: {
pathname: CONFIRM_TRANSACTION_ROUTE,
},
})
}
// if (!props.noActiveNotices) { // if (!props.noActiveNotices) {
// log.debug('rendering notice screen for unread notices.') // log.debug('rendering notice screen for unread notices.')
// return h(NoticeScreen, { // return h(NoticeScreen, {

@ -10,11 +10,16 @@ const clone = require('clone')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN const BN = ethUtil.BN
const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
const classnames = require('classnames')
const { const {
conversionUtil, conversionUtil,
addCurrencies, addCurrencies,
multiplyCurrencies, multiplyCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const {
getGasTotal,
isBalanceSufficient,
} = require('../send/send-utils')
const GasFeeDisplay = require('../send/gas-fee-display-v2') const GasFeeDisplay = require('../send/gas-fee-display-v2')
const SenderToRecipient = require('../sender-to-recipient') const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display') const NetworkDisplay = require('../network-display')
@ -41,12 +46,14 @@ function mapStateToProps (state) {
} = state.metamask } = state.metamask
const accounts = state.metamask.accounts const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const { balance } = accounts[selectedAddress]
return { return {
conversionRate, conversionRate,
identities, identities,
selectedAddress, selectedAddress,
currentCurrency, currentCurrency,
send, send,
balance,
} }
} }
@ -96,6 +103,7 @@ function mapDispatchToProps (dispatch) {
})) }))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
}, },
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
} }
} }
@ -106,6 +114,52 @@ function ConfirmSendEther () {
this.onSubmit = this.onSubmit.bind(this) this.onSubmit = this.onSubmit.bind(this)
} }
ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
const {
balance: oldBalance,
conversionRate: oldConversionRate,
} = prevProps
const {
updateSendErrors,
balance,
conversionRate,
send: {
errors: {
simulationFails,
},
},
} = this.props
const txMeta = this.gatherTxMeta()
const shouldUpdateBalanceSendErrors = balance && [
balance !== oldBalance,
conversionRate !== oldConversionRate,
].some(x => Boolean(x))
if (shouldUpdateBalanceSendErrors) {
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
updateSendErrors({
insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
})
}
const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
if (shouldUpdateSimulationSendError) {
updateSendErrors({
simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
})
}
}
ConfirmSendEther.prototype.componentWillMount = function () {
this.updateComponentSendErrors({})
}
ConfirmSendEther.prototype.componentDidUpdate = function (prevProps) {
this.updateComponentSendErrors(prevProps)
}
ConfirmSendEther.prototype.getAmount = function () { ConfirmSendEther.prototype.getAmount = function () {
const { conversionRate, currentCurrency } = this.props const { conversionRate, currentCurrency } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
@ -234,7 +288,12 @@ ConfirmSendEther.prototype.render = function () {
conversionRate, conversionRate,
currentCurrency: convertedCurrency, currentCurrency: convertedCurrency,
showCustomizeGasModal, showCustomizeGasModal,
send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice }, send: {
gasTotal,
gasLimit: sendGasLimit,
gasPrice: sendGasPrice,
errors,
},
} = this.props } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {} const txParams = txMeta.txParams || {}
@ -342,7 +401,12 @@ ConfirmSendEther.prototype.render = function () {
]), ]),
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [ h('div', {
className: classnames({
'confirm-screen-section-column--with-error': errors['insufficientFunds'],
'confirm-screen-section-column': !errors['insufficientFunds'],
}),
}, [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]), ]),
@ -351,6 +415,8 @@ ConfirmSendEther.prototype.render = function () {
h('div.confirm-screen-row-info', `${totalInFIAT} ${currentCurrency.toUpperCase()}`), h('div.confirm-screen-row-info', `${totalInFIAT} ${currentCurrency.toUpperCase()}`),
h('div.confirm-screen-row-detail', `${totalInETH} ETH`), h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
]), ]),
this.renderErrorMessage('insufficientFunds'),
]), ]),
]), ]),
@ -436,8 +502,10 @@ ConfirmSendEther.prototype.render = function () {
]), ]),
h('form#pending-tx-form', { h('form#pending-tx-form', {
className: 'confirm-screen-form',
onSubmit: this.onSubmit, onSubmit: this.onSubmit,
}, [ }, [
this.renderErrorMessage('simulationFails'),
h('.page-container__footer', [ h('.page-container__footer', [
// Cancel Button // Cancel Button
h('button.btn-cancel.page-container__footer-button.allcaps', { h('button.btn-cancel.page-container__footer-button.allcaps', {
@ -455,16 +523,28 @@ ConfirmSendEther.prototype.render = function () {
) )
} }
ConfirmSendEther.prototype.renderErrorMessage = function (message) {
const { send: { errors } } = this.props
return errors[message]
? h('div.confirm-screen-error', [ errors[message] ])
: null
}
ConfirmSendEther.prototype.onSubmit = function (event) { ConfirmSendEther.prototype.onSubmit = function (event) {
event.preventDefault() event.preventDefault()
const { updateSendErrors } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const valid = this.checkValidity() const valid = this.checkValidity()
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
this.setState({ valid, submitting: true }) this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) { if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event) this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') })
} else { } else {
this.props.dispatch(actions.displayWarning(this.context.t('invalidGasParams'))) updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') })
this.setState({ submitting: false }) this.setState({ submitting: false })
} }
} }
@ -477,6 +557,28 @@ ConfirmSendEther.prototype.cancel = function (event, txMeta) {
.then(() => this.props.history.push(DEFAULT_ROUTE)) .then(() => this.props.history.push(DEFAULT_ROUTE))
} }
ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
const {
balance,
conversionRate,
} = this.props
const {
txParams: {
gas,
gasPrice,
value: amount,
},
} = txMeta
const gasTotal = getGasTotal(gas, gasPrice)
return isBalanceSufficient({
amount,
gasTotal,
balance,
conversionRate,
})
}
ConfirmSendEther.prototype.checkValidity = function () { ConfirmSendEther.prototype.checkValidity = function () {
const form = this.getFormEl() const form = this.getFormEl()
const valid = form.checkValidity() const valid = form.checkValidity()

@ -19,9 +19,14 @@ const {
multiplyCurrencies, multiplyCurrencies,
addCurrencies, addCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const {
getGasTotal,
isBalanceSufficient,
} = require('../send/send-utils')
const { const {
calcTokenAmount, calcTokenAmount,
} = require('../../token-util') } = require('../../token-util')
const classnames = require('classnames')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
@ -52,9 +57,10 @@ function mapStateToProps (state, ownProps) {
identities, identities,
currentCurrency, currentCurrency,
} = state.metamask } = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = getSelectedAddress(state) const selectedAddress = getSelectedAddress(state)
const tokenExchangeRate = getTokenExchangeRate(state, symbol) const tokenExchangeRate = getTokenExchangeRate(state, symbol)
const { balance } = accounts[selectedAddress]
return { return {
conversionRate, conversionRate,
identities, identities,
@ -64,6 +70,7 @@ function mapStateToProps (state, ownProps) {
currentCurrency: currentCurrency.toUpperCase(), currentCurrency: currentCurrency.toUpperCase(),
send: state.metamask.send, send: state.metamask.send,
tokenContract: getSelectedTokenContract(state), tokenContract: getSelectedTokenContract(state),
balance,
} }
} }
@ -135,6 +142,7 @@ function mapDispatchToProps (dispatch, ownProps) {
})) }))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
}, },
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
} }
} }
@ -151,6 +159,44 @@ ConfirmSendToken.prototype.editTransaction = function (txMeta) {
history.push(SEND_ROUTE) history.push(SEND_ROUTE)
} }
ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) {
const {
balance: oldBalance,
conversionRate: oldConversionRate,
} = prevProps
const {
updateSendErrors,
balance,
conversionRate,
send: {
errors: {
simulationFails,
},
},
} = this.props
const txMeta = this.gatherTxMeta()
const shouldUpdateBalanceSendErrors = balance && [
balance !== oldBalance,
conversionRate !== oldConversionRate,
].some(x => Boolean(x))
if (shouldUpdateBalanceSendErrors) {
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
updateSendErrors({
insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
})
}
const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
if (shouldUpdateSimulationSendError) {
updateSendErrors({
simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
})
}
}
ConfirmSendToken.prototype.componentWillMount = function () { ConfirmSendToken.prototype.componentWillMount = function () {
const { tokenContract, selectedAddress } = this.props const { tokenContract, selectedAddress } = this.props
tokenContract && tokenContract tokenContract && tokenContract
@ -158,6 +204,11 @@ ConfirmSendToken.prototype.componentWillMount = function () {
.then(usersToken => { .then(usersToken => {
}) })
this.props.updateTokenExchangeRate() this.props.updateTokenExchangeRate()
this.updateComponentSendErrors({})
}
ConfirmSendToken.prototype.componentDidUpdate = function (prevProps) {
this.updateComponentSendErrors(prevProps)
} }
ConfirmSendToken.prototype.getAmount = function () { ConfirmSendToken.prototype.getAmount = function () {
@ -318,7 +369,7 @@ ConfirmSendToken.prototype.renderGasFee = function () {
} }
ConfirmSendToken.prototype.renderTotalPlusGas = function () { ConfirmSendToken.prototype.renderTotalPlusGas = function () {
const { token: { symbol }, currentCurrency } = this.props const { token: { symbol }, currentCurrency, send: { errors } } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const { fiat: fiatGas, token: tokenGas } = this.getGasFee() const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
@ -338,7 +389,12 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () {
) )
: ( : (
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [ h('div', {
className: classnames({
'confirm-screen-section-column--with-error': errors['insufficientFunds'],
'confirm-screen-section-column': !errors['insufficientFunds'],
}),
}, [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]), ]),
@ -347,12 +403,21 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () {
h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`), h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`), h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`),
]), ]),
this.renderErrorMessage('insufficientFunds'),
]) ])
) )
} }
ConfirmSendToken.prototype.renderErrorMessage = function (message) {
const { send: { errors } } = this.props
return errors[message]
? h('div.confirm-screen-error', [ errors[message] ])
: null
}
ConfirmSendToken.prototype.render = function () { ConfirmSendToken.prototype.render = function () {
const { editTransaction } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const { const {
from: { from: {
@ -379,7 +444,7 @@ ConfirmSendToken.prototype.render = function () {
h('div.page-container', [ h('div.page-container', [
h('div.page-container__header', [ h('div.page-container__header', [
!txMeta.lastGasPrice && h('button.confirm-screen-back-button', { !txMeta.lastGasPrice && h('button.confirm-screen-back-button', {
onClick: () => editTransaction(txMeta), onClick: () => this.editTransaction(txMeta),
}, this.context.t('edit')), }, this.context.t('edit')),
h('div.page-container__title', title), h('div.page-container__title', title),
h('div.page-container__subtitle', subtitle), h('div.page-container__subtitle', subtitle),
@ -448,8 +513,10 @@ ConfirmSendToken.prototype.render = function () {
]), ]),
h('form#pending-tx-form', { h('form#pending-tx-form', {
className: 'confirm-screen-form',
onSubmit: this.onSubmit, onSubmit: this.onSubmit,
}, [ }, [
this.renderErrorMessage('simulationFails'),
h('.page-container__footer', [ h('.page-container__footer', [
// Cancel Button // Cancel Button
h('button.btn-cancel.page-container__footer-button.allcaps', { h('button.btn-cancel.page-container__footer-button.allcaps', {
@ -467,18 +534,44 @@ ConfirmSendToken.prototype.render = function () {
ConfirmSendToken.prototype.onSubmit = function (event) { ConfirmSendToken.prototype.onSubmit = function (event) {
event.preventDefault() event.preventDefault()
const { updateSendErrors } = this.props
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const valid = this.checkValidity() const valid = this.checkValidity()
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
this.setState({ valid, submitting: true }) this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) { if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event) this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') })
} else { } else {
this.props.dispatch(actions.displayWarning(this.context.t('invalidGasParams'))) updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') })
this.setState({ submitting: false }) this.setState({ submitting: false })
} }
} }
ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
const {
balance,
conversionRate,
} = this.props
const {
txParams: {
gas,
gasPrice,
},
} = txMeta
const gasTotal = getGasTotal(gas, gasPrice)
return isBalanceSufficient({
amount: '0',
gasTotal,
balance,
conversionRate,
})
}
ConfirmSendToken.prototype.cancel = function (event, txMeta) { ConfirmSendToken.prototype.cancel = function (event, txMeta) {
event.preventDefault() event.preventDefault()
const { cancelTransaction } = this.props const { cancelTransaction } = this.props

@ -2,6 +2,7 @@ const {
addCurrencies, addCurrencies,
conversionUtil, conversionUtil,
conversionGTE, conversionGTE,
multiplyCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const { const {
calcTokenAmount, calcTokenAmount,
@ -31,7 +32,7 @@ function isBalanceSufficient ({
{ {
value: totalAmount, value: totalAmount,
fromNumericBase: 'hex', fromNumericBase: 'hex',
conversionRate: amountConversionRate, conversionRate: amountConversionRate || conversionRate,
fromCurrency: primaryCurrency, fromCurrency: primaryCurrency,
}, },
) )
@ -62,7 +63,16 @@ function isTokenBalanceSufficient ({
return tokenBalanceIsSufficient return tokenBalanceIsSufficient
} }
function getGasTotal (gasLimit, gasPrice) {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
}
module.exports = { module.exports = {
getGasTotal,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
} }

@ -68,20 +68,24 @@ TxListItem.prototype.getAddressText = function () {
const { const {
address, address,
txParams = {}, txParams = {},
isMsg,
} = this.props } = this.props
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { name: txDataName, params = [] } = decodedData || {} const { name: txDataName, params = [] } = decodedData || {}
const { value } = params[0] || {} const { value } = params[0] || {}
switch (txDataName) { let addressText
case 'transfer': if (txDataName === 'transfer' || address) {
return `${value.slice(0, 10)}...${value.slice(-4)}` const addressToRender = txDataName === 'transfer' ? value : address
default: addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}`
return address } else if (isMsg) {
? `${address.slice(0, 10)}...${address.slice(-4)}` addressText = this.context.t('sigRequest')
: this.context.t('contractDeployment') } else {
addressText = this.context.t('contractDeployment')
} }
return addressText
} }
TxListItem.prototype.getSendEtherTotal = function () { TxListItem.prototype.getSendEtherTotal = function () {
@ -191,6 +195,9 @@ TxListItem.prototype.showRetryButton = function () {
transactionId, transactionId,
txParams, txParams,
} = this.props } = this.props
if (!txParams) {
return false
}
const currentNonce = txParams.nonce const currentNonce = txParams.nonce
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce) const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted') const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')

@ -82,9 +82,9 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
const props = { const props = {
dateString: formatDate(transaction.time), dateString: formatDate(transaction.time),
address: transaction.txParams.to, address: transaction.txParams && transaction.txParams.to,
transactionStatus: transaction.status, transactionStatus: transaction.status,
transactionAmount: transaction.txParams.value, transactionAmount: transaction.txParams && transaction.txParams.value,
transactionId: transaction.id, transactionId: transaction.id,
transactionHash: transaction.hash, transactionHash: transaction.hash,
transactionNetworkId: transaction.metamaskNetworkId, transactionNetworkId: transaction.metamaskNetworkId,
@ -106,6 +106,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
const opts = { const opts = {
key: transactionId || transactionHash, key: transactionId || transactionHash,
txParams: transaction.txParams, txParams: transaction.txParams,
isMsg: Boolean(transaction.msgParams),
transactionStatus, transactionStatus,
transactionId, transactionId,
dateString, dateString,

@ -55,11 +55,25 @@ function ConfirmTxScreen () {
Component.call(this) Component.call(this)
} }
ConfirmTxScreen.prototype.componentDidMount = function () {
const {
unapprovedTxs = {},
network,
send,
} = this.props
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
if (unconfTxList.length === 0 && !send.to) {
this.props.history.push(DEFAULT_ROUTE)
}
}
ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) { ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
const { const {
unapprovedTxs, unapprovedTxs = {},
network, network,
selectedAddressTxList, selectedAddressTxList,
send,
} = this.props } = this.props
const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network)
@ -67,7 +81,7 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {}
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
if (prevTx.status === 'dropped' && unconfTxList.length === 0) { if (unconfTxList.length === 0 && (prevTx.status === 'dropped' || !send.to)) {
this.props.history.push(DEFAULT_ROUTE) this.props.history.push(DEFAULT_ROUTE)
} }
} }
@ -109,13 +123,6 @@ ConfirmTxScreen.prototype.render = function () {
*/ */
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
if (unconfTxList.length === 0) {
return h(Redirect, {
to: {
pathname: DEFAULT_ROUTE,
},
})
}
return currentTxView({ return currentTxView({
// Properties // Properties

@ -266,6 +266,7 @@ section .confirm-screen-account-number,
.confirm-screen-total-box { .confirm-screen-total-box {
background-color: $wild-sand; background-color: $wild-sand;
position: relative;
.confirm-screen-label { .confirm-screen-label {
line-height: 21px; line-height: 21px;
@ -287,6 +288,41 @@ section .confirm-screen-account-number,
} }
} }
.confirm-screen-error {
font-size: 12px;
line-height: 12px;
color: #f00;
position: absolute;
right: 12px;
width: 80px;
text-align: right;
}
.confirm-screen-row.confirm-screen-total-box {
.confirm-screen-section-column--with-error {
flex: 0.6;
}
}
@media screen and (max-width: 379px) {
.confirm-screen-row.confirm-screen-total-box {
.confirm-screen-section-column--with-error {
flex: 0.4;
}
}
}
.confirm-screen-form {
position: relative;
.confirm-screen-error {
right: 0;
width: 100%;
margin-top: 7px;
text-align: center;
}
}
.confirm-screen-confirm-button { .confirm-screen-confirm-button {
height: 50px; height: 50px;
border-radius: 4px; border-radius: 4px;

@ -27,6 +27,7 @@ const {
const { const {
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
getGasTotal,
} = require('./components/send/send-utils') } = require('./components/send/send-utils')
const { isValidAddress } = require('./util') const { isValidAddress } = require('./util')
const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes') const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes')
@ -133,7 +134,7 @@ SendTransactionScreen.prototype.updateGas = function () {
estimateGas(estimateGasParams), estimateGas(estimateGasParams),
]) ])
.then(([gasPrice, gas]) => { .then(([gasPrice, gas]) => {
const newGasTotal = this.getGasTotal(gas, gasPrice) const newGasTotal = getGasTotal(gas, gasPrice)
updateGasTotal(newGasTotal) updateGasTotal(newGasTotal)
this.setState({ gasLoadingError: false }) this.setState({ gasLoadingError: false })
}) })
@ -141,19 +142,11 @@ SendTransactionScreen.prototype.updateGas = function () {
this.setState({ gasLoadingError: true }) this.setState({ gasLoadingError: true })
}) })
} else { } else {
const newGasTotal = this.getGasTotal(gasLimit, gasPrice) const newGasTotal = getGasTotal(gasLimit, gasPrice)
updateGasTotal(newGasTotal) updateGasTotal(newGasTotal)
} }
} }
SendTransactionScreen.prototype.getGasTotal = function (gasLimit, gasPrice) {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
}
SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) { SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
const { const {
from: { balance }, from: { balance },
@ -642,6 +635,10 @@ SendTransactionScreen.prototype.onSubmit = function (event) {
txParams.to = to txParams.to = to
} }
Object.keys(txParams).forEach(key => {
txParams[key] = ethUtil.addHexPrefix(txParams[key])
})
selectedToken selectedToken
? signTokenTx(selectedToken.address, to, amount, txParams) ? signTokenTx(selectedToken.address, to, amount, txParams)
: signTx(txParams) : signTx(txParams)

@ -69,6 +69,16 @@ async function startApp (metamaskState, accountManager, opts) {
store.dispatch(actions.updateMetamaskState(metamaskState)) store.dispatch(actions.updateMetamaskState(metamaskState))
}) })
// global metamask api - used by tooling
global.metamask = {
updateCurrentLocale: (code) => {
store.dispatch(actions.updateCurrentLocale(code))
},
setProviderType: (type) => {
store.dispatch(actions.setProviderType(type))
},
}
// start app // start app
render( render(
h(Root, { h(Root, {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save