commit
a2c14ad02b
@ -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": { |
||||
"message": "MetaMask", |
||||
"description": "The name of the application" |
||||
}, |
||||
"appDescription": { |
||||
"message": "Ethereum Identity Management", |
||||
"description": "The description of the application" |
||||
"attemptingConnect": { |
||||
"message": "Attempting to connect to blockchain." |
||||
}, |
||||
"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 path = require('path') |
||||
const { promisify } = require('util') |
||||
|
||||
const statesPath = path.join(__dirname, 'states') |
||||
const stateNames = fs.readdirSync(statesPath) |
||||
start().catch(console.error) |
||||
|
||||
const states = stateNames.reduce((result, stateFileName) => { |
||||
const statePath = path.join(__dirname, 'states', stateFileName) |
||||
const stateFile = fs.readFileSync(statePath).toString() |
||||
const state = JSON.parse(stateFile) |
||||
result[stateFileName.split('.')[0].replace(/-/g, ' ', 'g')] = state |
||||
return result |
||||
}, {}) |
||||
|
||||
const result = `module.exports = ${JSON.stringify(states)}` |
||||
|
||||
const statesJsonPath = path.join(__dirname, 'states.js') |
||||
fs.writeFileSync(statesJsonPath, result) |
||||
async function start () { |
||||
const statesPath = path.join(__dirname, 'states') |
||||
const stateFilesNames = await promisify(fs.readdir)(statesPath) |
||||
const states = {} |
||||
await Promise.all(stateFilesNames.map(async (stateFileName) => { |
||||
const stateFilePath = path.join(__dirname, 'states', stateFileName) |
||||
const stateFileContent = await promisify(fs.readFile)(stateFilePath, 'utf8') |
||||
const state = JSON.parse(stateFileContent) |
||||
const stateName = stateFileName.split('.')[0].replace(/-/g, ' ', 'g') |
||||
states[stateName] = state |
||||
})) |
||||
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 h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('../../../../ui/app/actions') |
||||
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' |
||||
|
||||
module.exports = connect(mapStateToProps)(JsonImportSubview) |
||||
class JsonImportSubview extends Component { |
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
error: state.appState.warning, |
||||
this.state = { |
||||
file: null, |
||||
fileContents: '', |
||||
} |
||||
} |
||||
} |
||||
|
||||
inherits(JsonImportSubview, Component) |
||||
function JsonImportSubview () { |
||||
Component.call(this) |
||||
} |
||||
render () { |
||||
const { error } = this.props |
||||
|
||||
JsonImportSubview.prototype.render = function () { |
||||
const { error } = this.props |
||||
|
||||
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), |
||||
return ( |
||||
h('div', { |
||||
style: { |
||||
margin: '20px 0px 12px 20px', |
||||
fontSize: '15px', |
||||
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: { |
||||
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', { |
||||
type: 'password', |
||||
placeholder: 'Enter password', |
||||
id: 'json-password-box', |
||||
onKeyPress: this.createKeyringOnEnter.bind(this), |
||||
style: { |
||||
width: 260, |
||||
marginTop: 12, |
||||
}, |
||||
}), |
||||
onLoad (event, file) { |
||||
this.setState({file: file, fileContents: event.target.result}) |
||||
} |
||||
|
||||
h('button.primary', { |
||||
onClick: this.createNewKeychain.bind(this), |
||||
style: { |
||||
margin: 12, |
||||
}, |
||||
}, 'Import'), |
||||
createKeyringOnEnter (event) { |
||||
if (event.key === 'Enter') { |
||||
event.preventDefault() |
||||
this.createNewKeychain() |
||||
} |
||||
} |
||||
|
||||
error ? h('span.error', error) : null, |
||||
]) |
||||
) |
||||
} |
||||
createNewKeychain () { |
||||
const { fileContents } = this.state |
||||
|
||||
JsonImportSubview.prototype.onLoad = function (event, file) { |
||||
this.setState({file: file, fileContents: event.target.result}) |
||||
} |
||||
if (!fileContents) { |
||||
const message = 'You must select a file to import.' |
||||
return this.props.displayWarning(message) |
||||
} |
||||
|
||||
JsonImportSubview.prototype.createKeyringOnEnter = function (event) { |
||||
if (event.key === 'Enter') { |
||||
event.preventDefault() |
||||
this.createNewKeychain() |
||||
} |
||||
} |
||||
const passwordInput = document.getElementById('json-password-box') |
||||
const password = passwordInput.value |
||||
|
||||
JsonImportSubview.prototype.createNewKeychain = function () { |
||||
const state = this.state |
||||
const { fileContents } = state |
||||
if (!password) { |
||||
const message = 'You must enter a password for the selected file.' |
||||
return this.props.displayWarning(message) |
||||
} |
||||
|
||||
if (!fileContents) { |
||||
const message = 'You must select a file to import.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
this.props.importNewAccount([ fileContents, password ]) |
||||
} |
||||
} |
||||
|
||||
const passwordInput = document.getElementById('json-password-box') |
||||
const password = passwordInput.value |
||||
JsonImportSubview.propTypes = { |
||||
error: PropTypes.string, |
||||
displayWarning: PropTypes.func, |
||||
importNewAccount: PropTypes.func, |
||||
} |
||||
|
||||
if (!password) { |
||||
const message = 'You must enter a password for the selected file.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
const mapStateToProps = state => { |
||||
return { |
||||
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 sinon = require('sinon') |
||||
const clone = require('clone') |
||||
const nock = require('nock') |
||||
const MetaMaskController = require('../../app/scripts/metamask-controller') |
||||
const blacklistJSON = require('../stub/blacklist') |
||||
const firstTimeState = require('../../app/scripts/first-time-state') |
||||
const BN = require('ethereumjs-util').BN |
||||
const GWEI_BN = new BN('1000000000') |
||||
|
||||
describe('MetaMaskController', function () { |
||||
const noop = () => {} |
||||
const metamaskController = new MetaMaskController({ |
||||
showUnconfirmedMessage: 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), |
||||
}) |
||||
let metamaskController |
||||
const sandbox = sinon.sandbox.create() |
||||
const noop = () => { } |
||||
|
||||
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 () { |
||||
// sinon requires cleanup otherwise it will overwrite context
|
||||
this.sinon.restore() |
||||
nock.cleanAll() |
||||
sandbox.restore() |
||||
}) |
||||
|
||||
describe('Metamask Controller', function () { |
||||
assert(metamaskController) |
||||
|
||||
beforeEach(function () { |
||||
sinon.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||
sinon.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
metamaskController.keyringController.createNewVaultAndKeychain.restore() |
||||
metamaskController.keyringController.createNewVaultAndRestore.restore() |
||||
}) |
||||
|
||||
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: [] |
||||
} |
||||
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, '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 () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
const password = 'a-fake-password' |
||||
const password = 'a-fake-password' |
||||
|
||||
const first = await metamaskController.createNewVaultAndKeychain(password) |
||||
const second = await metamaskController.createNewVaultAndKeychain(password) |
||||
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 () { |
||||
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) |
||||
}) |
||||
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 PropTypes = require('prop-types') |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('../../actions') |
||||
const FileInput = require('react-simple-file-input').default |
||||
const t = require('../../../i18n') |
||||
|
||||
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) { |
||||
return { |
||||
error: state.appState.warning, |
||||
this.state = { |
||||
file: null, |
||||
fileContents: '', |
||||
} |
||||
} |
||||
} |
||||
|
||||
inherits(JsonImportSubview, Component) |
||||
function JsonImportSubview () { |
||||
Component.call(this) |
||||
} |
||||
render () { |
||||
const { error } = this.props |
||||
|
||||
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', { |
||||
onClick: () => this.createNewKeychain.bind(this), |
||||
}, [ |
||||
'IMPORT', |
||||
]), |
||||
error ? h('span.error', error) : null, |
||||
]) |
||||
) |
||||
} |
||||
|
||||
]), |
||||
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) { |
||||
this.setState({file: file, fileContents: event.target.result}) |
||||
} |
||||
createNewKeychain () { |
||||
const state = this.state |
||||
|
||||
JsonImportSubview.prototype.createKeyringOnEnter = function (event) { |
||||
if (event.key === 'Enter') { |
||||
event.preventDefault() |
||||
this.createNewKeychain() |
||||
} |
||||
} |
||||
if (!state) { |
||||
const message = 'You must select a valid file to import.' |
||||
return this.props.displayWarning(message) |
||||
} |
||||
|
||||
JsonImportSubview.prototype.createNewKeychain = function () { |
||||
const state = this.state |
||||
const { fileContents } = state |
||||
|
||||
if (!state) { |
||||
const message = 'You must select a valid file to import.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
} |
||||
if (!fileContents) { |
||||
const message = t('needImportFile') |
||||
return this.props.displayWarning(message) |
||||
} |
||||
|
||||
const { fileContents } = state |
||||
const passwordInput = document.getElementById('json-password-box') |
||||
const password = passwordInput.value |
||||
|
||||
if (!fileContents) { |
||||
const message = 'You must select a file to import.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
if (!password) { |
||||
const message = t('needImportPassword') |
||||
return this.props.displayWarning(message) |
||||
} |
||||
|
||||
this.props.importNewJsonAccount([ fileContents, password ]) |
||||
} |
||||
} |
||||
|
||||
const passwordInput = document.getElementById('json-password-box') |
||||
const password = passwordInput.value |
||||
JsonImportSubview.propTypes = { |
||||
error: PropTypes.string, |
||||
goHome: PropTypes.func, |
||||
displayWarning: PropTypes.func, |
||||
importNewJsonAccount: PropTypes.func, |
||||
} |
||||
|
||||
if (!password) { |
||||
const message = 'You must enter a password for the selected file.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
const mapStateToProps = state => { |
||||
return { |
||||
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