commit
c83a9ceb04
@ -0,0 +1,202 @@ |
|||||||
|
version: 2 |
||||||
|
|
||||||
|
workflows: |
||||||
|
version: 2 |
||||||
|
full_test: |
||||||
|
jobs: |
||||||
|
- prep-deps-npm |
||||||
|
- prep-deps-firefox |
||||||
|
- prep-scss: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- test-lint: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- test-unit: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- test-integration-mascara-chrome: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- prep-scss |
||||||
|
- test-integration-mascara-firefox: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- prep-deps-firefox |
||||||
|
- prep-scss |
||||||
|
- test-integration-flat-chrome: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- prep-scss |
||||||
|
- test-integration-flat-firefox: |
||||||
|
requires: |
||||||
|
- prep-deps-npm |
||||||
|
- prep-deps-firefox |
||||||
|
- prep-scss |
||||||
|
|
||||||
|
jobs: |
||||||
|
prep-deps-npm: |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Install deps via npm |
||||||
|
command: npm install |
||||||
|
- save_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
paths: |
||||||
|
- node_modules |
||||||
|
|
||||||
|
prep-deps-firefox: |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- run: |
||||||
|
name: Download Firefox |
||||||
|
command: > |
||||||
|
wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 |
||||||
|
&& tar xjf firefox-58.0.tar.bz2 |
||||||
|
- save_cache: |
||||||
|
key: dependency-cache-firefox-{{ .Revision }} |
||||||
|
paths: |
||||||
|
- firefox |
||||||
|
|
||||||
|
|
||||||
|
prep-scss: |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Get Scss Cache key |
||||||
|
# this allows us to checksum against a whole directory |
||||||
|
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum |
||||||
|
- run: |
||||||
|
name: Build for integration tests |
||||||
|
command: npm run test:integration:build |
||||||
|
- save_cache: |
||||||
|
key: scss-cache-{{ checksum "scss_checksum" }} |
||||||
|
paths: |
||||||
|
- ui/app/css/output |
||||||
|
|
||||||
|
test-lint: |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Test |
||||||
|
command: npm run lint |
||||||
|
|
||||||
|
test-unit: |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: test:coverage |
||||||
|
command: npm run test:coverage |
||||||
|
|
||||||
|
test-integration-flat-firefox: |
||||||
|
environment: |
||||||
|
browsers: '["Firefox"]' |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-firefox-{{ .Revision }} |
||||||
|
- run: |
||||||
|
name: Install firefox |
||||||
|
command: > |
||||||
|
sudo rm -r /opt/firefox |
||||||
|
&& sudo mv firefox /opt/firefox58 |
||||||
|
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old |
||||||
|
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Get Scss Cache key |
||||||
|
# this allows us to checksum against a whole directory |
||||||
|
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum |
||||||
|
- restore_cache: |
||||||
|
key: scss-cache-{{ checksum "scss_checksum" }} |
||||||
|
- run: |
||||||
|
name: test:integration:flat |
||||||
|
command: npm run test:flat |
||||||
|
|
||||||
|
test-integration-flat-chrome: |
||||||
|
environment: |
||||||
|
browsers: '["Chrome"]' |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Get Scss Cache key |
||||||
|
# this allows us to checksum against a whole directory |
||||||
|
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum |
||||||
|
- restore_cache: |
||||||
|
key: scss-cache-{{ checksum "scss_checksum" }} |
||||||
|
- run: |
||||||
|
name: test:integration:flat |
||||||
|
command: npm run test:flat |
||||||
|
|
||||||
|
test-integration-mascara-firefox: |
||||||
|
environment: |
||||||
|
browsers: '["Firefox"]' |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-firefox-{{ .Revision }} |
||||||
|
- run: |
||||||
|
name: Install firefox |
||||||
|
command: > |
||||||
|
sudo rm -r /opt/firefox |
||||||
|
&& sudo mv firefox /opt/firefox58 |
||||||
|
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old |
||||||
|
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Get Scss Cache key |
||||||
|
# this allows us to checksum against a whole directory |
||||||
|
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum |
||||||
|
- restore_cache: |
||||||
|
key: scss-cache-{{ checksum "scss_checksum" }} |
||||||
|
- run: |
||||||
|
name: test:integration:mascara |
||||||
|
command: npm run test:mascara |
||||||
|
|
||||||
|
test-integration-mascara-chrome: |
||||||
|
environment: |
||||||
|
browsers: '["Chrome"]' |
||||||
|
docker: |
||||||
|
- image: circleci/node:8-browsers |
||||||
|
steps: |
||||||
|
- checkout |
||||||
|
- restore_cache: |
||||||
|
key: dependency-cache-{{ checksum "package-lock.json" }} |
||||||
|
- run: |
||||||
|
name: Get Scss Cache key |
||||||
|
# this allows us to checksum against a whole directory |
||||||
|
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum |
||||||
|
- restore_cache: |
||||||
|
key: scss-cache-{{ checksum "scss_checksum" }} |
||||||
|
- run: |
||||||
|
name: test:integration:mascara |
||||||
|
command: npm run test:mascara |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"exceptions": ["https://nodesecurity.io/advisories/566"] |
||||||
|
} |
@ -1,10 +1,609 @@ |
|||||||
{ |
{ |
||||||
|
"accept": { |
||||||
|
"message": "Accept" |
||||||
|
}, |
||||||
|
"account": { |
||||||
|
"message": "Account" |
||||||
|
}, |
||||||
|
"accountDetails": { |
||||||
|
"message": "Account Details" |
||||||
|
}, |
||||||
|
"accountName": { |
||||||
|
"message": "Account Name" |
||||||
|
}, |
||||||
|
"address": { |
||||||
|
"message": "Address" |
||||||
|
}, |
||||||
|
"addToken": { |
||||||
|
"message": "Add Token" |
||||||
|
}, |
||||||
|
"amount": { |
||||||
|
"message": "Amount" |
||||||
|
}, |
||||||
|
"amountPlusGas": { |
||||||
|
"message": "Amount + Gas" |
||||||
|
}, |
||||||
|
"appDescription": { |
||||||
|
"message": "Ethereum Browser Extension", |
||||||
|
"description": "The description of the application" |
||||||
|
}, |
||||||
"appName": { |
"appName": { |
||||||
"message": "MetaMask", |
"message": "MetaMask", |
||||||
"description": "The name of the application" |
"description": "The name of the application" |
||||||
}, |
}, |
||||||
"appDescription": { |
"attemptingConnect": { |
||||||
"message": "Ethereum Identity Management", |
"message": "Attempting to connect to blockchain." |
||||||
"description": "The description of the application" |
}, |
||||||
|
"available": { |
||||||
|
"message": "Available" |
||||||
|
}, |
||||||
|
"back": { |
||||||
|
"message": "Back" |
||||||
|
}, |
||||||
|
"balance": { |
||||||
|
"message": "Balance:" |
||||||
|
}, |
||||||
|
"balanceIsInsufficientGas": { |
||||||
|
"message": "Insufficient balance for current gas total" |
||||||
|
}, |
||||||
|
"beta": { |
||||||
|
"message": "BETA" |
||||||
|
}, |
||||||
|
"betweenMinAndMax": { |
||||||
|
"message": "must be greater than or equal to $1 and less than or equal to $2.", |
||||||
|
"description": "helper for inputting hex as decimal input" |
||||||
|
}, |
||||||
|
"borrowDharma": { |
||||||
|
"message": "Borrow With Dharma (Beta)" |
||||||
|
}, |
||||||
|
"buy": { |
||||||
|
"message": "Buy" |
||||||
|
}, |
||||||
|
"buyCoinbase": { |
||||||
|
"message": "Buy on Coinbase" |
||||||
|
}, |
||||||
|
"buyCoinbaseExplainer": { |
||||||
|
"message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin." |
||||||
|
}, |
||||||
|
"cancel": { |
||||||
|
"message": "Cancel" |
||||||
|
}, |
||||||
|
"clickCopy": { |
||||||
|
"message": "Click to Copy" |
||||||
|
}, |
||||||
|
"confirm": { |
||||||
|
"message": "Confirm" |
||||||
|
}, |
||||||
|
"confirmContract": { |
||||||
|
"message": "Confirm Contract" |
||||||
|
}, |
||||||
|
"confirmPassword": { |
||||||
|
"message": "Confirm Password" |
||||||
|
}, |
||||||
|
"confirmTransaction": { |
||||||
|
"message": "Confirm Transaction" |
||||||
|
}, |
||||||
|
"continueToCoinbase": { |
||||||
|
"message": "Continue to Coinbase" |
||||||
|
}, |
||||||
|
"contractDeployment": { |
||||||
|
"message": "Contract Deployment" |
||||||
|
}, |
||||||
|
"conversionProgress": { |
||||||
|
"message": "Conversion in progress" |
||||||
|
}, |
||||||
|
"copiedButton": { |
||||||
|
"message": "Copied" |
||||||
|
}, |
||||||
|
"copiedClipboard": { |
||||||
|
"message": "Copied to Clipboard" |
||||||
|
}, |
||||||
|
"copiedExclamation": { |
||||||
|
"message": "Copied!" |
||||||
|
}, |
||||||
|
"copy": { |
||||||
|
"message": "Copy" |
||||||
|
}, |
||||||
|
"copyToClipboard": { |
||||||
|
"message": "Copy to clipboard" |
||||||
|
}, |
||||||
|
"copyButton": { |
||||||
|
"message": " Copy " |
||||||
|
}, |
||||||
|
"copyPrivateKey": { |
||||||
|
"message": "This is your private key (click to copy)" |
||||||
|
}, |
||||||
|
"create": { |
||||||
|
"message": "Create" |
||||||
|
}, |
||||||
|
"createAccount": { |
||||||
|
"message": "Create Account" |
||||||
|
}, |
||||||
|
"createDen": { |
||||||
|
"message": "Create" |
||||||
|
}, |
||||||
|
"crypto": { |
||||||
|
"message": "Crypto", |
||||||
|
"description": "Exchange type (cryptocurrencies)" |
||||||
|
}, |
||||||
|
"customGas": { |
||||||
|
"message": "Customize Gas" |
||||||
|
}, |
||||||
|
"customize": { |
||||||
|
"message": "Customize" |
||||||
|
}, |
||||||
|
"customRPC": { |
||||||
|
"message": "Custom RPC" |
||||||
|
}, |
||||||
|
"defaultNetwork": { |
||||||
|
"message": "The default network for Ether transactions is Main Net." |
||||||
|
}, |
||||||
|
"denExplainer": { |
||||||
|
"message": "Your DEN is your password-encrypted storage within MetaMask." |
||||||
|
}, |
||||||
|
"deposit": { |
||||||
|
"message": "Deposit" |
||||||
|
}, |
||||||
|
"depositBTC": { |
||||||
|
"message": "Deposit your BTC to the address below:" |
||||||
|
}, |
||||||
|
"depositCoin": { |
||||||
|
"message": "Deposit your $1 to the address below", |
||||||
|
"description": "Tells the user what coin they have selected to deposit with shapeshift" |
||||||
|
}, |
||||||
|
"depositEth": { |
||||||
|
"message": "Deposit Eth" |
||||||
|
}, |
||||||
|
"depositEther": { |
||||||
|
"message": "Deposit Ether" |
||||||
|
}, |
||||||
|
"depositFiat": { |
||||||
|
"message": "Deposit with Fiat" |
||||||
|
}, |
||||||
|
"depositFromAccount": { |
||||||
|
"message": "Deposit from another account" |
||||||
|
}, |
||||||
|
"depositShapeShift": { |
||||||
|
"message": "Deposit with ShapeShift" |
||||||
|
}, |
||||||
|
"depositShapeShiftExplainer": { |
||||||
|
"message": "If you own other cryptocurrencies, you can trade and deposit Ether directly into your MetaMask wallet. No Account Needed." |
||||||
|
}, |
||||||
|
"details": { |
||||||
|
"message": "Details" |
||||||
|
}, |
||||||
|
"directDeposit": { |
||||||
|
"message": "Direct Deposit" |
||||||
|
}, |
||||||
|
"directDepositEther": { |
||||||
|
"message": "Directly Deposit Ether" |
||||||
|
}, |
||||||
|
"directDepositEtherExplainer": { |
||||||
|
"message": "If you already have some Ether, the quickest way to get Ether in your new wallet by direct deposit." |
||||||
|
}, |
||||||
|
"done": { |
||||||
|
"message": "Done" |
||||||
|
}, |
||||||
|
"edit": { |
||||||
|
"message": "Edit" |
||||||
|
}, |
||||||
|
"editAccountName": { |
||||||
|
"message": "Edit Account Name" |
||||||
|
}, |
||||||
|
"encryptNewDen": { |
||||||
|
"message": "Encrypt your new DEN" |
||||||
|
}, |
||||||
|
"enterPassword": { |
||||||
|
"message": "Enter password" |
||||||
|
}, |
||||||
|
"etherscanView": { |
||||||
|
"message": "View account on Etherscan" |
||||||
|
}, |
||||||
|
"exchangeRate": { |
||||||
|
"message": "Exchange Rate" |
||||||
|
}, |
||||||
|
"exportPrivateKey": { |
||||||
|
"message": "Export Private Key" |
||||||
|
}, |
||||||
|
"exportPrivateKeyWarning": { |
||||||
|
"message": "Export private keys at your own risk." |
||||||
|
}, |
||||||
|
"failed": { |
||||||
|
"message": "Failed" |
||||||
|
}, |
||||||
|
"fiat": { |
||||||
|
"message": "FIAT", |
||||||
|
"description": "Exchange type" |
||||||
|
}, |
||||||
|
"fileImportFail": { |
||||||
|
"message": "File import not working? Click here!", |
||||||
|
"description": "Helps user import their account from a JSON file" |
||||||
|
}, |
||||||
|
"from": { |
||||||
|
"message": "From" |
||||||
|
}, |
||||||
|
"fromShapeShift": { |
||||||
|
"message": "From ShapeShift" |
||||||
|
}, |
||||||
|
"gas": { |
||||||
|
"message": "Gas", |
||||||
|
"description": "Short indication of gas cost" |
||||||
|
}, |
||||||
|
"gasFee": { |
||||||
|
"message": "Gas Fee" |
||||||
|
}, |
||||||
|
"gasLimit": { |
||||||
|
"message": "Gas Limit" |
||||||
|
}, |
||||||
|
"gasLimitCalculation": { |
||||||
|
"message": "We calculate the suggested gas limit based on network success rates." |
||||||
|
}, |
||||||
|
"gasLimitRequired": { |
||||||
|
"message": "Gas Limit Required" |
||||||
|
}, |
||||||
|
"gasLimitTooLow": { |
||||||
|
"message": "Gas limit must be at least 21000" |
||||||
|
}, |
||||||
|
"gasPrice": { |
||||||
|
"message": "Gas Price (GWEI)" |
||||||
|
}, |
||||||
|
"gasPriceCalculation": { |
||||||
|
"message": "We calculate the suggested gas prices based on network success rates." |
||||||
|
}, |
||||||
|
"gasPriceRequired": { |
||||||
|
"message": "Gas Price Required" |
||||||
|
}, |
||||||
|
"getEther": { |
||||||
|
"message": "Get Ether" |
||||||
|
}, |
||||||
|
"getEtherFromFaucet": { |
||||||
|
"message": "Get Ether from a faucet for the $1", |
||||||
|
"description": "Displays network name for Ether faucet" |
||||||
|
}, |
||||||
|
"greaterThanMin": { |
||||||
|
"message": "must be greater than or equal to $1.", |
||||||
|
"description": "helper for inputting hex as decimal input" |
||||||
|
}, |
||||||
|
"here": { |
||||||
|
"message": "here", |
||||||
|
"description": "as in -click here- for more information (goes with troubleTokenBalances)" |
||||||
|
}, |
||||||
|
"hide": { |
||||||
|
"message": "Hide" |
||||||
|
}, |
||||||
|
"hideToken": { |
||||||
|
"message": "Hide Token" |
||||||
|
}, |
||||||
|
"hideTokenPrompt": { |
||||||
|
"message": "Hide Token?" |
||||||
|
}, |
||||||
|
"howToDeposit": { |
||||||
|
"message": "How would you like to deposit Ether?" |
||||||
|
}, |
||||||
|
"import": { |
||||||
|
"message": "Import", |
||||||
|
"description": "Button to import an account from a selected file" |
||||||
|
}, |
||||||
|
"importAccount": { |
||||||
|
"message": "Import Account" |
||||||
|
}, |
||||||
|
"importAnAccount": { |
||||||
|
"message": "Import an account" |
||||||
|
}, |
||||||
|
"importDen": { |
||||||
|
"message": "Import Existing DEN" |
||||||
|
}, |
||||||
|
"imported": { |
||||||
|
"message": "Imported", |
||||||
|
"description": "status showing that an account has been fully loaded into the keyring" |
||||||
|
}, |
||||||
|
"infoHelp": { |
||||||
|
"message": "Info & Help" |
||||||
|
}, |
||||||
|
"invalidAddress": { |
||||||
|
"message": "Invalid address" |
||||||
|
}, |
||||||
|
"invalidGasParams": { |
||||||
|
"message": "Invalid Gas Parameters" |
||||||
|
}, |
||||||
|
"invalidInput": { |
||||||
|
"message": "Invalid input." |
||||||
|
}, |
||||||
|
"invalidRequest": { |
||||||
|
"message": "Invalid Request" |
||||||
|
}, |
||||||
|
"jsonFile": { |
||||||
|
"message": "JSON File", |
||||||
|
"description": "format for importing an account" |
||||||
|
}, |
||||||
|
"kovan": { |
||||||
|
"message": "Kovan Test Network" |
||||||
|
}, |
||||||
|
"lessThanMax": { |
||||||
|
"message": "must be less than or equal to $1.", |
||||||
|
"description": "helper for inputting hex as decimal input" |
||||||
|
}, |
||||||
|
"limit": { |
||||||
|
"message": "Limit" |
||||||
|
}, |
||||||
|
"loading": { |
||||||
|
"message": "Loading..." |
||||||
|
}, |
||||||
|
"loadingTokens": { |
||||||
|
"message": "Loading Tokens..." |
||||||
|
}, |
||||||
|
"localhost": { |
||||||
|
"message": "Localhost 8545" |
||||||
|
}, |
||||||
|
"logout": { |
||||||
|
"message": "Log out" |
||||||
|
}, |
||||||
|
"loose": { |
||||||
|
"message": "Loose" |
||||||
|
}, |
||||||
|
"mainnet": { |
||||||
|
"message": "Main Ethereum Network" |
||||||
|
}, |
||||||
|
"message": { |
||||||
|
"message": "Message" |
||||||
|
}, |
||||||
|
"min": { |
||||||
|
"message": "Minimum" |
||||||
|
}, |
||||||
|
"myAccounts": { |
||||||
|
"message": "My Accounts" |
||||||
|
}, |
||||||
|
"needEtherInWallet": { |
||||||
|
"message": "To interact with decentralized applications using MetaMask, you’ll need Ether in your wallet." |
||||||
|
}, |
||||||
|
"needImportFile": { |
||||||
|
"message": "You must select a file to import.", |
||||||
|
"description": "User is important an account and needs to add a file to continue" |
||||||
|
}, |
||||||
|
"needImportPassword": { |
||||||
|
"message": "You must enter a password for the selected file.", |
||||||
|
"description": "Password and file needed to import an account" |
||||||
|
}, |
||||||
|
"networks": { |
||||||
|
"message": "Networks" |
||||||
|
}, |
||||||
|
"newAccount": { |
||||||
|
"message": "New Account" |
||||||
|
}, |
||||||
|
"newAccountNumberName": { |
||||||
|
"message": "Account $1", |
||||||
|
"description": "Default name of next account to be created on create account screen" |
||||||
|
}, |
||||||
|
"newContract": { |
||||||
|
"message": "New Contract" |
||||||
|
}, |
||||||
|
"newPassword": { |
||||||
|
"message": "New Password (min 8 chars)" |
||||||
|
}, |
||||||
|
"newRecipient": { |
||||||
|
"message": "New Recipient" |
||||||
|
}, |
||||||
|
"next": { |
||||||
|
"message": "Next" |
||||||
|
}, |
||||||
|
"noAddressForName": { |
||||||
|
"message": "No address has been set for this name." |
||||||
|
}, |
||||||
|
"noDeposits": { |
||||||
|
"message": "No deposits received" |
||||||
|
}, |
||||||
|
"noTransactionHistory": { |
||||||
|
"message": "No transaction history." |
||||||
|
}, |
||||||
|
"noTransactions": { |
||||||
|
"message": "No Transactions" |
||||||
|
}, |
||||||
|
"notStarted": { |
||||||
|
"message": "Not Started" |
||||||
|
}, |
||||||
|
"oldUI": { |
||||||
|
"message": "Old UI" |
||||||
|
}, |
||||||
|
"oldUIMessage": { |
||||||
|
"message": "You have returned to the old UI. You can switch back to the New UI through the option in the top right dropdown menu." |
||||||
|
}, |
||||||
|
"or": { |
||||||
|
"message": "or", |
||||||
|
"description": "choice between creating or importing a new account" |
||||||
|
}, |
||||||
|
"passwordMismatch": { |
||||||
|
"message": "passwords don't match", |
||||||
|
"description": "in password creation process, the two new password fields did not match" |
||||||
|
}, |
||||||
|
"passwordShort": { |
||||||
|
"message": "password not long enough", |
||||||
|
"description": "in password creation process, the password is not long enough to be secure" |
||||||
|
}, |
||||||
|
"pastePrivateKey": { |
||||||
|
"message": "Paste your private key string here:", |
||||||
|
"description": "For importing an account from a private key" |
||||||
|
}, |
||||||
|
"pasteSeed": { |
||||||
|
"message": "Paste your seed phrase here!" |
||||||
|
}, |
||||||
|
"pleaseReviewTransaction": { |
||||||
|
"message": "Please review your transaction." |
||||||
|
}, |
||||||
|
"privateKey": { |
||||||
|
"message": "Private Key", |
||||||
|
"description": "select this type of file to use to import an account" |
||||||
|
}, |
||||||
|
"privateKeyWarning": { |
||||||
|
"message": "Warning: Never disclose this key. Anyone with your private keys can take steal any assets held in your account." |
||||||
|
}, |
||||||
|
"privateNetwork": { |
||||||
|
"message": "Private Network" |
||||||
|
}, |
||||||
|
"qrCode": { |
||||||
|
"message": "Show QR Code" |
||||||
|
}, |
||||||
|
"readdToken": { |
||||||
|
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu." |
||||||
|
}, |
||||||
|
"readMore": { |
||||||
|
"message": "Read more here." |
||||||
|
}, |
||||||
|
"receive": { |
||||||
|
"message": "Receive" |
||||||
|
}, |
||||||
|
"recipientAddress": { |
||||||
|
"message": "Recipient Address" |
||||||
|
}, |
||||||
|
"refundAddress": { |
||||||
|
"message": "Your Refund Address" |
||||||
|
}, |
||||||
|
"rejected": { |
||||||
|
"message": "Rejected" |
||||||
|
}, |
||||||
|
"required": { |
||||||
|
"message": "Required" |
||||||
|
}, |
||||||
|
"retryWithMoreGas": { |
||||||
|
"message": "Retry with a higher gas price here" |
||||||
|
}, |
||||||
|
"revert": { |
||||||
|
"message": "Revert" |
||||||
|
}, |
||||||
|
"rinkeby": { |
||||||
|
"message": "Rinkeby Test Network" |
||||||
|
}, |
||||||
|
"ropsten": { |
||||||
|
"message": "Ropsten Test Network" |
||||||
|
}, |
||||||
|
"sampleAccountName": { |
||||||
|
"message": "E.g. My new account", |
||||||
|
"description": "Help user understand concept of adding a human-readable name to their account" |
||||||
|
}, |
||||||
|
"save": { |
||||||
|
"message": "Save" |
||||||
|
}, |
||||||
|
"saveAsFile": { |
||||||
|
"message": "Save as File", |
||||||
|
"description": "Account export process" |
||||||
|
}, |
||||||
|
"selectService": { |
||||||
|
"message": "Select Service" |
||||||
|
}, |
||||||
|
"send": { |
||||||
|
"message": "Send" |
||||||
|
}, |
||||||
|
"sendTokens": { |
||||||
|
"message": "Send Tokens" |
||||||
|
}, |
||||||
|
"sendTokensAnywhere": { |
||||||
|
"message": "Send Tokens to anyone with an Ethereum account" |
||||||
|
}, |
||||||
|
"settings": { |
||||||
|
"message": "Settings" |
||||||
|
}, |
||||||
|
"shapeshiftBuy": { |
||||||
|
"message": "Buy with Shapeshift" |
||||||
|
}, |
||||||
|
"showPrivateKeys": { |
||||||
|
"message": "Show Private Keys" |
||||||
|
}, |
||||||
|
"showQRCode": { |
||||||
|
"message": "Show QR Code" |
||||||
|
}, |
||||||
|
"sign": { |
||||||
|
"message": "Sign" |
||||||
|
}, |
||||||
|
"signMessage": { |
||||||
|
"message": "Sign Message" |
||||||
|
}, |
||||||
|
"signNotice": { |
||||||
|
"message": "Signing this message can have \ndangerous side effects. Only sign messages from \nsites you fully trust with your entire account.\n This dangerous method will be removed in a future version. " |
||||||
|
}, |
||||||
|
"sigRequest": { |
||||||
|
"message": "Signature Request" |
||||||
|
}, |
||||||
|
"sigRequested": { |
||||||
|
"message": "Signature Requested" |
||||||
|
}, |
||||||
|
"status": { |
||||||
|
"message": "Status" |
||||||
|
}, |
||||||
|
"submit": { |
||||||
|
"message": "Submit" |
||||||
|
}, |
||||||
|
"takesTooLong": { |
||||||
|
"message": "Taking too long?" |
||||||
|
}, |
||||||
|
"testFaucet": { |
||||||
|
"message": "Test Faucet" |
||||||
|
}, |
||||||
|
"to": { |
||||||
|
"message": "To" |
||||||
|
}, |
||||||
|
"toETHviaShapeShift": { |
||||||
|
"message": "$1 to ETH via ShapeShift", |
||||||
|
"description": "system will fill in deposit type in start of message" |
||||||
|
}, |
||||||
|
"tokenBalance": { |
||||||
|
"message": "Your Token Balance is:" |
||||||
|
}, |
||||||
|
"total": { |
||||||
|
"message": "Total" |
||||||
|
}, |
||||||
|
"transactionMemo": { |
||||||
|
"message": "Transaction memo (optional)" |
||||||
|
}, |
||||||
|
"transactionNumber": { |
||||||
|
"message": "Transaction Number" |
||||||
|
}, |
||||||
|
"transfers": { |
||||||
|
"message": "Transfers" |
||||||
|
}, |
||||||
|
"troubleTokenBalances": { |
||||||
|
"message": "We had trouble loading your token balances. You can view them ", |
||||||
|
"description": "Followed by a link (here) to view token balances" |
||||||
|
}, |
||||||
|
"typePassword": { |
||||||
|
"message": "Type Your Password" |
||||||
|
}, |
||||||
|
"uiWelcome": { |
||||||
|
"message": "Welcome to the New UI (Beta)" |
||||||
|
}, |
||||||
|
"uiWelcomeMessage": { |
||||||
|
"message": "You are now using the new Metamask UI. Take a look around, try out new features like sending tokens, and let us know if you have any issues." |
||||||
|
}, |
||||||
|
"unavailable": { |
||||||
|
"message": "Unavailable" |
||||||
|
}, |
||||||
|
"unknown": { |
||||||
|
"message": "Unknown" |
||||||
|
}, |
||||||
|
"unknownNetwork": { |
||||||
|
"message": "Unknown Private Network" |
||||||
|
}, |
||||||
|
"unknownNetworkId": { |
||||||
|
"message": "Unknown network ID" |
||||||
|
}, |
||||||
|
"usaOnly": { |
||||||
|
"message": "USA only", |
||||||
|
"description": "Using this exchange is limited to people inside the USA" |
||||||
|
}, |
||||||
|
"usedByClients": { |
||||||
|
"message": "Used by a variety of different clients" |
||||||
|
}, |
||||||
|
"viewAccount": { |
||||||
|
"message": "View Account" |
||||||
|
}, |
||||||
|
"warning": { |
||||||
|
"message": "Warning" |
||||||
|
}, |
||||||
|
"whatsThis": { |
||||||
|
"message": "What's this?" |
||||||
|
}, |
||||||
|
"yourSigRequested": { |
||||||
|
"message": "Your signature is being requested" |
||||||
|
}, |
||||||
|
"youSign": { |
||||||
|
"message": "You are signing" |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,48 @@ |
|||||||
|
const KeyringController = require('eth-keyring-controller') |
||||||
|
|
||||||
|
const seedPhraseVerifier = { |
||||||
|
|
||||||
|
// Verifies if the seed words can restore the accounts.
|
||||||
|
//
|
||||||
|
// The seed words can recreate the primary keyring and the accounts belonging to it.
|
||||||
|
// The created accounts in the primary keyring are always the same.
|
||||||
|
// The keyring always creates the accounts in the same sequence.
|
||||||
|
verifyAccounts (createdAccounts, seedWords) { |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
|
||||||
|
if (!createdAccounts || createdAccounts.length < 1) { |
||||||
|
return reject(new Error('No created accounts defined.')) |
||||||
|
} |
||||||
|
|
||||||
|
const keyringController = new KeyringController({}) |
||||||
|
const Keyring = keyringController.getKeyringClassForType('HD Key Tree') |
||||||
|
const opts = { |
||||||
|
mnemonic: seedWords, |
||||||
|
numberOfAccounts: createdAccounts.length, |
||||||
|
} |
||||||
|
|
||||||
|
const keyring = new Keyring(opts) |
||||||
|
keyring.getAccounts() |
||||||
|
.then((restoredAccounts) => { |
||||||
|
|
||||||
|
log.debug('Created accounts: ' + JSON.stringify(createdAccounts)) |
||||||
|
log.debug('Restored accounts: ' + JSON.stringify(restoredAccounts)) |
||||||
|
|
||||||
|
if (restoredAccounts.length !== createdAccounts.length) { |
||||||
|
// this should not happen...
|
||||||
|
return reject(new Error('Wrong number of accounts')) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < restoredAccounts.length; i++) { |
||||||
|
if (restoredAccounts[i].toLowerCase() !== createdAccounts[i].toLowerCase()) { |
||||||
|
return reject(new Error('Not identical accounts! Original: ' + createdAccounts[i] + ', Restored: ' + restoredAccounts[i])) |
||||||
|
} |
||||||
|
} |
||||||
|
return resolve() |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = seedPhraseVerifier |
@ -1,17 +0,0 @@ |
|||||||
machine: |
|
||||||
node: |
|
||||||
version: 8.1.4 |
|
||||||
test: |
|
||||||
override: |
|
||||||
- "npm test" |
|
||||||
dependencies: |
|
||||||
pre: |
|
||||||
- sudo apt-get update |
|
||||||
# get latest stable firefox |
|
||||||
- sudo apt-get install firefox |
|
||||||
- firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd |
|
||||||
# get latest stable chrome |
|
||||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - |
|
||||||
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' |
|
||||||
- sudo apt-get update |
|
||||||
- sudo apt-get install google-chrome-stable |
|
@ -1,18 +1,21 @@ |
|||||||
const fs = require('fs') |
const fs = require('fs') |
||||||
const path = require('path') |
const path = require('path') |
||||||
|
const { promisify } = require('util') |
||||||
|
|
||||||
const statesPath = path.join(__dirname, 'states') |
start().catch(console.error) |
||||||
const stateNames = fs.readdirSync(statesPath) |
|
||||||
|
|
||||||
const states = stateNames.reduce((result, stateFileName) => { |
async function start () { |
||||||
const statePath = path.join(__dirname, 'states', stateFileName) |
const statesPath = path.join(__dirname, 'states') |
||||||
const stateFile = fs.readFileSync(statePath).toString() |
const stateFilesNames = await promisify(fs.readdir)(statesPath) |
||||||
const state = JSON.parse(stateFile) |
const states = {} |
||||||
result[stateFileName.split('.')[0].replace(/-/g, ' ', 'g')] = state |
await Promise.all(stateFilesNames.map(async (stateFileName) => { |
||||||
return result |
const stateFilePath = path.join(__dirname, 'states', stateFileName) |
||||||
}, {}) |
const stateFileContent = await promisify(fs.readFile)(stateFilePath, 'utf8') |
||||||
|
const state = JSON.parse(stateFileContent) |
||||||
const result = `module.exports = ${JSON.stringify(states)}` |
const stateName = stateFileName.split('.')[0].replace(/-/g, ' ', 'g') |
||||||
|
states[stateName] = state |
||||||
const statesJsonPath = path.join(__dirname, 'states.js') |
})) |
||||||
fs.writeFileSync(statesJsonPath, result) |
const generatedFileContent = `module.exports = ${JSON.stringify(states)}` |
||||||
|
const generatedFilePath = path.join(__dirname, 'states.js') |
||||||
|
await promisify(fs.writeFile)(generatedFilePath, generatedFileContent) |
||||||
|
} |
||||||
|
@ -0,0 +1,18 @@ |
|||||||
|
# MetaMask Translation Guide |
||||||
|
|
||||||
|
The MetaMask browser extension supports new translations added in the form of new locales files added in `app/_locales`. |
||||||
|
|
||||||
|
- [The MDN Guide to Internationalizing Extensions](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Internationalization) |
||||||
|
|
||||||
|
## 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/)). |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
## Testing |
||||||
|
|
||||||
|
To verify that your translation works, you will need to [build a local copy](https://github.com/MetaMask/metamask-extension#building-locally) of MetaMask. |
||||||
|
|
@ -1,100 +1,122 @@ |
|||||||
const inherits = require('util').inherits |
|
||||||
const Component = require('react').Component |
const Component = require('react').Component |
||||||
const h = require('react-hyperscript') |
const h = require('react-hyperscript') |
||||||
const connect = require('react-redux').connect |
const connect = require('react-redux').connect |
||||||
const actions = require('../../../../ui/app/actions') |
const actions = require('../../../../ui/app/actions') |
||||||
const FileInput = require('react-simple-file-input').default |
const FileInput = require('react-simple-file-input').default |
||||||
|
const PropTypes = require('prop-types') |
||||||
|
|
||||||
const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' |
const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' |
||||||
|
|
||||||
module.exports = connect(mapStateToProps)(JsonImportSubview) |
class JsonImportSubview extends Component { |
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
function mapStateToProps (state) { |
this.state = { |
||||||
return { |
file: null, |
||||||
error: state.appState.warning, |
fileContents: '', |
||||||
|
} |
||||||
} |
} |
||||||
} |
|
||||||
|
|
||||||
inherits(JsonImportSubview, Component) |
render () { |
||||||
function JsonImportSubview () { |
const { error } = this.props |
||||||
Component.call(this) |
|
||||||
} |
|
||||||
|
|
||||||
JsonImportSubview.prototype.render = function () { |
return ( |
||||||
const { error } = this.props |
h('div', { |
||||||
|
|
||||||
return ( |
|
||||||
h('div', { |
|
||||||
style: { |
|
||||||
display: 'flex', |
|
||||||
flexDirection: 'column', |
|
||||||
alignItems: 'center', |
|
||||||
padding: '5px 15px 0px 15px', |
|
||||||
}, |
|
||||||
}, [ |
|
||||||
|
|
||||||
h('p', 'Used by a variety of different clients'), |
|
||||||
h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), |
|
||||||
|
|
||||||
h(FileInput, { |
|
||||||
readAs: 'text', |
|
||||||
onLoad: this.onLoad.bind(this), |
|
||||||
style: { |
style: { |
||||||
margin: '20px 0px 12px 20px', |
display: 'flex', |
||||||
fontSize: '15px', |
flexDirection: 'column', |
||||||
|
alignItems: 'center', |
||||||
|
padding: '5px 15px 0px 15px', |
||||||
}, |
}, |
||||||
}), |
}, [ |
||||||
|
|
||||||
|
h('p', 'Used by a variety of different clients'), |
||||||
|
h('a.warning', { |
||||||
|
href: HELP_LINK, |
||||||
|
target: '_blank', |
||||||
|
}, 'File import not working? Click here!'), |
||||||
|
|
||||||
|
h(FileInput, { |
||||||
|
readAs: 'text', |
||||||
|
onLoad: this.onLoad.bind(this), |
||||||
|
style: { |
||||||
|
margin: '20px 0px 12px 20px', |
||||||
|
fontSize: '15px', |
||||||
|
}, |
||||||
|
}), |
||||||
|
|
||||||
|
h('input.large-input.letter-spacey', { |
||||||
|
type: 'password', |
||||||
|
placeholder: 'Enter password', |
||||||
|
id: 'json-password-box', |
||||||
|
onKeyPress: this.createKeyringOnEnter.bind(this), |
||||||
|
style: { |
||||||
|
width: 260, |
||||||
|
marginTop: 12, |
||||||
|
}, |
||||||
|
}), |
||||||
|
|
||||||
|
h('button.primary', { |
||||||
|
onClick: this.createNewKeychain.bind(this), |
||||||
|
style: { |
||||||
|
margin: 12, |
||||||
|
}, |
||||||
|
}, 'Import'), |
||||||
|
|
||||||
|
error ? h('span.error', error) : null, |
||||||
|
]) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
h('input.large-input.letter-spacey', { |
onLoad (event, file) { |
||||||
type: 'password', |
this.setState({file: file, fileContents: event.target.result}) |
||||||
placeholder: 'Enter password', |
} |
||||||
id: 'json-password-box', |
|
||||||
onKeyPress: this.createKeyringOnEnter.bind(this), |
|
||||||
style: { |
|
||||||
width: 260, |
|
||||||
marginTop: 12, |
|
||||||
}, |
|
||||||
}), |
|
||||||
|
|
||||||
h('button.primary', { |
createKeyringOnEnter (event) { |
||||||
onClick: this.createNewKeychain.bind(this), |
if (event.key === 'Enter') { |
||||||
style: { |
event.preventDefault() |
||||||
margin: 12, |
this.createNewKeychain() |
||||||
}, |
} |
||||||
}, 'Import'), |
} |
||||||
|
|
||||||
error ? h('span.error', error) : null, |
createNewKeychain () { |
||||||
]) |
const { fileContents } = this.state |
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
JsonImportSubview.prototype.onLoad = function (event, file) { |
if (!fileContents) { |
||||||
this.setState({file: file, fileContents: event.target.result}) |
const message = 'You must select a file to import.' |
||||||
} |
return this.props.displayWarning(message) |
||||||
|
} |
||||||
|
|
||||||
JsonImportSubview.prototype.createKeyringOnEnter = function (event) { |
const passwordInput = document.getElementById('json-password-box') |
||||||
if (event.key === 'Enter') { |
const password = passwordInput.value |
||||||
event.preventDefault() |
|
||||||
this.createNewKeychain() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
JsonImportSubview.prototype.createNewKeychain = function () { |
if (!password) { |
||||||
const state = this.state |
const message = 'You must enter a password for the selected file.' |
||||||
const { fileContents } = state |
return this.props.displayWarning(message) |
||||||
|
} |
||||||
|
|
||||||
if (!fileContents) { |
this.props.importNewAccount([ fileContents, password ]) |
||||||
const message = 'You must select a file to import.' |
|
||||||
return this.props.dispatch(actions.displayWarning(message)) |
|
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
const passwordInput = document.getElementById('json-password-box') |
JsonImportSubview.propTypes = { |
||||||
const password = passwordInput.value |
error: PropTypes.string, |
||||||
|
displayWarning: PropTypes.func, |
||||||
|
importNewAccount: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
if (!password) { |
const mapStateToProps = state => { |
||||||
const message = 'You must enter a password for the selected file.' |
return { |
||||||
return this.props.dispatch(actions.displayWarning(message)) |
error: state.appState.warning, |
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) |
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
goHome: () => dispatch(actions.goHome()), |
||||||
|
displayWarning: warning => dispatch(actions.displayWarning(warning)), |
||||||
|
importNewAccount: options => dispatch(actions.importNewAccount('JSON File', options)), |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
module.exports = connect(mapStateToProps, mapDispatchToProps)(JsonImportSubview) |
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,53 @@ |
|||||||
|
module.exports = { |
||||||
|
timeout, |
||||||
|
queryAsync, |
||||||
|
findAsync, |
||||||
|
pollUntilTruthy, |
||||||
|
} |
||||||
|
|
||||||
|
function timeout (time) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
setTimeout(resolve, time || 1500) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async function findAsync(container, selector, opts) { |
||||||
|
try { |
||||||
|
return await pollUntilTruthy(() => { |
||||||
|
const result = container.find(selector) |
||||||
|
if (result.length > 0) return result |
||||||
|
}, opts) |
||||||
|
} catch (err) { |
||||||
|
throw new Error(`Failed to find element within interval: "${selector}"`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function queryAsync(jQuery, selector, opts) { |
||||||
|
try { |
||||||
|
return await pollUntilTruthy(() => { |
||||||
|
const result = jQuery(selector) |
||||||
|
if (result.length > 0) return result |
||||||
|
}, opts) |
||||||
|
} catch (err) { |
||||||
|
throw new Error(`Failed to find element within interval: "${selector}"`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function pollUntilTruthy(fn, opts = {}){ |
||||||
|
const pollingInterval = opts.pollingInterval || 100 |
||||||
|
const timeoutInterval = opts.timeoutInterval || 5000 |
||||||
|
const start = Date.now() |
||||||
|
let result |
||||||
|
while (!result) { |
||||||
|
// check if timedout
|
||||||
|
const now = Date.now() |
||||||
|
if ((now - start) > timeoutInterval) { |
||||||
|
throw new Error(`pollUntilTruthy - failed to return truthy within interval`) |
||||||
|
} |
||||||
|
// check for result
|
||||||
|
result = fn() |
||||||
|
// run again after timeout
|
||||||
|
await timeout(pollingInterval, timeoutInterval) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -1,129 +1,103 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const sinon = require('sinon') |
const sinon = require('sinon') |
||||||
const clone = require('clone') |
const clone = require('clone') |
||||||
|
const nock = require('nock') |
||||||
const MetaMaskController = require('../../app/scripts/metamask-controller') |
const MetaMaskController = require('../../app/scripts/metamask-controller') |
||||||
|
const blacklistJSON = require('../stub/blacklist') |
||||||
const firstTimeState = require('../../app/scripts/first-time-state') |
const firstTimeState = require('../../app/scripts/first-time-state') |
||||||
const BN = require('ethereumjs-util').BN |
|
||||||
const GWEI_BN = new BN('1000000000') |
|
||||||
|
|
||||||
describe('MetaMaskController', function () { |
describe('MetaMaskController', function () { |
||||||
const noop = () => {} |
let metamaskController |
||||||
const metamaskController = new MetaMaskController({ |
const sandbox = sinon.sandbox.create() |
||||||
showUnconfirmedMessage: noop, |
const noop = () => { } |
||||||
unlockAccountMessage: noop, |
|
||||||
showUnapprovedTx: noop, |
|
||||||
platform: {}, |
|
||||||
encryptor: { |
|
||||||
encrypt: function(password, object) { |
|
||||||
this.object = object |
|
||||||
return Promise.resolve() |
|
||||||
}, |
|
||||||
decrypt: function () { |
|
||||||
return Promise.resolve(this.object) |
|
||||||
} |
|
||||||
}, |
|
||||||
// initial state
|
|
||||||
initState: clone(firstTimeState), |
|
||||||
}) |
|
||||||
|
|
||||||
beforeEach(function () { |
beforeEach(function () { |
||||||
// sinon allows stubbing methods that are easily verified
|
|
||||||
this.sinon = sinon.sandbox.create() |
nock('https://api.infura.io') |
||||||
|
.persist() |
||||||
|
.get('/v2/blacklist') |
||||||
|
.reply(200, blacklistJSON) |
||||||
|
|
||||||
|
nock('https://api.infura.io') |
||||||
|
.persist() |
||||||
|
.get(/.*/) |
||||||
|
.reply(200) |
||||||
|
|
||||||
|
metamaskController = new MetaMaskController({ |
||||||
|
showUnapprovedTx: noop, |
||||||
|
encryptor: { |
||||||
|
encrypt: function (password, object) { |
||||||
|
this.object = object |
||||||
|
return Promise.resolve() |
||||||
|
}, |
||||||
|
decrypt: function () { |
||||||
|
return Promise.resolve(this.object) |
||||||
|
}, |
||||||
|
}, |
||||||
|
initState: clone(firstTimeState), |
||||||
|
}) |
||||||
|
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||||
|
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||||
}) |
}) |
||||||
|
|
||||||
afterEach(function () { |
afterEach(function () { |
||||||
// sinon requires cleanup otherwise it will overwrite context
|
nock.cleanAll() |
||||||
this.sinon.restore() |
sandbox.restore() |
||||||
}) |
}) |
||||||
|
|
||||||
describe('Metamask Controller', function () { |
describe('#getGasPrice', function () { |
||||||
assert(metamaskController) |
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
||||||
|
const realRecentBlocksController = metamaskController.recentBlocksController |
||||||
beforeEach(function () { |
metamaskController.recentBlocksController = { |
||||||
sinon.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
store: { |
||||||
sinon.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
getState: () => { |
||||||
}) |
return { |
||||||
|
recentBlocks: [ |
||||||
afterEach(function () { |
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||||
metamaskController.keyringController.createNewVaultAndKeychain.restore() |
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||||
metamaskController.keyringController.createNewVaultAndRestore.restore() |
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||||
}) |
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||||
|
], |
||||||
describe('#getGasPrice', function () { |
|
||||||
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
|
||||||
const realRecentBlocksController = metamaskController.recentBlocksController |
|
||||||
metamaskController.recentBlocksController = { |
|
||||||
store: { |
|
||||||
getState: () => { |
|
||||||
return { |
|
||||||
recentBlocks: [ |
|
||||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
|
||||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
|
||||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
|
||||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const gasPrice = metamaskController.getGasPrice() |
|
||||||
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
|
||||||
|
|
||||||
metamaskController.recentBlocksController = realRecentBlocksController |
|
||||||
}) |
|
||||||
|
|
||||||
it('gives the 1 gwei price if no blocks have been seen.', async function () { |
|
||||||
const realRecentBlocksController = metamaskController.recentBlocksController |
|
||||||
metamaskController.recentBlocksController = { |
|
||||||
store: { |
|
||||||
getState: () => { |
|
||||||
return { |
|
||||||
recentBlocks: [] |
|
||||||
} |
|
||||||
} |
} |
||||||
} |
}, |
||||||
} |
}, |
||||||
|
} |
||||||
const gasPrice = metamaskController.getGasPrice() |
|
||||||
assert.equal(gasPrice, '0x' + GWEI_BN.toString(16), 'defaults to 1 gwei') |
|
||||||
|
|
||||||
metamaskController.recentBlocksController = realRecentBlocksController |
const gasPrice = metamaskController.getGasPrice() |
||||||
}) |
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
||||||
|
|
||||||
|
metamaskController.recentBlocksController = realRecentBlocksController |
||||||
}) |
}) |
||||||
|
}) |
||||||
|
|
||||||
describe('#createNewVaultAndKeychain', function () { |
describe('#createNewVaultAndKeychain', function () { |
||||||
it('can only create new vault on keyringController once', async function () { |
it('can only create new vault on keyringController once', async function () { |
||||||
const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity') |
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||||
|
|
||||||
|
|
||||||
const password = 'a-fake-password' |
const password = 'a-fake-password' |
||||||
|
|
||||||
const first = await metamaskController.createNewVaultAndKeychain(password) |
await metamaskController.createNewVaultAndKeychain(password) |
||||||
const second = await metamaskController.createNewVaultAndKeychain(password) |
await metamaskController.createNewVaultAndKeychain(password) |
||||||
|
|
||||||
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
||||||
|
|
||||||
selectStub.reset() |
selectStub.reset() |
||||||
}) |
|
||||||
}) |
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#createNewVaultAndRestore', function () { |
||||||
|
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
||||||
|
|
||||||
|
const password = 'what-what-what' |
||||||
|
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu' |
||||||
|
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||||
|
await metamaskController.createNewVaultAndRestore(password, wrongSeed) |
||||||
|
.catch((e) => { |
||||||
|
return |
||||||
|
}) |
||||||
|
await metamaskController.createNewVaultAndRestore(password, rightSeed) |
||||||
|
|
||||||
describe('#createNewVaultAndRestore', function () { |
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
||||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
|
||||||
// const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity')
|
|
||||||
|
|
||||||
const password = 'what-what-what' |
|
||||||
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu' |
|
||||||
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
|
||||||
const first = await metamaskController.createNewVaultAndRestore(password, wrongSeed) |
|
||||||
.catch((e) => { |
|
||||||
return |
|
||||||
}) |
|
||||||
const second = await metamaskController.createNewVaultAndRestore(password, rightSeed) |
|
||||||
|
|
||||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
|
||||||
}) |
|
||||||
}) |
}) |
||||||
}) |
}) |
||||||
}) |
}) |
||||||
|
@ -0,0 +1,133 @@ |
|||||||
|
const assert = require('assert') |
||||||
|
const clone = require('clone') |
||||||
|
const KeyringController = require('eth-keyring-controller') |
||||||
|
const firstTimeState = require('../../app/scripts/first-time-state') |
||||||
|
const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier') |
||||||
|
const mockEncryptor = require('../lib/mock-encryptor') |
||||||
|
|
||||||
|
describe('SeedPhraseVerifier', function () { |
||||||
|
|
||||||
|
describe('verifyAccounts', function () { |
||||||
|
|
||||||
|
let password = 'passw0rd1' |
||||||
|
let hdKeyTree = 'HD Key Tree' |
||||||
|
|
||||||
|
let keyringController |
||||||
|
let vault |
||||||
|
let primaryKeyring |
||||||
|
|
||||||
|
beforeEach(async function () { |
||||||
|
keyringController = new KeyringController({ |
||||||
|
initState: clone(firstTimeState), |
||||||
|
encryptor: mockEncryptor, |
||||||
|
}) |
||||||
|
|
||||||
|
assert(keyringController) |
||||||
|
|
||||||
|
vault = await keyringController.createNewVaultAndKeychain(password) |
||||||
|
primaryKeyring = keyringController.getKeyringsByType(hdKeyTree)[0] |
||||||
|
}) |
||||||
|
|
||||||
|
it('should be able to verify created account with seed words', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = serialized.mnemonic |
||||||
|
assert.notEqual(seedWords.length, 0) |
||||||
|
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should be able to verify created account (upper case) with seed words', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
|
||||||
|
let upperCaseAccounts = [createdAccounts[0].toUpperCase()] |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = serialized.mnemonic |
||||||
|
assert.notEqual(seedWords.length, 0) |
||||||
|
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(upperCaseAccounts, seedWords) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should be able to verify created account (lower case) with seed words', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
let lowerCaseAccounts = [createdAccounts[0].toLowerCase()] |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = serialized.mnemonic |
||||||
|
assert.notEqual(seedWords.length, 0) |
||||||
|
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(lowerCaseAccounts, seedWords) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return error with good but different seed words', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) |
||||||
|
assert.fail("Should reject") |
||||||
|
} catch (err) { |
||||||
|
assert.ok(err.message.indexOf('Not identical accounts!') >= 0, 'Wrong error message') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return error with undefined existing accounts', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(undefined, seedWords) |
||||||
|
assert.fail("Should reject") |
||||||
|
} catch (err) { |
||||||
|
assert.equal(err.message, 'No created accounts defined.') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return error with empty accounts array', async function () { |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 1) |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts([], seedWords) |
||||||
|
assert.fail("Should reject") |
||||||
|
} catch (err) { |
||||||
|
assert.equal(err.message, 'No created accounts defined.') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
it('should be able to verify more than one created account with seed words', async function () { |
||||||
|
|
||||||
|
const keyState = await keyringController.addNewAccount(primaryKeyring) |
||||||
|
const keyState2 = await keyringController.addNewAccount(primaryKeyring) |
||||||
|
|
||||||
|
let createdAccounts = await primaryKeyring.getAccounts() |
||||||
|
assert.equal(createdAccounts.length, 3) |
||||||
|
|
||||||
|
let serialized = await primaryKeyring.serialize() |
||||||
|
let seedWords = serialized.mnemonic |
||||||
|
assert.notEqual(seedWords.length, 0) |
||||||
|
|
||||||
|
let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -1,106 +1,131 @@ |
|||||||
const inherits = require('util').inherits |
|
||||||
const Component = require('react').Component |
const Component = require('react').Component |
||||||
|
const PropTypes = require('prop-types') |
||||||
const h = require('react-hyperscript') |
const h = require('react-hyperscript') |
||||||
const connect = require('react-redux').connect |
const connect = require('react-redux').connect |
||||||
const actions = require('../../actions') |
const actions = require('../../actions') |
||||||
const FileInput = require('react-simple-file-input').default |
const FileInput = require('react-simple-file-input').default |
||||||
|
const t = require('../../../i18n') |
||||||
|
|
||||||
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' |
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' |
||||||
|
|
||||||
module.exports = connect(mapStateToProps)(JsonImportSubview) |
class JsonImportSubview extends Component { |
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
function mapStateToProps (state) { |
this.state = { |
||||||
return { |
file: null, |
||||||
error: state.appState.warning, |
fileContents: '', |
||||||
|
} |
||||||
} |
} |
||||||
} |
|
||||||
|
|
||||||
inherits(JsonImportSubview, Component) |
render () { |
||||||
function JsonImportSubview () { |
const { error } = this.props |
||||||
Component.call(this) |
|
||||||
} |
return ( |
||||||
|
h('div.new-account-import-form__json', [ |
||||||
|
|
||||||
|
h('p', t('usedByClients')), |
||||||
|
h('a.warning', { |
||||||
|
href: HELP_LINK, |
||||||
|
target: '_blank', |
||||||
|
}, t('fileImportFail')), |
||||||
|
|
||||||
|
h(FileInput, { |
||||||
|
readAs: 'text', |
||||||
|
onLoad: this.onLoad.bind(this), |
||||||
|
style: { |
||||||
|
margin: '20px 0px 12px 34%', |
||||||
|
fontSize: '15px', |
||||||
|
display: 'flex', |
||||||
|
justifyContent: 'center', |
||||||
|
}, |
||||||
|
}), |
||||||
|
|
||||||
|
h('input.new-account-import-form__input-password', { |
||||||
|
type: 'password', |
||||||
|
placeholder: t('enterPassword'), |
||||||
|
id: 'json-password-box', |
||||||
|
onKeyPress: this.createKeyringOnEnter.bind(this), |
||||||
|
}), |
||||||
|
|
||||||
|
h('div.new-account-create-form__buttons', {}, [ |
||||||
|
|
||||||
|
h('button.new-account-create-form__button-cancel', { |
||||||
|
onClick: () => this.props.goHome(), |
||||||
|
}, [ |
||||||
|
t('cancel'), |
||||||
|
]), |
||||||
|
|
||||||
|
h('button.new-account-create-form__button-create', { |
||||||
|
onClick: () => this.createNewKeychain(), |
||||||
|
}, [ |
||||||
|
t('import'), |
||||||
|
]), |
||||||
|
|
||||||
JsonImportSubview.prototype.render = function () { |
|
||||||
const { error } = this.props |
|
||||||
|
|
||||||
return ( |
|
||||||
h('div.new-account-import-form__json', [ |
|
||||||
|
|
||||||
h('p', 'Used by a variety of different clients'), |
|
||||||
h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), |
|
||||||
|
|
||||||
h(FileInput, { |
|
||||||
readAs: 'text', |
|
||||||
onLoad: this.onLoad.bind(this), |
|
||||||
style: { |
|
||||||
margin: '20px 0px 12px 34%', |
|
||||||
fontSize: '15px', |
|
||||||
display: 'flex', |
|
||||||
justifyContent: 'center', |
|
||||||
}, |
|
||||||
}), |
|
||||||
|
|
||||||
h('input.new-account-import-form__input-password', { |
|
||||||
type: 'password', |
|
||||||
placeholder: 'Enter password', |
|
||||||
id: 'json-password-box', |
|
||||||
onKeyPress: this.createKeyringOnEnter.bind(this), |
|
||||||
}), |
|
||||||
|
|
||||||
h('div.new-account-create-form__buttons', {}, [ |
|
||||||
|
|
||||||
h('button.new-account-create-form__button-cancel', { |
|
||||||
onClick: () => this.props.goHome(), |
|
||||||
}, [ |
|
||||||
'CANCEL', |
|
||||||
]), |
]), |
||||||
|
|
||||||
h('button.new-account-create-form__button-create', { |
error ? h('span.error', error) : null, |
||||||
onClick: () => this.createNewKeychain.bind(this), |
]) |
||||||
}, [ |
) |
||||||
'IMPORT', |
} |
||||||
]), |
|
||||||
|
|
||||||
]), |
onLoad (event, file) { |
||||||
|
this.setState({file: file, fileContents: event.target.result}) |
||||||
|
} |
||||||
|
|
||||||
error ? h('span.error', error) : null, |
createKeyringOnEnter (event) { |
||||||
]) |
if (event.key === 'Enter') { |
||||||
) |
event.preventDefault() |
||||||
} |
this.createNewKeychain() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
JsonImportSubview.prototype.onLoad = function (event, file) { |
createNewKeychain () { |
||||||
this.setState({file: file, fileContents: event.target.result}) |
const state = this.state |
||||||
} |
|
||||||
|
|
||||||
JsonImportSubview.prototype.createKeyringOnEnter = function (event) { |
if (!state) { |
||||||
if (event.key === 'Enter') { |
const message = 'You must select a valid file to import.' |
||||||
event.preventDefault() |
return this.props.displayWarning(message) |
||||||
this.createNewKeychain() |
} |
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
JsonImportSubview.prototype.createNewKeychain = function () { |
const { fileContents } = state |
||||||
const state = this.state |
|
||||||
|
|
||||||
if (!state) { |
if (!fileContents) { |
||||||
const message = 'You must select a valid file to import.' |
const message = t('needImportFile') |
||||||
return this.props.dispatch(actions.displayWarning(message)) |
return this.props.displayWarning(message) |
||||||
} |
} |
||||||
|
|
||||||
|
const passwordInput = document.getElementById('json-password-box') |
||||||
|
const password = passwordInput.value |
||||||
|
|
||||||
const { fileContents } = state |
if (!password) { |
||||||
|
const message = t('needImportPassword') |
||||||
|
return this.props.displayWarning(message) |
||||||
|
} |
||||||
|
|
||||||
if (!fileContents) { |
this.props.importNewJsonAccount([ fileContents, password ]) |
||||||
const message = 'You must select a file to import.' |
|
||||||
return this.props.dispatch(actions.displayWarning(message)) |
|
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
const passwordInput = document.getElementById('json-password-box') |
JsonImportSubview.propTypes = { |
||||||
const password = passwordInput.value |
error: PropTypes.string, |
||||||
|
goHome: PropTypes.func, |
||||||
|
displayWarning: PropTypes.func, |
||||||
|
importNewJsonAccount: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
if (!password) { |
const mapStateToProps = state => { |
||||||
const message = 'You must enter a password for the selected file.' |
return { |
||||||
return this.props.dispatch(actions.displayWarning(message)) |
error: state.appState.warning, |
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) |
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
goHome: () => dispatch(actions.goHome()), |
||||||
|
displayWarning: warning => dispatch(actions.displayWarning(warning)), |
||||||
|
importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)), |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
module.exports = connect(mapStateToProps, mapDispatchToProps)(JsonImportSubview) |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue