diff options
Diffstat (limited to 'app')
59 files changed, 2075 insertions, 1316 deletions
diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 6a4ebc8a5..55344f3e1 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -796,7 +796,7 @@ "message": "Testovací faucet" }, "to": { - "message": "Komu: " + "message": "Komu" }, "toETHviaShapeShift": { "message": "$1 na ETH přes ShapeShift", diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 1d2619672..352d5ad7d 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -84,7 +84,7 @@ "message": "Auf Coinbase kaufen" }, "buyCoinbaseExplainer": { - "message": "Coinbase ist die weltweit bekannteste Art und Weise um bitcoin, ethereum und litecoin zu kaufen und verkaufen." + "message": "Coinbase ist die weltweit bekannteste Art und Weise um Bitcoin, Ethereum und Litecoin zu kaufen und verkaufen." }, "ok": { "message": "Ok" @@ -384,7 +384,7 @@ "infoHelp": { "message": "Info & Hilfe" }, - "insufficientFunds": { + "insufficientFunds": { "message": "Nicht genügend Guthaben." }, "insufficientTokens": { @@ -572,7 +572,7 @@ "description": "Wähle diesen Dateityp um damit einen Account zu importieren" }, "privateKeyWarning": { - "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen." + "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen." }, "privateNetwork": { "message": "Privates Netzwerk" @@ -775,7 +775,7 @@ "message": "Testfaucet" }, "to": { - "message": "An:" + "message": "An" }, "toETHviaShapeShift": { "message": "$1 an ETH via ShapeShift", @@ -828,7 +828,7 @@ "message": "Willkommen zur neuen Oberfläche (Beta)" }, "uiWelcomeMessage": { - "message": "Du verwendest nun die neue Metamask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast." + "message": "Du verwendest nun die neue MetaMask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast." }, "unapproved": { "message": "Nicht genehmigt" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 62ec4ce37..f6cf7cf69 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -17,6 +17,9 @@ "accountSelectionRequired": { "message": "You need to select an account!" }, + "activityLog": { + "message": "activity log" + }, "address": { "message": "Address" }, @@ -29,6 +32,9 @@ "addTokens": { "message": "Add Tokens" }, + "addSuggestedTokens": { + "message": "Add Suggested Tokens" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -55,6 +61,12 @@ "attemptingConnect": { "message": "Attempting to connect to blockchain." }, + "attemptToCancel": { + "message": "Attempt to Cancel?" + }, + "attemptToCancelDescription": { + "message": "Attempting to cancel does not guarantee your original transaction will be cancelled. If cancelled, you are still required to pay a transaction fee to the network." + }, "attributions": { "message": "Attributions" }, @@ -99,7 +111,7 @@ "message": "Buy on Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin." + "message": "Coinbase is the world’s most popular way to buy and sell Bitcoin, Ethereum, and Litecoin." }, "bytes": { "message": "Bytes" @@ -110,6 +122,12 @@ "cancel": { "message": "Cancel" }, + "cancelAttempt": { + "message": "Cancel Attempt" + }, + "cancellationGasFee": { + "message": "Cancellation Gas Fee" + }, "classicInterface": { "message": "Use classic interface" }, @@ -119,8 +137,8 @@ "close": { "message": "Close" }, - "chromeRequiredForTrezor":{ - "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device." + "chromeRequiredForHardwareWallets":{ + "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, "confirm": { "message": "Confirm" @@ -146,15 +164,12 @@ "connecting": { "message": "Connecting..." }, + "connectToLedger": { + "message": "Connect to Ledger" + }, "connectToTrezor": { "message": "Connect to Trezor" }, - "connectToTrezorHelp": { - "message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked." - }, - "connectToTrezorTrouble": { - "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." - }, "continue": { "message": "Continue" }, @@ -213,6 +228,9 @@ "currentConversion": { "message": "Current Conversion" }, + "currentLanguage": { + "message": "Current Language" + }, "currentNetwork": { "message": "Current Network" }, @@ -289,8 +307,8 @@ "downloadStateLogs": { "message": "Download State Logs" }, - "dontHaveATrezorWallet": { - "message": "Don't have a TREZOR hardware wallet?" + "dontHaveAHardwareWallet": { + "message": "Don’t have a hardware wallet?" }, "dropped": { "message": "Dropped" @@ -426,11 +444,11 @@ "hardwareWalletConnected": { "message": "Hardware wallet connected" }, - "hardwareSupport": { - "message": "Hardware Support" + "hardwareWallets": { + "message": "Connect a hardware wallet" }, - "hardwareSupportMsg": { - "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + "hardwareWalletsMsg": { + "message": "Select a hardware wallet you'd like to use with MetaMask" }, "havingTroubleConnecting": { "message": "Having trouble connecting?" @@ -454,6 +472,9 @@ "hideTokenPrompt": { "message": "Hide Token?" }, + "history": { + "message": "History" + }, "howToDeposit": { "message": "How would you like to deposit Ether?" }, @@ -538,6 +559,9 @@ "learnMore": { "message": "Learn more" }, + "ledgerAccountRestriction": { + "message": "You need to make use your last account before you can add a new one." + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -587,6 +611,9 @@ "metamaskSeedWords": { "message": "MetaMask Seed Words" }, + "metamaskVersion": { + "message": "MetaMask Version" + }, "min": { "message": "Minimum" }, @@ -651,7 +678,7 @@ "message": "No transaction history." }, "noTransactions": { - "message": "No Transactions" + "message": "You have no transactions" }, "notFound": { "message": "Not Found" @@ -702,6 +729,9 @@ "pasteSeed": { "message": "Paste your seed phrase here!" }, + "pending": { + "message": "pending" + }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." }, @@ -730,6 +760,9 @@ "qrCode": { "message": "Show QR Code" }, + "queue": { + "message": "Queue" + }, "readdToken": { "message": "You can add this token back in the future by going go to “Add token” in your accounts options menu." }, @@ -845,6 +878,9 @@ "save": { "message": "Save" }, + "speedUp": { + "message": "speed up" + }, "speedUpTitle": { "message": "Speed Up Transaction" }, @@ -870,6 +906,12 @@ "secretPhrase": { "message": "Enter your secret twelve word phrase here to restore your vault." }, + "showHexData": { + "message": "Show Hex Data" + }, + "showHexDataDescription": { + "message": "Select this to show the hex data field on the send screen" + }, "newPassword8Chars": { "message": "New Password (min 8 chars)" }, @@ -882,6 +924,9 @@ "selectCurrency": { "message": "Select Currency" }, + "selectLocale": { + "message": "Select Locale" + }, "selectService": { "message": "Select Service" }, @@ -897,6 +942,12 @@ "sendTokens": { "message": "Send Tokens" }, + "sentEther": { + "message": "sent ether" + }, + "sentTokens": { + "message": "sent tokens" + }, "separateEachWord": { "message": "Separate each word with a single space" }, @@ -908,7 +959,10 @@ "description": "displays token symbol" }, "orderOneHere": { - "message": "Order one here." + "message": "Order a Trezor or Ledger and keep your funds in cold storage" + }, + "outgoing": { + "message": "Outgoing" }, "searchTokens": { "message": "Search Tokens" @@ -920,7 +974,13 @@ "message": "Select an Account" }, "selectAnAccountHelp": { - "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask." + "message": "Select the account to view in MetaMask" + }, + "selectHdPath": { + "message": "Select HD Path" + }, + "selectPathHelp": { + "message": "If you don't see your existing Ledger accounts below, try switching paths to \"Legacy (MEW / MyCrypto)\"" }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" @@ -967,6 +1027,9 @@ "sign": { "message": "Sign" }, + "signatureRequest": { + "message": "Signature Request" + }, "signed": { "message": "Signed" }, @@ -1019,7 +1082,7 @@ "message": "Test Faucet" }, "to": { - "message": "To: " + "message": "To" }, "toETHviaShapeShift": { "message": "$1 to ETH via ShapeShift", @@ -1049,6 +1112,30 @@ "total": { "message": "Total" }, + "transaction": { + "message": "transaction" + }, + "transactionConfirmed": { + "message": "Transaction confirmed on $2." + }, + "transactionCreated": { + "message": "Transaction created with a value of $1 on $2." + }, + "transactionWithNonce": { + "message": "Transaction $1" + }, + "transactionDropped": { + "message": "Transaction dropped on $2." + }, + "transactionSubmitted": { + "message": "Transaction submitted on $2." + }, + "transactionUpdated": { + "message": "Transaction updated on $2." + }, + "transactionUpdatedGas": { + "message": "Transaction updated with a gas price of $1 on $2." + }, "transactions": { "message": "transactions" }, @@ -1087,7 +1174,7 @@ "message": "Welcome to the New UI (Beta)" }, "uiWelcomeMessage": { - "message": "You are now using the new Metamask UI." + "message": "You are now using the new MetaMask UI." }, "unapproved": { "message": "Unapproved" @@ -1095,6 +1182,9 @@ "unavailable": { "message": "Unavailable" }, + "units": { + "message": "units" + }, "unknown": { "message": "Unknown" }, @@ -1122,6 +1212,9 @@ "unlockMessage": { "message": "The decentralized web awaits" }, + "updatedWithDate": { + "message": "Updated $1" + }, "uriErrorMsg": { "message": "URIs require the appropriate HTTP/HTTPS prefix." }, @@ -1162,6 +1255,9 @@ "whatsThis": { "message": "What's this?" }, + "yesLetsTry": { + "message": "Yes, let's try" + }, "youNeedToAllowCameraAccess": { "message": "You need to allow camera access to use this feature." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index f287674f1..3e43a7b43 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -75,7 +75,7 @@ "message": "Pedir prestado con Dharma (Beta)" }, "builtInCalifornia": { - "message": "Metamask fue diseñado y construido en California" + "message": "MetaMask fue diseñado y construido en California" }, "buy": { "message": "Comprar" @@ -772,7 +772,7 @@ "message": "Probar Faucet" }, "to": { - "message": "Para:" + "message": "Para" }, "toETHviaShapeShift": { "message": "$1 a ETH via ShapeShift", @@ -874,7 +874,7 @@ "message": "Advertencia" }, "welcomeBeta": { - "message": "Bienvenido a Metamask Beta" + "message": "Bienvenido a MetaMask Beta" }, "whatsThis": { "message": "¿Qué es esto?" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index da2cfe4f8..6f850d89b 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -63,7 +63,7 @@ "message": "Acheter sur Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du bitcoin, de l'ethereum et du litecoin." + "message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du Bitcoin, de l'Ethereum et du Litecoin." }, "cancel": { "message": "Annuler" @@ -490,7 +490,7 @@ "message": "Sélectionner un service" }, "send": { - "message": "Envoyé" + "message": "Envoyer" }, "sendTokens": { "message": "Envoyer des jetons" @@ -570,7 +570,7 @@ "message": "Bienvenue dans la nouvelle interface utilisateur (Beta)" }, "uiWelcomeMessage": { - "message": "Vous utilisez maintenant la nouvelle interface utilisateur Metamask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes." + "message": "Vous utilisez maintenant la nouvelle interface utilisateur MetaMask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes." }, "unavailable": { "message": "Indisponible" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index ef3ed4025..2ae5fae72 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -81,7 +81,7 @@ "message": "Compra su Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere bitcoin, ethereum e litecoin." + "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere Bitcoin, Ethereum e Litecoin." }, "cancel": { "message": "Cancella" @@ -178,7 +178,7 @@ "message": "La rete predefinita per transazioni in Ether è la Rete Ethereum Principale." }, "denExplainer": { - "message": "Il DEN è il tuo archivio crittato con password dentro Metamask." + "message": "Il DEN è il tuo archivio crittato con password dentro MetaMask." }, "deposit": { "message": "Deposita" @@ -816,4 +816,4 @@ "youSign": { "message": "Ti stai connettendo" } -}
\ No newline at end of file +} diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d3801c4f5..30d032357 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3,86 +3,125 @@ "message": "수락" }, "account": { - "message": "계좌" + "message": "계정" }, "accountDetails": { - "message": "계좌 상세보기" + "message": "계정 상세보기" }, "accountName": { - "message": "계좌 이름" + "message": "계정 이름" }, "address": { "message": "주소" }, + "addCustomToken": { + "message": "사용자 정의 토큰 추가" + }, "addToken": { "message": "토큰 추가" }, + "addTokens": { + "message": "토큰 추가" + }, + "addAcquiredTokens": { + "message": "메타마스크를 통해 획득한 토큰 추가" + }, "amount": { - "message": "금액" + "message": "수량" }, "amountPlusGas": { - "message": "금액 + 가스" + "message": "수량 + 가스" }, "appDescription": { "message": "이더리움 브라우저 확장 프로그램", - "description": "어플리케이션 내용" + "description": "어플리케이션 설명" }, "appName": { "message": "메타마스크", "description": "어플리케이션 이름" }, + "approved": { + "message": "수락" + }, "attemptingConnect": { - "message": "블록체인에 접속 시도 중입니다." + "message": "블록체인에 접속을 시도하는 중입니다." + }, + "attributions": { + "message": "속성" }, "available": { - "message": "사용 가능한" + "message": "사용 가능" }, "back": { - "message": "뒤로" + "message": "돌아가기" }, "balance": { - "message": "잔액:" + "message": "잔액" + }, + "balances": { + "message": "토큰 잔액" }, "balanceIsInsufficientGas": { - "message": "가스가 충분하지 않습니다." + "message": "현재 가스 총합에 대해 잔액이 부족합니다" }, "beta": { - "message": "베타" + "message": "BETA" }, "betweenMinAndMax": { "message": "$1 이상 $2 이하여야 합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" + }, + "blockiesIdenticon": { + "message": "Blockies 아이덴티콘 사용" }, "borrowDharma": { - "message": "Dharma에서 빌리기(베타)" + "message": "Dharma에서 대여하기(Beta)" + }, + "builtInCalifornia": { + "message": "메타마스크는 캘리포니아에서 디자인되고 만들어졌습니다." }, "buy": { "message": "구매" }, "buyCoinbase": { - "message": "코인베이스에서 구매" + "message": "코인베이스에서 구매하기" }, "buyCoinbaseExplainer": { - "message": "코인베이스에서 비트코인, 이더리움, 라이트코인을 구매하실 수 있습니다." + "message": "코인베이스는 비트코인, 이더리움, 라이트코인을 거래할 수 있는 유명한 거래소입니다." + }, + "ok": { + "message": "확인" }, "cancel": { "message": "취소" }, + "classicInterface": { + "message": "예전 인터페이스" + }, "clickCopy": { "message": "클릭하여 복사" }, + "close": { + "message": "닫기" + }, "confirm": { "message": "승인" }, + "confirmed": { + "message": "승인됨" + }, "confirmContract": { "message": "컨트랙트 승인" }, "confirmPassword": { - "message": "패스워드 승인" + "message": "비밀번호 확인" }, "confirmTransaction": { "message": "트랜잭션 승인" }, + "continue": { + "message": "계속" + }, "continueToCoinbase": { "message": "코인베이스로 계속하기" }, @@ -90,72 +129,93 @@ "message": "컨트랙트 배포" }, "conversionProgress": { - "message": "변환중.." + "message": "변환 진행중" }, "copiedButton": { - "message": "복사되었습니다." + "message": "복사됨" }, "copiedClipboard": { - "message": "클립보드에 복사되었습니다." + "message": "클립보드에 복사되었습니다" }, "copiedExclamation": { - "message": "복사되었습니다." + "message": "복사됨!" + }, + "copiedSafe": { + "message": "안전한 곳에 복사하였습니다" }, "copy": { - "message": "복사하기" + "message": "복사" + }, + "copyContractAddress": { + "message": "컨트랙트 주소 복사" }, "copyToClipboard": { - "message": "클립보드에 복사" + "message": "클립보드로 복사" }, "copyButton": { "message": " 복사 " }, "copyPrivateKey": { - "message": "비밀 키 (클릭하여 복사)" + "message": "비밀 키입니다 (클릭하여 복사)" }, "create": { "message": "생성" }, "createAccount": { - "message": "계좌 생성" + "message": "계정 생성" }, "createDen": { "message": "생성" }, "crypto": { "message": "암호화폐", - "description": "Exchange type (cryptocurrencies)" + "description": "거래 유형 (암호화폐)" + }, + "currentConversion": { + "message": "선택된 단위" + }, + "currentNetwork": { + "message": "현재 네트워크" }, "customGas": { "message": "가스 설정" }, + "customToken": { + "message": "사용자 정의 토큰" + }, "customize": { - "message": "커스터마이즈" + "message": "맞춤화 하기" }, "customRPC": { - "message": "커스텀 RPC" + "message": "사용자 정의 RPC" + }, + "decimalsMustZerotoTen": { + "message": "소수점은 0 이상이고 36 이하여야 합니다." + }, + "decimal": { + "message": "소수점 정확도" }, "defaultNetwork": { "message": "이더리움 트랜잭션의 기본 네트워크는 메인넷입니다." }, "denExplainer": { - "message": "DEN은 비밀번호가 암호화 된 MetaMask의 스토리지입니다." + "message": "DEN은 비밀번호로 암호화 된 메타마스크의 저장소입니다." }, "deposit": { "message": "입금" }, "depositBTC": { - "message": "아래 주소로 BTC를 입급해주세요." + "message": "다음 주소로 BTC를 입급해주세요." }, "depositCoin": { - "message": "아래 주소로 $1를 입금해주세요.", - "description": "Tells the user what coin they have selected to deposit with shapeshift" + "message": "다음 주소로 $1 만큼 입금해주세요.", + "description": "사용자에게 shapeshift에서 어떤 코인을 선택해 입금했는지 알려줍니다" }, "depositEth": { - "message": "이더 입금" + "message": "이더 입금하기" }, "depositEther": { - "message": "이더 입금" + "message": "이더리움 입금하기" }, "depositFiat": { "message": "현금으로 입금하기" @@ -167,10 +227,10 @@ "message": "ShapeShift를 통해 입금하기" }, "depositShapeShiftExplainer": { - "message": "다른 암호화폐를 가지고 있으면, 계좌 생성 필요없이, 거래를 하거나 메타마스크 지갑을 통해 이더를 입금할 수 있습니다." + "message": "다른 암호화폐를 가지고 있으면, 계정을 생성할 필요없이 메타마스크 지갑에 이더리움을 바로 거래하거나 입금할 수 있습니다." }, "details": { - "message": "상세" + "message": "세부사항" }, "directDeposit": { "message": "즉시 입금" @@ -179,70 +239,103 @@ "message": "이더 즉시 입금" }, "directDepositEtherExplainer": { - "message": "이더를 이미 보유하고 있다면, 직접 입금을 통해 이더를 즉시 입금하실 수 있습니다." + "message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다." }, "done": { "message": "완료" }, + "downloadStateLogs": { + "message": "상태 로그 다운로드" + }, + "dropped": { + "message": "중단됨" + }, "edit": { "message": "수정" }, "editAccountName": { - "message": "계좌명 수정" + "message": "계정 이름 수정" + }, + "editingTransaction": { + "message": "트랜젝션을 변경합니다" + }, + "emailUs": { + "message": "저자에게 메일 보내기!" }, "encryptNewDen": { - "message": "새 DEN 암호화" + "message": "새로운 DEN을 암호화" }, "enterPassword": { - "message": "패스워드를 입력해주세요." + "message": "비밀번호를 입력해주세요" + }, + "enterPasswordConfirm": { + "message": "비밀번호를 다시 입력해 주세요" + }, + "enterPasswordContinue": { + "message": "계속하기 위해 비밀번호 입력" + }, + "passwordNotLongEnough": { + "message": "비밀번호가 충분히 길지 않습니다" + }, + "passwordsDontMatch": { + "message": "비밀번호가 맞지 않습니다" }, "etherscanView": { - "message": "이더스캔에서 계좌보기" + "message": "이더스캔에서 계정보기" }, "exchangeRate": { "message": "환율" }, "exportPrivateKey": { - "message": "비밀키 내보내기" + "message": "개인키 내보내기" }, "exportPrivateKeyWarning": { - "message": "Export private keys at your own risk." + "message": "개인키 내보내기는 위험을 감수해야 합니다." }, "failed": { "message": "실패" }, "fiat": { "message": "FIAT", - "description": "Exchange type" + "description": "거래 형식" }, "fileImportFail": { - "message": "파일을 가져올 수 없나요? 여기를 클릭해주세요!", - "description": "Helps user import their account from a JSON file" + "message": "파일을 가져올 수 없나요? 이곳을 클릭해주세요!", + "description": "JSON 파일로부터 계정 가져오기를 도와줍니다" + }, + "followTwitter": { + "message": "트위터에서 팔로우하세요" }, "from": { - "message": "보내는 사람" + "message": "보내는 이" + }, + "fromToSame": { + "message": "보내고 받는 주소는 동일할 수 없습니다" }, "fromShapeShift": { "message": "ShapeShift로 부터" }, "gas": { "message": "가스", - "description": "Short indication of gas cost" + "description": "가스 가격의 줄임" }, "gasFee": { "message": "가스 수수료" }, "gasLimit": { - "message": "가스 리밋" + "message": "가스 한도" }, "gasLimitCalculation": { - "message": "네트워크 성공률을 기반으로 적합한 가스 리밋을 계산합니다." + "message": "네트워크 성공률을 기반으로 적합한 가스 한도를 계산합니다." }, "gasLimitRequired": { - "message": "가스 리밋이 필요합니다." + "message": "가스 한도가 필요합니다." }, "gasLimitTooLow": { - "message": "가스 리밋은 21000 이상이여야 합니다." + "message": "가스 한도는 최소 21000 이상이여야 합니다." + }, + "generatingSeed": { + "message": "시드 생성중..." }, "gasPrice": { "message": "가스 가격 (GWEI)" @@ -253,21 +346,27 @@ "gasPriceRequired": { "message": "가스 가격이 필요합니다." }, + "generatingTransaction": { + "message": "트랜잭션 생성중" + }, "getEther": { "message": "이더 얻기" }, "getEtherFromFaucet": { - "message": "faucet에서 $1에 달하는 이더를 얻으세요.", - "description": "Displays network name for Ether faucet" + "message": "파우셋에서 $1에 달하는 이더를 얻으세요.", + "description": "이더 파우셋에 대한 네트워크 이름을 표시합니다" }, "greaterThanMin": { "message": "$1 이상이어야 합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" }, "here": { "message": "여기", "description": "as in -click here- for more information (goes with troubleTokenBalances)" }, + "hereList": { + "message": "리스트가 있습니다!!!!" + }, "hide": { "message": "숨기기" }, @@ -280,51 +379,96 @@ "howToDeposit": { "message": "어떤 방법으로 이더를 입금하시겠습니까?" }, + "holdEther": { + "message": "It allows you to hold ether & tokens, and serves as your bridge to decentralized applications." + }, "import": { - "message": "파일에서 가져오기", - "description": "Button to import an account from a selected file" + "message": "가져오기", + "description": "선택된 파일로부터 계정 가져오기 버튼" }, "importAccount": { - "message": "계좌 가져오기" + "message": "계정 가져오기" + }, + "importAccountMsg": { + "message": " 가져온 계정은 메타마스크에서 원래 생성된 계정의 시드구문과 연관성이 없습니다. 가져온 계정에 대해 더 배우기 " }, "importAnAccount": { - "message": "계좌 가져오기" + "message": "계정 가져오기" }, "importDen": { - "message": "기존 DEN 가져오기" + "message": "기존의 DEN 가져오기" }, "imported": { - "message": "가져오기 완료", - "description": "status showing that an account has been fully loaded into the keyring" + "message": "가져온 계정", + "description": "이 상태는 해당 계정이 keyring으로 완전히 적재된 상태임을 표시합니다" + }, + "importUsingSeed": { + "message": "계정 시드 구문으로 가져오기" }, "infoHelp": { "message": "정보 및 도움말" }, + "initialTransactionConfirmed": { + "message": "초기 트랜잭션이 네트워크를 통해 확정되었습니다. 확인을 누르고 이전으로 돌아갑니다." + }, + "insufficientFunds": { + "message": "충분하지 않은 자금." + }, + "insufficientTokens": { + "message": "충분하지 않은 토큰." + }, "invalidAddress": { - "message": "유효하지 않은 주소" + "message": "올바르지 않은 주소" + }, + "invalidAddressRecipient": { + "message": "수신 주소가 올바르지 않습니다" }, "invalidGasParams": { - "message": "유효하지 않은 가스 입력값" + "message": "올바르지 않은 가스 입력값" }, "invalidInput": { - "message": "유효하지 않은 입력값" + "message": "올바르지 않은 입력값" }, "invalidRequest": { "message": "유효하지 않은 요청" }, + "invalidRPC": { + "message": "올바르지 않은 RPC URI" + }, + "jsonFail": { + "message": "이상이 있습니다. JSON 파일이 올바른 파일인지 확인해주세요." + }, "jsonFile": { "message": "JSON 파일", - "description": "format for importing an account" + "description": "계정을 가져오기 위한 형식" + }, + "keepTrackTokens": { + "message": "메타마스크 계정을 통해 구입한 토큰 기록을 추적 및 보관합니다." }, "kovan": { "message": "Kovan 테스트넷" }, + "knowledgeDataBase": { + "message": "지식 베이스 방문" + }, + "max": { + "message": "최대" + }, + "learnMore": { + "message": "더 배우기." + }, "lessThanMax": { "message": "$1 이하여야합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" + }, + "likeToAddTokens": { + "message": "토큰을 추가하시겠습니까?" + }, + "links": { + "message": "링크" }, "limit": { - "message": "리밋" + "message": "한도" }, "loading": { "message": "로딩중..." @@ -335,11 +479,17 @@ "localhost": { "message": "로컬호스트 8545" }, + "login": { + "message": "로그인" + }, "logout": { "message": "로그아웃" }, "loose": { - "message": "외부 비밀키" + "message": "느슨함" + }, + "loweCaseWords": { + "message": "시드 단어는 소문자만 가능합니다" }, "mainnet": { "message": "이더리움 메인넷" @@ -347,94 +497,127 @@ "message": { "message": "메시지" }, + "metamaskDescription": { + "message": "메타마스크는 이더리움을 위한 안전한 신분 저장소입니다." + }, + "metamaskSeedWords": { + "message": "메타마스크 시드 단어" + }, "min": { "message": "최소" }, "myAccounts": { - "message": "내 계좌" + "message": "내 계정" + }, + "mustSelectOne": { + "message": "적어도 하나의 토큰을 선택하세요." }, "needEtherInWallet": { - "message": "dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다." + "message": "메타마스크를 통한 dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다." }, "needImportFile": { "message": "가져올 파일을 선택해주세요.", - "description": "User is important an account and needs to add a file to continue" + "description": "사용자는 계정을 가져오기 위해서 파일을 추가후 계속 진행해야 합니다" }, "needImportPassword": { - "message": "선택 된 파일에 패스워드를 입력해주세요.", - "description": "Password and file needed to import an account" + "message": "선택 된 파일에 대한 비밀번호를 입력해주세요.", + "description": "계정을 가져오기 위해서 비밀번호와 파일이 필요합니다." + }, + "negativeETH": { + "message": "음수값의 이더를 보낼 수 없습니다." }, "networks": { "message": "네트워크" }, + "nevermind": { + "message": "상관안함" + }, "newAccount": { - "message": "새 계좌" + "message": "새 계정" }, "newAccountNumberName": { - "message": "새 계좌 $1", - "description": "Default name of next account to be created on create account screen" + "message": "새 계정 $1", + "description": "계정 생성시 볼 수 있는 새 계정의 기본 이름" }, "newContract": { "message": "새 컨트랙트" }, "newPassword": { - "message": "새 패스워드 (최소 8자 이상)" + "message": "새 비밀번호 (최소 8자 이상)" }, "newRecipient": { "message": "받는 사람" }, + "newRPC": { + "message": "새로운 RPC URL" + }, "next": { "message": "다음" }, "noAddressForName": { - "message": "이 이름에는 주소가 설정되어 있지 않습니다." + "message": "이 이름에 대해 주소가 설정되어 있지 않습니다." }, "noDeposits": { - "message": "입금이 없습니다." + "message": "입금 내역이 없습니다." }, "noTransactionHistory": { "message": "트랜잭션 기록이 없습니다." }, "noTransactions": { - "message": "트랜잭션이 없습니다." + "message": "트랜잭션이 없습니다" }, "notStarted": { - "message": "시작되지 않음." + "message": "시작 안됨" }, "oldUI": { - "message": "구버전의 UI" + "message": "구버전 UI" }, "oldUIMessage": { - "message": "구버전 UI로 변경하셨습니다. 우 상단 드랍다운 메뉴에서 새 UI로 변경하실 수 있습니다." + "message": "구버전 UI로 변경하셨습니다. 우 상단 드롭다운 메뉴에서 새 UI로 변경하실 수 있습니다." }, "or": { "message": "또는", - "description": "choice between creating or importing a new account" + "description": "새 계정을 만들거나 가져오기중에 선택하기" + }, + "password": { + "message": "비밀번호" + }, + "passwordCorrect": { + "message": "비밀번호가 맞는지 확인해주세요." }, "passwordMismatch": { - "message": "패스워드가 일치하지 않습니다.", - "description": "in password creation process, the two new password fields did not match" + "message": "비밀번호가 일치하지 않습니다.", + "description": "비밀번호를 생성하는 과정에서 두개의 새 비밀번호 입력란이 일치하지 않습니다" }, "passwordShort": { - "message": "패스워드가 너무 짧습니다.", - "description": "in password creation process, the password is not long enough to be secure" + "message": "비밀번호가 짧습니다.", + "description": "비밀번호를 생성하는 과정에서 비밀번호가 짧으면 안전하지 않습니다" }, "pastePrivateKey": { - "message": "비밀키를 입력해주세요.", - "description": "For importing an account from a private key" + "message": "개인키를 입력해주세요:", + "description": "개인키로부터 계정을 가져오는 것입니다" }, "pasteSeed": { - "message": "시드 문장들을 붙여넣어주세요." + "message": "시드 구문을 이곳에 붙여넣어주세요!" + }, + "personalAddressDetected": { + "message": "개인 주소가 탐지됨. 토큰 컨트랙트 주소를 입력하세요." }, "pleaseReviewTransaction": { "message": "트랜잭션을 검토해주세요." }, + "popularTokens": { + "message": "인기있는 토큰" + }, + "privacyMsg": { + "message": "개인정보 보호 정책" + }, "privateKey": { - "message": "비밀키", - "description": "select this type of file to use to import an account" + "message": "개인키", + "description": "이 형식의 파일을 선택하여 계정을 가져올 수 있습니다" }, "privateKeyWarning": { - "message": " 절대 이 키를 노출하지 마십시오. 비밀키가 노출되면 누구나 당신의 계좌에서 자산을 빼갈 수 있습니다." + "message": " 절대 이 키를 노출하지 마십시오. 개인키가 노출되면 누구나 당신의 계정에서 자산을 빼갈 수 있습니다." }, "privateNetwork": { "message": "프라이빗 네트워크" @@ -446,28 +629,64 @@ "message": "옵션 메뉴에서 “토큰 추가”를 눌러서 추후에 다시 이 토큰을 추가하실 수 있습니다." }, "readMore": { - "message": "더 읽기." + "message": "여기서 더 보기." + }, + "readMore2": { + "message": "더 보기." }, "receive": { "message": "받기" }, "recipientAddress": { - "message": "받는 사람 주소" + "message": "받는 주소" }, "refundAddress": { "message": "환불받을 주소" }, "rejected": { - "message": "거부되었음." + "message": "거부됨" + }, + "reset": { + "message": "초기화" + }, + "resetAccount": { + "message": "계정 초기화" + }, + "resetAccountDescription": { + "message": "계정을 초기화 하는 경우에 트랜잭션 기록이 삭제됩니다." + }, + "restoreFromSeed": { + "message": "계정을 복구하시겠습니까?" + }, + "restoreVault": { + "message": "저장소 복구" }, "required": { - "message": "필요함." + "message": "필요함" }, "retryWithMoreGas": { - "message": "더 높은 가스 가격으로 다시 시도해주세요." + "message": "더 높은 가스 가격으로 다시 시도해주세요" + }, + "walletSeed": { + "message": "지갑 시드값" + }, + "revealSeedWords": { + "message": "시드 단어 보이기" + }, + "revealSeedWordsTitle": { + "message": "시드 단어" + }, + "revealSeedWordsDescription": { + "message": "브라우저를 바꾸거나 컴퓨터를 옮기는 경우 이 시드 구문이 필요하며 이를 통해 계정에 접근할 수 있습니다. 시드 구문을 안전한 곳에 보관하세요." + }, + "revealSeedWordsWarningTitle": { + "message": "이 구문을 다른사람과 절대로 공유하지 마세요!" + }, + "revealSeedWordsWarning": { + "message": "이 단어모음은 당신의 모든 계정을 훔치는데 사용할 수 있습니다." }, "revert": { - "message": "취소" + "message": "되돌림" }, "rinkeby": { "message": "Rinkeby 테스트넷" @@ -475,46 +694,122 @@ "ropsten": { "message": "Ropsten 테스트넷" }, + "rpc": { + "message": "사용자 정의 RPC" + }, + "currentRpc": { + "message": "현재 RPC" + }, + "connectingToMainnet": { + "message": "이더리움 메인넷 접속중" + }, + "connectingToRopsten": { + "message": "Ropsten 테스트넷 접속중" + }, + "connectingToKovan": { + "message": "Kovan 테스트넷 접속중" + }, + "connectingToRinkeby": { + "message": "Rinkeby 테스트넷 접속중" + }, + "connectingToUnknown": { + "message": "알려지지 않은 네트워크 접속중" + }, "sampleAccountName": { - "message": "예) 나의 새 계좌", - "description": "Help user understand concept of adding a human-readable name to their account" + "message": "예) 나의 새 계정", + "description": "각 계정에 대해서 구별하기 쉬운 이름을 지정하여 사용자가 쉽게 이해할 수 있게 합니다" }, "save": { "message": "저장" }, + "speedUpTitle": { + "message": "트랜잭션 속도 향상하기" + }, + "speedUpSubtitle": { + "message": "트랜잭션 가스 가격을 늘려서 해당 트랙잭션에 덮어쓰기 하여 속도를 빠르게 합니다" + }, + "saveAsCsvFile": { + "message": "CSV 파일로 저장" + }, "saveAsFile": { "message": "파일로 저장", - "description": "Account export process" + "description": "계정 내보내기 절차" + }, + "saveSeedAsFile": { + "message": "시드 단어를 파일로 저장하기" + }, + "search": { + "message": "검색" + }, + "searchResults": { + "message": "검색 결과" + }, + "secretPhrase": { + "message": "12개 단어로 구성된 비밀 구문을 입력하여 저장소를 복구하세요." + }, + "newPassword8Chars": { + "message": "새 비밀번호 (최소 8문자)" + }, + "seedPhraseReq": { + "message": "시드 구문은 12개의 단어입니다" + }, + "select": { + "message": "선택" + }, + "selectCurrency": { + "message": "통화 선택" }, "selectService": { "message": "서비스 선택" }, + "selectType": { + "message": "형식 선택" + }, "send": { "message": "전송" }, + "sendETH": { + "message": "ETH 보내기" + }, "sendTokens": { "message": "토큰 전송" }, + "onlySendToEtherAddress": { + "message": "이더리움 주소로 ETH만 송금하세요." + }, + "onlySendTokensToAccountAddress": { + "message": "이더리움계정 주소로 $1 만 보내기.", + "description": "토큰 심볼 보이기" + }, + "searchTokens": { + "message": "토큰 검색" + }, "sendTokensAnywhere": { - "message": "이더 계좌로 토큰 전송" + "message": "이더 계정로 토큰 전송" }, "settings": { "message": "설정" }, + "info": { + "message": "정보" + }, "shapeshiftBuy": { "message": "Shapeshift를 통해서 구매하기" }, "showPrivateKeys": { - "message": "비밀키 보기" + "message": "개인키 보기" }, "showQRCode": { - "message": "QR코드 보기" + "message": "QR 코드 보기" }, "sign": { "message": "서명" }, + "signed": { + "message": "서명됨" + }, "signMessage": { - "message": "서명 메시지" + "message": "메시지 서명" }, "signNotice": { "message": "이 메시지에 대한 서명은 위험할 수 있습니다.\n 완전히 신뢰할 수 있는 사이트에서만 서명해주세요.\n 안전을 위해 추후의 버전에서는 삭제될 기능입니다. " @@ -523,33 +818,81 @@ "message": "서명 요청" }, "sigRequested": { - "message": "서명이 요청되었습니다." + "message": "서명이 요청됨" + }, + "spaceBetween": { + "message": "단어 사이에는 공백만 올 수 있습니다" }, "status": { "message": "상태" }, + "stateLogs": { + "message": "상태 로그" + }, + "stateLogsDescription": { + "message": "상태 로그에는 공개 계정 주소와 송신 트랜잭션 정보가 들어있습니다." + }, + "stateLogError": { + "message": "상태 로그 받기 실패." + }, "submit": { "message": "제출" }, + "submitted": { + "message": "제출됨" + }, + "supportCenter": { + "message": "지원 센터에 방문하기" + }, + "symbolBetweenZeroTen": { + "message": "심볼은 0에서 10개 사이의 문자여야 합니다." + }, "takesTooLong": { "message": "너무 오래걸리나요?" }, + "terms": { + "message": "사용 지침" + }, "testFaucet": { - "message": "Faucet 테스트" + "message": "파우셋 테스트" }, "to": { - "message": "대상" + "message": "받는이: " }, "toETHviaShapeShift": { "message": "ShapeShift를 통해 $1를 ETH로 바꾸기", - "description": "system will fill in deposit type in start of message" + "description": "시스템이 시작할 때에 입금 유형을 입력해줍니다" + }, + "token": { + "message": "토큰" + }, + "tokenAddress": { + "message": "토큰 주소" + }, + "tokenAlreadyAdded": { + "message": "토큰이 이미 추가되어있습니다." }, "tokenBalance": { - "message": "현재 토큰 잔액: " + "message": "현재 토큰 잔액:" + }, + "tokenSelection": { + "message": "토큰을 검색하거나 유명한 토큰 리스트에서 선택하시기 바랍니다." + }, + "tokenSymbol": { + "message": "토큰 기호" + }, + "tokenWarning1": { + "message": "메타마스크 계좌를 통해 구입한 토큰을 추적합니다. 다른 계정으로 토큰을 구입한 경우 이곳에 나타나지 않습니다." }, "total": { "message": "합계" }, + "transactions": { + "message": "트랜잭션" + }, + "transactionError": { + "message": "트랜잭션 오류. 컨트랙트 코드에서 예외 발생(Exception thrown)." + }, "transactionMemo": { "message": "트랜잭션 메모 (선택사항)" }, @@ -560,23 +903,29 @@ "message": "전송" }, "troubleTokenBalances": { - "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. (여기)서 상세내용을 볼 수 있습니다.", - "description": "Followed by a link (here) to view token balances" + "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. 링크에서 상세내용을 볼 수 있습니다.", + "description": "토큰 잔액을 보려면 (here) 링크를 따라가세요" + }, + "twelveWords": { + "message": "12개의 단어는 메타마스크 계정을 복구하기 위한 유일한 방법입니다.\n안전한 장소에 보관하시기 바랍니다." }, "typePassword": { - "message": "패스워드를 입력하세요." + "message": "비밀번호를 입력하세요" }, "uiWelcome": { - "message": "새 UI에 오신 것을 환영합니다. (베타)" + "message": "새로운 UI에 오신 것을 환영합니다. (Beta)" }, "uiWelcomeMessage": { - "message": "새 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요." + "message": "새로운 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요." + }, + "unapproved": { + "message": "허가안됨" }, "unavailable": { - "message": "유효하지 않은" + "message": "유효하지 않음" }, "unknown": { - "message": "알려지지 않은" + "message": "알려지지 않음" }, "unknownNetwork": { "message": "알려지지 않은 프라이빗 네트워크" @@ -584,19 +933,46 @@ "unknownNetworkId": { "message": "알려지지 않은 네트워크 ID" }, + "unlockMessage": { + "message": "우리가 기다리던 분권형 웹입니다" + }, + "uriErrorMsg": { + "message": "URI는 HTTP/HTTPS로 시작해야 합니다." + }, "usaOnly": { "message": "USA 거주자 한정", - "description": "Using this exchange is limited to people inside the USA" + "description": "해당 거래소는 USA거주자에 한해서만 사용가능합니다" }, "usedByClients": { - "message": "다양한 클라이언트에서 사용되고 있습니다." + "message": "다양한 클라이언트에서 사용되고 있습니다" + }, + "useOldUI": { + "message": "예전 UI 사용" + }, + "validFileImport": { + "message": "가져오기 위해 유효한 파일을 선택해야 합니다." + }, + "vaultCreated": { + "message": "저장소가 생성됨" }, "viewAccount": { - "message": "계좌 보기" + "message": "계정 보기" + }, + "viewOnEtherscan": { + "message": "이터스캔에서 보기" + }, + "visitWebSite": { + "message": "웹사이트 방문" }, "warning": { "message": "경고" }, + "welcomeBack": { + "message": "환영합니다!" + }, + "welcomeBeta": { + "message": "메타마스크 Beta에 오신 것을 환영합니다" + }, "whatsThis": { "message": "이것은 무엇인가요?" }, @@ -604,6 +980,204 @@ "message": "서명이 요청되고 있습니다." }, "youSign": { - "message": "서명 중입니다." + "message": "서명 중입니다" + }, + "yourPrivateSeedPhrase": { + "message": "개인 시드 구문" + }, + "accessingYourCamera": { + "message": "카메라 접근중..." + }, + "accountSelectionRequired": { + "message": "계정을 선택하셔야 합니다!" + }, + "approve": { + "message": "수락" + }, + "browserNotSupported": { + "message": "브라우저가 지원하지 않습니다..." + }, + "bytes": { + "message": "바이트" + }, + "chromeRequiredForHardwareWallets": { + "message": "하드웨어 지갑을 연결하기 위해서는 구글 크롬에서 메타마스크를 사용하셔야 합니다." + }, + "connectHardwareWallet": { + "message": "하드웨어 지갑 연결" + }, + "connect": { + "message": "연결" + }, + "connecting": { + "message": "연결중..." + }, + "connectToLedger": { + "message": "Ledger 연결" + }, + "connectToTrezor": { + "message": "Trezor 연결" + }, + "copyAddress": { + "message": "클립보드로 주소 복사" + }, + "downloadGoogleChrome": { + "message": "구글 크롬 다운로드" + }, + "dontHaveAHardwareWallet": { + "message": "하드웨어 지갑이 없나요?" + }, + "ensNameNotFound": { + "message": "ENS 이름을 찾을 수 없습니다" + }, + "parameters": { + "message": "매개변수" + }, + "forgetDevice": { + "message": "장치 연결 해제" + }, + "functionType": { + "message": "함수 유형" + }, + "getHelp": { + "message": "도움말" + }, + "hardware": { + "message": "하드웨어" + }, + "hardwareWalletConnected": { + "message": "하드웨어 지갑이 연결됨" + }, + "hardwareWallets": { + "message": "하드웨어 지갑 연결" + }, + "hardwareWalletsMsg": { + "message": "메타마스크에서 사용할 하드웨어 지갑을 선택해주세요" + }, + "havingTroubleConnecting": { + "message": "연결에 문제가 있나요?" + }, + "hexData": { + "message": "Hex 데이터" + }, + "invalidSeedPhrase": { + "message": "잘못된 시드 구문" + }, + "ledgerAccountRestriction": { + "message": "새 계정을 추가하려면 최소 마지막 계정을 사용해야 합니다." + }, + "menu": { + "message": "메뉴" + }, + "noConversionRateAvailable": { + "message": "변환 비율을 찾을 수 없습니다" + }, + "notFound": { + "message": "찾을 수 없음" + }, + "noWebcamFoundTitle": { + "message": "웹캠이 없습니다" + }, + "noWebcamFound": { + "message": "컴퓨터의 웹캠을 찾을 수 없습니다. 다시 시도해보세요." + }, + "openInTab": { + "message": "탭으로 열기" + }, + "origin": { + "message": "Origin" + }, + "prev": { + "message": "이전" + }, + "restoreAccountWithSeed": { + "message": "시드 구문으로 계정 복구하기" + }, + "restore": { + "message": "복구" + }, + "remove": { + "message": "제거" + }, + "removeAccount": { + "message": "계정 제거" + }, + "removeAccountDescription": { + "message": "이 계정한 지갑에서 삭제될 것입니다. 지우기 전에 이 계정에 대한 개인 키 혹은 시드 구문을 가지고 있는지 확인하세요. 계정 드랍다운 메뉴를 통해서 계정을 가져오거나 생성할 수 있습니다." + }, + "readyToConnect": { + "message": "접속 준비되었나요?" + }, + "separateEachWord": { + "message": "각 단어는 공백 한칸으로 분리합니다" + }, + "orderOneHere": { + "message": "Trezor 혹은 Ledger를 구입하고 자금을 콜드 스토리지에 저장합니다" + }, + "selectAnAddress": { + "message": "주소 선택" + }, + "selectAnAccount": { + "message": "계정 선택" + }, + "selectAnAccountHelp": { + "message": "메타마스크에서 보기위한 계정 선택" + }, + "selectHdPath": { + "message": "HD 경로 지정" + }, + "selectPathHelp": { + "message": "하단에서 Ledger지갑 계정을 찾지 못하겠으면 \"Legacy (MEW / MyCrypto)\" 경로로 바꿔보세요" + }, + "step1HardwareWallet": { + "message": "1. 하드웨어 지갑 연결" + }, + "step1HardwareWalletMsg": { + "message": "하드웨어 지갑을 컴퓨터에 연결해주세요." + }, + "step2HardwareWallet": { + "message": "2. 계정 선택" + }, + "step2HardwareWalletMsg": { + "message": "보고 싶은 계정을 선택합니다. 한번에 하나의 계정만 선택할 수 있습니다." + }, + "step3HardwareWallet": { + "message": "3. dApps을 사용하거나 다른 것을 합니다!" + }, + "step3HardwareWalletMsg": { + "message": "다른 이더리움 계정을 사용하듯 하드웨어 계정을 사용합니다. dApps을 로그인하거나, 이더를 보내거나, ERC20토큰 혹은 대체가능하지 않은 토큰 (예를 들어 CryptoKitties)을 사거나 저장하거나 합니다." + }, + "scanInstructions": { + "message": "QR 코드를 카메라 앞에 가져다 놓아주세요" + }, + "scanQrCode": { + "message": "QR 코드 스캔" + }, + "transfer": { + "message": "전송" + }, + "trezorHardwareWallet": { + "message": "TREZOR 하드웨어 지갑" + }, + "tryAgain": { + "message": "다시 시도하세요" + }, + "unknownFunction": { + "message": "알 수 없는 함수" + }, + "unknownQrCode": { + "message": "오류: QR 코드를 확인할 수 없습니다" + }, + "unknownCameraErrorTitle": { + "message": "이런! 뭔가 잘못되었습니다...." + }, + "unknownCameraError": { + "message": "카메라를 접근하는 중 오류가 발생했습니다. 다시 시도해 주세요..." + }, + "unlock": { + "message": "잠금 해제" + }, + "youNeedToAllowCameraAccess": { + "message": "이 기능을 사용하려면 카메라 접근을 허용해야 합니다." } } diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 38289f602..46847f1bf 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -81,7 +81,7 @@ "message": "Koop op Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase is 's werelds populairste manier om bitcoin, ethereum en litecoin te kopen en verkopen." + "message": "Coinbase is 's werelds populairste manier om Bitcoin, Ethereum en Litecoin te kopen en verkopen." }, "cancel": { "message": "Annuleer" @@ -435,7 +435,7 @@ "message": "back-up woorden hebben alleen kleine letters" }, "mainnet": { - "message": "belangrijkste ethereum-netwerk" + "message": "belangrijkste Ethereum-netwerk" }, "message": { "message": "Bericht" @@ -762,7 +762,7 @@ "message": "Welkom bij de nieuwe gebruikersinterface (bèta)" }, "uiWelcomeMessage": { - "message": "U gebruikt nu de nieuwe gebruikersinterface van Metamask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt." + "message": "U gebruikt nu de nieuwe gebruikersinterface van MetaMask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt." }, "unavailable": { "message": "Niet beschikbaar" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 29d63be02..9a243447a 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -63,7 +63,7 @@ "message": "Bumili sa Coinbase" }, "buyCoinbaseExplainer": { - "message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng bitcoin, ethereum, at litecoin sa buong mundo." + "message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng Bitcoin, Ethereum, at Litecoin sa buong mundo." }, "cancel": { "message": "Kanselahin" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e770392d0..6f0bb1584 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -81,7 +81,7 @@ "message": "Comprar no Coinbase" }, "buyCoinbaseExplainer": { - "message": "Coinbase é a forma mais conhecida para comprar e vender bitcoin, ethereum, e litecoin." + "message": "Coinbase é a forma mais conhecida para comprar e vender Bitcoin, Ethereum, e Litecoin." }, "cancel": { "message": "Cancelar" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b43251064..6344e1beb 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -84,7 +84,7 @@ "message": "Купить на Coinbase" }, "buyCoinbaseExplainer": { - "message": "Биржа Coinbase – это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin." + "message": "Биржа Coinbase – это наиболее популярный способ купить или продать Bitcoin, Ethereum и Litecoin." }, "ok": { "message": "ОК" @@ -784,7 +784,7 @@ "message": "Тестовый кран" }, "to": { - "message": "Получатель: " + "message": "Получатель" }, "toETHviaShapeShift": { "message": "$1 в ETH через ShapeShift", diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 3d7dec226..c9aaa0394 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -762,7 +762,7 @@ "message": "ยินดีต้อนรับสู่หน้าตาใหม่ (เบต้า)" }, "uiWelcomeMessage": { - "message": "ขณะนี้คุณใช้งาน Metamask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ" + "message": "ขณะนี้คุณใช้งาน MetaMask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ" }, "unavailable": { "message": "ใช้งานไม่ได้" diff --git a/app/_locales/tml/messages.json b/app/_locales/tml/messages.json index fcc418bac..4f733458e 100644 --- a/app/_locales/tml/messages.json +++ b/app/_locales/tml/messages.json @@ -796,7 +796,7 @@ "message": "சோதனை குழாய்" }, "to": { - "message": "பெறுநர்: " + "message": "பெறுநர்" }, "toETHviaShapeShift": { "message": "$ 1 முதல் ETH வரை வடிவம்", diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index c2c09bc91..8be695108 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -84,7 +84,7 @@ "message": "Coinbase'de satın al" }, "buyCoinbaseExplainer": { - "message": "Coinbase bitcoin, ethereum, and litecoin alıp satmanın dünyadaki en popüler yolu" + "message": "Coinbase Bitcoin, Ethereum, and Litecoin alıp satmanın dünyadaki en popüler yolu" }, "ok": { "message": "Tamam" @@ -796,7 +796,7 @@ "message": "Test Musluğu" }, "to": { - "message": "Kime: " + "message": "Kime" }, "toETHviaShapeShift": { "message": "ShapeShift üstünden $1'dan ETH'e", @@ -852,7 +852,7 @@ "message": "Yeni UI (Beta)'ya hoşgeldiniz" }, "uiWelcomeMessage": { - "message": "Şu anda yeni Metamask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin" + "message": "Şu anda yeni MetaMask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin" }, "unapproved": { "message": "Onaylanmadı" @@ -909,4 +909,4 @@ "youSign": { "message": "İmzalıyorsunuz" } -}
\ No newline at end of file +} diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index cd30de4de..782dfd119 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -570,7 +570,7 @@ "message": "Chào mừng bạn đến với giao diện mới (Beta)" }, "uiWelcomeMessage": { - "message": "Bạn đang sử dụng giao diện mới của Metamask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn." + "message": "Bạn đang sử dụng giao diện mới của MetaMask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn." }, "unavailable": { "message": "Không có sẵn" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 241ea948d..8ce0671a4 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -23,6 +23,9 @@ "addTokens": { "message": "添加代币" }, + "addAcquiredTokens": { + "message": "在Metamask上添加已用的代币" + }, "amount": { "message": "数量" }, @@ -116,6 +119,9 @@ "confirmTransaction": { "message": "确认交易" }, + "connectHardwareWallet": { + "message": "链接硬件钱包" + }, "continue": { "message": "继续" }, @@ -714,6 +720,9 @@ "search": { "message": "搜索" }, + "searchResults": { + "message": "搜索结果" + }, "secretPhrase": { "message": "输入12位助记词以恢复金库." }, @@ -879,7 +888,7 @@ "message": "欢迎使用新版界面 (Beta)" }, "uiWelcomeMessage": { - "message": "你现在正在使用新的 Metamask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" + "message": "你现在正在使用新的 MetaMask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" }, "unapproved": { "message": "未批准" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 9aaee0e16..200c75c3c 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -362,7 +362,7 @@ "message": "你想怎麼存入 Ether?" }, "holdEther": { - "message": "Metamask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑." + "message": "MetaMask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑." }, "import": { "message": "導入", diff --git a/app/images/arrow-popout.svg b/app/images/arrow-popout.svg new file mode 100644 index 000000000..7e25f7cd2 --- /dev/null +++ b/app/images/arrow-popout.svg @@ -0,0 +1,3 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.67589 0.641872C8.65169 0.642635 8.62756 0.644749 8.6036 0.648202H4.79279C4.55863 0.644896 4.34082 0.767704 4.22278 0.969601C4.10473 1.1715 4.10473 1.4212 4.22278 1.6231C4.34082 1.825 4.55863 1.9478 4.79279 1.9445H7.12113L0.437932 8.61587C0.268309 8.77843 0.19998 9.01984 0.259298 9.24697C0.318616 9.47411 0.496311 9.65149 0.723852 9.71071C0.951393 9.76992 1.19322 9.70171 1.35608 9.53239L8.03927 2.86102V5.18524C8.03596 5.41898 8.15899 5.6364 8.36124 5.75424C8.56349 5.87208 8.81364 5.87208 9.0159 5.75424C9.21815 5.6364 9.34118 5.41898 9.33787 5.18524V1.37863C9.36404 1.18976 9.30558 0.998955 9.17804 0.857009C9.0505 0.715062 8.86682 0.636369 8.67589 0.641872Z" fill="#359BDD"/> +</svg> diff --git a/app/images/ledger-logo.svg b/app/images/ledger-logo.svg new file mode 100644 index 000000000..5ca90e16e --- /dev/null +++ b/app/images/ledger-logo.svg @@ -0,0 +1,33 @@ +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 197.4 48.6" style="enable-background:new 0 0 197.4 48.6;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#1D2028;} +</style> +<title>Fichier 8</title> +<g> + <path class="st0" d="M34.1,0H15.5v25.1h25.1V6.5C40.6,2.9,37.7,0,34.1,0z"/> + <path class="st0" d="M9.7,0H6.5C2.9,0,0,2.9,0,6.5v3.2h9.7V0z"/> + <rect y="15.5" class="st0" width="9.7" height="9.7"/> + <path class="st0" d="M31,40.6h3.2c3.6,0,6.5-2.9,6.5-6.5V31H31V40.6z"/> + <rect x="15.5" y="31" class="st0" width="9.7" height="9.7"/> + <path class="st0" d="M0,31v3.2c0,3.6,2.9,6.5,6.5,6.5h3.2V31H0z"/> + <g> + <polygon class="st0" points="65.4,2.6 61.6,2.6 61.6,38.1 81.7,38.1 81.7,34.7 65.4,34.7 "/> + <path class="st0" d="M93.9,12c-7.4,0-12.6,5.5-12.6,13.4c0,0.3,0,0.6,0,0.9c0.1,3.4,1.6,6.6,4.1,9c2.4,2.2,5.5,3.5,8.8,3.5 + c0.2,0,0.3,0,0.5,0c3.5,0,6.8-1.3,9.4-3.5l0.1-0.1l-1.7-2.8l-0.2,0.1c-2.1,1.9-4.7,3-7.5,3c-4.6,0-9.3-3-9.6-9.7h19.2v-0.2 + c0,0,0.1-1.2,0.1-1.8C104.5,16.6,100.3,12,93.9,12z M85.3,22.6c0.8-4.5,4.1-7.4,8.4-7.4c3.2,0,6.7,1.9,7,7.4H85.3z"/> + <path class="st0" d="M126.5,15c0,0.4,0,0.9,0,1.3c-1.6-2.7-4.6-4.4-7.7-4.4c-0.1,0-0.2,0-0.3,0c-6.8,0-11.5,5.4-11.5,13.3 + c0,8,4.5,13.4,11.2,13.4c5.3,0,7.7-3.2,8.5-4.6c0,0.4,0,0.8,0,1.1v2.8h3.6V2.6h-3.7V15H126.5z M118.7,35.3c-4.7,0-7.8-4-7.8-10 + c0-5.8,3.3-9.9,7.9-9.9c3.9,0,7.8,3.1,7.8,9.9C126.6,32.7,122.5,35.3,118.7,35.3z"/> + <path class="st0" d="M152.2,15.5c0,0.1,0,0.2,0,0.2c-0.7-1.2-2.9-3.8-8.2-3.8c-6.7,0-11.1,5.1-11.1,12.9s4.6,13.1,11.4,13.1 + c3.7,0,6.2-1.3,7.9-4c0,0.4,0,0.8,0,1.2v2.3c0,4.9-3.1,7.7-8.6,7.7c-2.3,0-4.7-0.6-6.8-1.7l-0.2-0.1l-1.4,3.1l0.2,0.1 + c2.6,1.3,5.5,2,8.3,2c5.9,0,12.2-3,12.2-11.3V12.6h-3.7L152.2,15.5L152.2,15.5z M144.8,34.6c-4.9,0-8.1-3.8-8.1-9.7 + c0-6,2.8-9.4,7.6-9.4c5.3,0,7.8,3.1,7.8,9.4C152.2,31.1,149.6,34.6,144.8,34.6z"/> + <path class="st0" d="M171,12c-7.4,0-12.5,5.5-12.5,13.3c0,0.3,0,0.6,0,0.9c0.1,3.4,1.6,6.6,4.1,9c2.4,2.2,5.5,3.5,8.8,3.5 + c0.2,0,0.3,0,0.5,0c3.5,0,6.8-1.3,9.4-3.5l0.1-0.1l-1.8-2.8l-0.2,0.1c-2.1,1.9-4.7,3-7.5,3c-4.6,0-9.3-3-9.6-9.7h19.3v-0.2 + c0,0,0.1-1.2,0.1-1.8C181.7,16.6,177.5,12,171,12z M162.5,22.6c0.8-4.5,4.1-7.4,8.4-7.4c3.2,0,6.7,1.9,7,7.4H162.5z"/> + <path class="st0" d="M197.3,12.5c-0.5-0.1-0.9-0.1-1.4-0.2c-3.5,0-6.4,2.2-7.9,5.9c0-0.3,0-0.7,0-1.1v-4.6h-3.7l0.1,25.3V38h3.8 + V27.3c0-1.6,0.2-3.3,0.7-4.8c1.2-3.9,3.9-6.4,7.1-6.4c0.4,0,0.8,0,1.2,0.1h0.2v-3.7L197.3,12.5z"/> + </g> +</g> +</svg> diff --git a/app/images/metamask-fox.svg b/app/images/logo/metamask-fox.svg index f3c24f79e..f3c24f79e 100644 --- a/app/images/metamask-fox.svg +++ b/app/images/logo/metamask-fox.svg diff --git a/app/images/logo/metamask-logo-horizontal-beta.svg b/app/images/logo/metamask-logo-horizontal-beta.svg new file mode 100644 index 000000000..b1fa040ac --- /dev/null +++ b/app/images/logo/metamask-logo-horizontal-beta.svg @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1672.1 241.2" + style="enable-background:new 0 0 1672.1 241.2;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#161616;} + .st1{fill:#E17726;stroke:#E17726;stroke-linecap:round;stroke-linejoin:round;} + .st2{fill:#E27625;stroke:#E27625;stroke-linecap:round;stroke-linejoin:round;} + .st3{fill:#D5BFB2;stroke:#D5BFB2;stroke-linecap:round;stroke-linejoin:round;} + .st4{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;} + .st5{fill:#CC6228;stroke:#CC6228;stroke-linecap:round;stroke-linejoin:round;} + .st6{fill:#E27525;stroke:#E27525;stroke-linecap:round;stroke-linejoin:round;} + .st7{fill:#F5841F;stroke:#F5841F;stroke-linecap:round;stroke-linejoin:round;} + .st8{fill:#C0AC9D;stroke:#C0AC9D;stroke-linecap:round;stroke-linejoin:round;} + .st9{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;} + .st10{fill:#763E1A;stroke:#763E1A;stroke-linecap:round;stroke-linejoin:round;} + .st11{fill:#F5841F;} +</style> +<g> + <path class="st0" d="M1157.7,121.9c-6.8-4.5-14.3-7.7-21.4-11.7c-4.6-2.6-9.5-4.9-13.5-8.2c-6.8-5.6-5.4-16.6,1.7-21.4 + c10.2-6.8,27.1-3,28.9,10.9c0,0.3,0.3,0.5,0.6,0.5h15.4c0.4,0,0.7-0.3,0.6-0.7c-0.8-9.6-4.5-17.6-11.3-22.7 + c-6.5-4.9-13.9-7.5-21.8-7.5c-40.7,0-44.4,43.1-22.5,56.7c2.5,1.6,24,12.4,31.6,17.1s10,13.3,6.7,20.1c-3,6.2-10.8,10.5-18.6,10 + c-8.5-0.5-15.1-5.1-17.4-12.3c-0.4-1.3-0.6-3.8-0.6-4.9c0-0.3-0.3-0.6-0.6-0.6h-16.7c-0.3,0-0.6,0.3-0.6,0.6 + c0,12.1,3,18.8,11.2,24.9c7.7,5.8,16.1,8.2,24.8,8.2c22.8,0,34.6-12.9,37-26.3C1173.3,141.5,1169.4,129.7,1157.7,121.9z"/> + <path class="st0" d="M432.6,63.3h-7.4h-8.1c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2 + c-0.1-0.3-0.3-0.4-0.6-0.4h-8.1h-7.4h-10c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6 + c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5 + c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L432.6,63.3 + L432.6,63.3z"/> + <path class="st0" d="M902,63.3c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2c-0.1-0.3-0.3-0.4-0.6-0.4h-25.4 + c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2 + c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7 + c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L902,63.3L902,63.3z"/> + <path class="st0" d="M686.6,63.3h-31.1h-16.7h-31.1c-0.3,0-0.6,0.3-0.6,0.6v14.4c0,0.3,0.3,0.6,0.6,0.6h30.5v100.4 + c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V78.9h30.5c0.3,0,0.6-0.3,0.6-0.6V63.9C687.2,63.6,687,63.3,686.6,63.3z"/> + <path class="st0" d="M785.1,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8L769.5,63.3c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8 + c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6 + c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C784.6,179.7,784.9,179.9,785.1,179.9z M745.2,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1 + c0.1,0.4-0.2,0.8-0.6,0.8h-24.4C745.4,129.7,745.1,129.3,745.2,128.9z"/> + <path class="st0" d="M1044.3,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8l-31.4-115.8c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8 + c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6 + c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C1043.8,179.7,1044,179.9,1044.3,179.9z M1004.4,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1 + c0.1,0.4-0.2,0.8-0.6,0.8H1005C1004.6,129.7,1004.3,129.3,1004.4,128.9z"/> + <path class="st0" d="M510.8,162.8V127c0-0.3,0.3-0.6,0.6-0.6h44.5c0.3,0,0.6-0.3,0.6-0.6v-14.4c0-0.3-0.3-0.6-0.6-0.6h-44.5 + c-0.3,0-0.6-0.3-0.6-0.6V79.6c0-0.3,0.3-0.6,0.6-0.6H562c0.3,0,0.6-0.3,0.6-0.6V64c0-0.3-0.3-0.6-0.6-0.6h-51.2h-17.3 + c-0.3,0-0.6,0.3-0.6,0.6v15v31.9v15.6v37v15.8c0,0.3,0.3,0.6,0.6,0.6h17.3h53.3c0.3,0,0.6-0.3,0.6-0.6v-15.2c0-0.3-0.3-0.6-0.6-0.6 + h-52.8C511,163.4,510.8,163.2,510.8,162.8z"/> + <path class="st0" d="M1310.3,178.9l-57.8-59.7c-0.2-0.2-0.2-0.6,0-0.8l52-54c0.4-0.4,0.1-1-0.4-1h-21.3c-0.2,0-0.3,0.1-0.4,0.2 + l-44.1,45.8c-0.4,0.4-1,0.1-1-0.4V64c0-0.3-0.3-0.6-0.6-0.6H1220c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7 + c0.3,0,0.6-0.3,0.6-0.6v-50.8c0-0.5,0.7-0.8,1-0.4l50,51.6c0.1,0.1,0.3,0.2,0.4,0.2h21.3C1310.4,179.9,1310.7,179.2,1310.3,178.9z" + /> +</g> +<g> + <polygon class="st1" points="247.1,1.2 146,76.2 164.8,32 "/> + <g> + <polygon class="st2" points="13.9,1.2 114.1,76.9 96.2,32 "/> + <polygon class="st2" points="210.7,175.1 183.8,216.3 241.4,232.2 257.9,176 "/> + <polygon class="st2" points="3.2,176 19.6,232.2 77.1,216.3 50.3,175.1 "/> + <polygon class="st2" points="74,105.5 58,129.7 115,132.3 113.1,70.8 "/> + <polygon class="st2" points="187,105.5 147.3,70.1 146,132.3 203,129.7 "/> + <polygon class="st2" points="77.1,216.3 111.6,199.6 81.9,176.4 "/> + <polygon class="st2" points="149.4,199.6 183.8,216.3 179.1,176.4 "/> + </g> + <g> + <polygon class="st3" points="183.8,216.3 149.4,199.6 152.2,222 151.9,231.5 "/> + <polygon class="st3" points="77.1,216.3 109.1,231.5 108.9,222 111.6,199.6 "/> + </g> + <polygon class="st4" points="109.7,161.6 81.1,153.2 101.3,143.9 "/> + <polygon class="st4" points="151.3,161.6 159.7,143.9 180,153.2 "/> + <g> + <polygon class="st5" points="77.1,216.3 82.1,175.1 50.3,176 "/> + <polygon class="st5" points="178.9,175.1 183.8,216.3 210.7,176 "/> + <polygon class="st5" points="203,129.7 146,132.3 151.3,161.6 159.7,143.9 180,153.2 "/> + <polygon class="st5" points="81.1,153.2 101.3,143.9 109.7,161.6 115,132.3 58,129.7 "/> + </g> + <g> + <polygon class="st6" points="58,129.7 81.9,176.4 81.1,153.2 "/> + <polygon class="st6" points="180,153.2 179.1,176.4 203,129.7 "/> + <polygon class="st6" points="115,132.3 109.7,161.6 116.4,196.2 117.9,150.6 "/> + <polygon class="st6" points="146,132.3 143.2,150.5 144.6,196.2 151.3,161.6 "/> + </g> + <polygon class="st7" points="151.3,161.6 144.6,196.2 149.4,199.6 179.1,176.4 180,153.2 "/> + <polygon class="st7" points="81.1,153.2 81.9,176.4 111.6,199.6 116.4,196.2 109.7,161.6 "/> + <polygon class="st8" points="151.9,231.5 152.2,222 149.6,219.8 111.4,219.8 108.9,222 109.1,231.5 77.1,216.3 88.3,225.5 + 111,241.2 149.9,241.2 172.7,225.5 183.8,216.3 "/> + <polygon class="st9" points="149.4,199.6 144.6,196.2 116.4,196.2 111.6,199.6 108.9,222 111.4,219.8 149.6,219.8 152.2,222 "/> + <g> + <polygon class="st10" points="251.4,81.1 259.9,39.7 247.1,1.2 149.4,73.7 187,105.5 240.1,121 251.8,107.3 246.7,103.6 + 254.8,96.2 248.6,91.4 256.7,85.2 "/> + <polygon class="st10" points="1.1,39.7 9.7,81.1 4.2,85.2 12.4,91.4 6.2,96.2 14.3,103.6 9.2,107.3 20.9,121 74,105.5 111.6,73.7 + 13.9,1.2 "/> + </g> + <polygon class="st7" points="240.1,121 187,105.5 203,129.7 179.1,176.4 210.7,176 257.9,176 "/> + <polygon class="st7" points="74,105.5 20.9,121 3.2,176 50.3,176 81.9,176.4 58,129.7 "/> + <polygon class="st7" points="146,132.3 149.4,73.7 164.8,32 96.2,32 111.6,73.7 115,132.3 116.3,150.7 116.4,196.2 144.6,196.2 + 144.7,150.7 "/> +</g> +<g> + <path class="st7" d="M1409.7,92.6V32.1h19.3c2.9,0,5.6,0.4,8.2,1c2.5,0.6,4.7,1.6,6.6,2.9c1.9,1.3,3.4,3,4.5,5.1 + c1.1,2.1,1.6,4.5,1.6,7.3c0,3-0.9,5.5-2.5,7.7c-1.6,2.1-3.8,3.8-6.6,4.9c1.7,0.5,3.2,1.1,4.5,2c1.3,0.9,2.4,1.9,3.3,3.1 + c0.9,1.2,1.6,2.6,2,4c0.5,1.5,0.7,3.1,0.7,4.7c0,2.9-0.5,5.4-1.6,7.6c-1.1,2.2-2.5,4-4.4,5.5c-1.9,1.5-4.1,2.6-6.6,3.3 + c-2.6,0.8-5.3,1.2-8.3,1.2H1409.7z M1419.8,57.6h9.6c1.5,0,2.9-0.2,4.2-0.6c1.3-0.4,2.4-0.9,3.3-1.7c0.9-0.7,1.7-1.6,2.2-2.7 + c0.6-1.1,0.8-2.3,0.8-3.7c0-1.5-0.3-2.8-0.8-3.9c-0.5-1.1-1.3-2-2.2-2.7c-1-0.7-2.1-1.2-3.4-1.5c-1.3-0.3-2.7-0.5-4.3-0.5h-9.5 + V57.6z M1419.8,65.2v19.3h10.9c1.6,0,3-0.3,4.3-0.7c1.3-0.5,2.4-1.1,3.4-1.9c0.9-0.8,1.7-1.8,2.2-2.9c0.5-1.2,0.8-2.5,0.8-3.9 + c0-1.5-0.2-2.9-0.7-4.1c-0.5-1.2-1.1-2.2-2-3.1c-0.9-0.8-2-1.5-3.2-1.9c-1.3-0.5-2.7-0.7-4.3-0.7H1419.8z"/> + <path class="st7" d="M1506.3,65.5h-24.9v19h29.1v8.1h-39.1V32.1h38.8v8.2h-28.8v17.1h24.9V65.5z"/> + <path class="st7" d="M1574.8,40.4h-18.6v52.2h-9.9V40.4h-18.4v-8.2h46.9V40.4z"/> + <path class="st7" d="M1615.2,78.6h-18.9l-4.2,14h-10.3l19.6-60.5h8.8l19.3,60.5h-10.3L1615.2,78.6z M1598.9,70h13.9l-6.9-23.7 + L1598.9,70z"/> +</g> +<path class="st11" d="M1644.3,8c10.7,0,19.5,8.7,19.5,19.5v69.8c0,10.7-8.7,19.5-19.5,19.5H1395c-10.7,0-19.5-8.7-19.5-19.5V27.5 + c0-10.7,8.7-19.5,19.5-19.5H1644.3 M1644.3,0H1395c-15.2,0-27.5,12.3-27.5,27.5v69.8c0,15.2,12.3,27.5,27.5,27.5h249.2 + c15.2,0,27.5-12.3,27.5-27.5V27.5C1671.7,12.3,1659.4,0,1644.3,0L1644.3,0z"/> +</svg> diff --git a/app/images/trezor-logo.svg b/app/images/trezor-logo.svg new file mode 100644 index 000000000..b8d85e3af --- /dev/null +++ b/app/images/trezor-logo.svg @@ -0,0 +1 @@ +<svg id="trezor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2567.5 722.3" width="2567.5" height="722.3"><style>.st0{display:none;fill:#fff;stroke:#000}</style><path id="rect25" class="st0" d="M1186 2932.6h46.2v147H1186v-147z"/><path id="path7" d="M249 0C149.9 0 69.7 80.2 69.7 179.3v67.2C34.9 252.8 0 261.2 0 272.1v350.7s0 9.7 10.9 14.3c39.5 16 194.9 71 230.6 83.6 4.6 1.7 5.9 1.7 7.1 1.7 1.7 0 2.5 0 7.1-1.7 35.7-12.6 191.5-67.6 231-83.6 10.1-4.2 10.5-13.9 10.5-13.9V272.1c0-10.9-34.4-19.7-69.3-25.6v-67.2C428.4 80.2 347.7 0 249 0zm0 85.7c58.4 0 93.7 35.3 93.7 93.7v58.4c-65.5-4.6-121.4-4.6-187.3 0v-58.4c0-58.5 35.3-93.7 93.6-93.7zm-.4 238.1c81.5 0 149.9 6.3 149.9 17.6v218.8c0 3.4-.4 3.8-3.4 5-2.9 1.3-139 50.4-139 50.4s-5.5 1.7-7.1 1.7c-1.7 0-7.1-2.1-7.1-2.1s-136.1-49.1-139-50.4-3.4-1.7-3.4-5V341c-.8-11.3 67.6-17.2 149.1-17.2z"/><g id="g3222" transform="translate(91.363 -287.434) scale(.95575)"><path id="path13" d="M666.6 890V639.3H575v-89.9h285.6v89.9h-90.7V890H666.6z"/><path id="path15" d="M1092 890l-47-107.1h-37.4V890H904.3V549.4h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5H1092zm12.2-223.9c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.2-.4 33.6-8.4 33.6-27.3z"/><path id="path17" d="M1262.9 890V549.4h258.3v89.9h-155.4v33.6h151.6v89.9h-151.6v37.4h155.4V890h-258.3z"/><path id="path19" d="M1574.9 890.4v-81.9l129.8-168.8h-129.8v-89.9h265.8v81.1l-130.2 169.7h134v89.9l-269.6-.1z"/><path id="path21" d="M1869.7 720.3c0-104.6 81.1-176.4 186.5-176.4 105 0 186.5 71.4 186.5 176.4 0 104.6-81.1 176-186.5 176s-186.5-71.4-186.5-176zm268 0c0-47.5-32.3-85.3-81.9-85.3-49.6 0-81.9 37.8-81.9 85.3s32.3 85.3 81.9 85.3c50 0 81.9-37.8 81.9-85.3z"/><path id="path23" d="M2473.6 890.4l-47-107.1h-37.4v107.1h-103.3V549.8h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5h-117.1zm12.6-224.3c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.3-.4 33.6-8.4 33.6-27.3z"/></g></svg>
\ No newline at end of file diff --git a/app/manifest.json b/app/manifest.json index 84cedd687..31b9ac9dd 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.9.1", + "version": "4.10.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -63,7 +63,6 @@ "activeTab", "webRequest", "*://*.eth/", - "*://*.test/", "notifications" ], "web_accessible_resources": [ diff --git a/app/phishing.html b/app/phishing.html index ab96063b5..eabb363b3 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -43,7 +43,7 @@ ga('create', 'UA-68598031-1', 'auto' {'allowLinker':true}); ga('send', 'pageview'); ga('require', 'linker'); - ga('linker:autoLink', ['harrydenley.com', 'metamask.io'], false, true); + ga('linker:autoLink', ['metamask.io'], false, true); </script> <script src="phishing-detect.js"></script> </head> diff --git a/app/scripts/background.js b/app/scripts/background.js index 3d3afdd4e..546fef569 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -15,11 +15,11 @@ const asStream = require('obs-store/lib/asStream') const ExtensionPlatform = require('./platforms/extension') const Migrator = require('./lib/migrator/') const migrations = require('./migrations/') -const PortStream = require('./lib/port-stream.js') +const PortStream = require('extension-port-stream') const createStreamSink = require('./lib/createStreamSink') const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') -const firstTimeState = require('./first-time-state') +const rawFirstTimeState = require('./first-time-state') const setupRaven = require('./lib/setupRaven') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') @@ -34,6 +34,9 @@ const { ENVIRONMENT_TYPE_FULLSCREEN, } = require('./lib/enums') +// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost +const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG) + const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = process.env.METAMASK_DEBUG @@ -253,6 +256,7 @@ function setupController (initState, initLangCode) { showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, + showWatchAssetUi: showWatchAssetUi, // initial state initState, // initial locale code @@ -440,9 +444,28 @@ function triggerUi () { }) } +/** + * Opens the browser popup for user confirmation of watchAsset + * then it waits until user interact with the UI + */ +function showWatchAssetUi () { + triggerUi() + return new Promise( + (resolve) => { + var interval = setInterval(() => { + if (!notificationIsOpen) { + clearInterval(interval) + resolve() + } + }, 1000) + } + ) +} + // On first install, open a window to MetaMask website to how-it-works. extension.runtime.onInstalled.addListener(function (details) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) { extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) } }) + diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index b6d5cfe9e..2cbfb811e 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -5,7 +5,7 @@ const LocalMessageDuplexStream = require('post-message-stream') const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') -const PortStream = require('./lib/port-stream.js') +const PortStream = require('extension-port-stream') const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 4c97810a3..465751e61 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -80,7 +80,7 @@ class BalanceController { } }) this.accountTracker.store.subscribe(update) - this.blockTracker.on('block', update) + this.blockTracker.on('latest', update) } /** diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index a93aff49b..d5bc5fe2b 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -1,4 +1,4 @@ - const ObservableStore = require('obs-store') +const ObservableStore = require('obs-store') const extend = require('xtend') const log = require('loglevel') diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js new file mode 100644 index 000000000..41af4d9f9 --- /dev/null +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -0,0 +1,25 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createBlockReEmitMiddleware = require('eth-json-rpc-middleware/block-reemit') +const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') +const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const createInfuraMiddleware = require('eth-json-rpc-infura') +const BlockTracker = require('eth-block-tracker') + +module.exports = createInfuraClient + +function createInfuraClient ({ network }) { + const infuraMiddleware = createInfuraMiddleware({ network }) + const blockProvider = providerFromMiddleware(infuraMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockReEmitMiddleware({ blockTracker, provider: blockProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + infuraMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js new file mode 100644 index 000000000..40c353f7f --- /dev/null +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -0,0 +1,25 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') +const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const BlockTracker = require('eth-block-tracker') + +module.exports = createJsonRpcClient + +function createJsonRpcClient ({ rpcUrl }) { + const fetchMiddleware = createFetchMiddleware({ rpcUrl }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js new file mode 100644 index 000000000..fecc512e8 --- /dev/null +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -0,0 +1,21 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const BlockTracker = require('eth-block-tracker') + +module.exports = createLocalhostClient + +function createLocalhostClient () { + const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js new file mode 100644 index 000000000..9e6a45888 --- /dev/null +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -0,0 +1,43 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') + +module.exports = createMetamaskMiddleware + +function createMetamaskMiddleware ({ + version, + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + getPendingNonce, +}) { + const metamaskMiddleware = mergeMiddleware([ + createScaffoldMiddleware({ + // staticSubprovider + eth_syncing: false, + web3_clientVersion: `MetaMask/v${version}`, + }), + createWalletSubprovider({ + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + }), + createPendingNonceMiddleware({ getPendingNonce }), + ]) + return metamaskMiddleware +} + +function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + if (req.method !== 'eth_getTransactionCount') return next() + const address = req.params[0] + const blockRef = req.params[1] + if (blockRef !== 'pending') return next() + res.result = await getPendingNonce(address) + }) +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index b6f7705b5..c1667d9a6 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,15 +1,17 @@ const assert = require('assert') const EventEmitter = require('events') -const createMetamaskProvider = require('web3-provider-engine/zero.js') -const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js') -const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') -const extend = require('xtend') const EthQuery = require('eth-query') -const createEventEmitterProxy = require('../../lib/events-proxy.js') +const JsonRpcEngine = require('json-rpc-engine') +const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') const log = require('loglevel') -const urlUtil = require('url') +const createMetamaskMiddleware = require('./createMetamaskMiddleware') +const createInfuraClient = require('./createInfuraClient') +const createJsonRpcClient = require('./createJsonRpcClient') +const createLocalhostClient = require('./createLocalhostClient') +const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') + const { ROPSTEN, RINKEBY, @@ -17,7 +19,6 @@ const { MAINNET, LOCALHOST, } = require('./enums') -const LOCALHOST_RPC_URL = 'http://localhost:8545' const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] const env = process.env.METAMASK_ENV @@ -39,21 +40,27 @@ module.exports = class NetworkController extends EventEmitter { this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - // create event emitter proxy - this._proxy = createEventEmitterProxy() - this.on('networkDidChange', this.lookupNetwork) + // provider and block tracker + this._provider = null + this._blockTracker = null + // provider and block tracker proxies - because the network changes + this._providerProxy = null + this._blockTrackerProxy = null } - initializeProvider (_providerParams) { - this._baseProviderParams = _providerParams + initializeProvider (providerParams) { + this._baseProviderParams = providerParams const { type, rpcTarget } = this.providerStore.getState() this._configureProvider({ type, rpcTarget }) - this._proxy.on('block', this._logBlock.bind(this)) - this._proxy.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this._proxy) this.lookupNetwork() - return this._proxy + } + + // return the proxies so the references will always be good + getProviderAndBlockTracker () { + const provider = this._providerProxy + const blockTracker = this._blockTrackerProxy + return { provider, blockTracker } } verifyNetwork () { @@ -75,10 +82,11 @@ module.exports = class NetworkController extends EventEmitter { lookupNetwork () { // Prevent firing when provider is not defined. - if (!this.ethQuery || !this.ethQuery.sendAsync) { - return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery') + if (!this._provider) { + return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } - this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + const ethQuery = new EthQuery(this._provider) + ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) this.setNetworkState(network) @@ -131,7 +139,7 @@ module.exports = class NetworkController extends EventEmitter { this._configureInfuraProvider(opts) // other type-based rpc endpoints } else if (type === LOCALHOST) { - this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL }) + this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { this._configureStandardProvider({ rpcUrl: rpcTarget }) @@ -141,49 +149,47 @@ module.exports = class NetworkController extends EventEmitter { } _configureInfuraProvider ({ type }) { - log.info('_configureInfuraProvider', type) - const infuraProvider = createInfuraProvider({ network: type }) - const infuraSubprovider = new SubproviderFromProvider(infuraProvider) - const providerParams = extend(this._baseProviderParams, { - engineParams: { - pollingInterval: 8000, - blockTrackerProvider: infuraProvider, - }, - dataSubprovider: infuraSubprovider, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) + log.info('NetworkController - configureInfuraProvider', type) + const networkClient = createInfuraClient({ network: type }) + this._setNetworkClient(networkClient) + } + + _configureLocalhostProvider () { + log.info('NetworkController - configureLocalhostProvider') + const networkClient = createLocalhostClient() + this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl }) { - // urlUtil handles malformed urls - rpcUrl = urlUtil.parse(rpcUrl).format() - const providerParams = extend(this._baseProviderParams, { - rpcUrl, - engineParams: { - pollingInterval: 8000, - }, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) - } - - _setProvider (provider) { - // collect old block tracker events - const oldProvider = this._provider - let blockTrackerHandlers - if (oldProvider) { - // capture old block handlers - blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers - // tear down - oldProvider.removeAllListeners() - oldProvider.stop() + log.info('NetworkController - configureStandardProvider', rpcUrl) + const networkClient = createJsonRpcClient({ rpcUrl }) + this._setNetworkClient(networkClient) + } + + _setNetworkClient ({ networkMiddleware, blockTracker }) { + const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams) + const engine = new JsonRpcEngine() + engine.push(metamaskMiddleware) + engine.push(networkMiddleware) + const provider = providerFromEngine(engine) + this._setProviderAndBlockTracker({ provider, blockTracker }) + } + + _setProviderAndBlockTracker ({ provider, blockTracker }) { + // update or intialize proxies + if (this._providerProxy) { + this._providerProxy.setTarget(provider) + } else { + this._providerProxy = createSwappableProxy(provider) + } + if (this._blockTrackerProxy) { + this._blockTrackerProxy.setTarget(blockTracker) + } else { + this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { eventFilter: 'skipInternal' }) } - // override block tracler - provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) - // set as new provider + // set new provider and blockTracker this._provider = provider - this._proxy.setTarget(provider) + this._blockTracker = blockTracker } _logBlock (block) { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 707fd7de9..928ebdf1f 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const normalizeAddress = require('eth-sig-util').normalize +const { isValidAddress } = require('ethereumjs-util') const extend = require('xtend') @@ -14,6 +15,7 @@ class PreferencesController { * @property {string} store.currentAccountTab Indicates the selected tab in the ui * @property {array} store.tokens The tokens the user wants display in their token lists * @property {object} store.accountTokens The tokens stored per account and then per network type + * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature @@ -26,22 +28,43 @@ class PreferencesController { frequentRpcList: [], currentAccountTab: 'history', accountTokens: {}, + assetImages: {}, tokens: [], + suggestedTokens: {}, useBlockie: false, featureFlags: {}, currentLocale: opts.initLangCode, identities: {}, lostIdentities: {}, + seedWords: null, + forgottenPassword: false, }, opts.initState) this.diagnostics = opts.diagnostics this.network = opts.network this.store = new ObservableStore(initState) + this.showWatchAssetUi = opts.showWatchAssetUi this._subscribeProviderType() } // PUBLIC METHODS /** + * Sets the {@code forgottenPassword} state property + * @param {boolean} forgottenPassword whether or not the user has forgotten their password + */ + setPasswordForgotten (forgottenPassword) { + this.store.updateState({ forgottenPassword }) + } + + /** + * Sets the {@code seedWords} seed words + * @param {string|null} seedWords the seed words + */ + setSeedWords (seedWords) { + this.store.updateState({ seedWords }) + } + + /** * Setter for the `useBlockie` property * * @param {boolean} val Whether or not the user prefers blockie indicators @@ -51,6 +74,53 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + getSuggestedTokens () { + return this.store.getState().suggestedTokens + } + + getAssetImages () { + return this.store.getState().assetImages + } + + addSuggestedERC20Asset (tokenOpts) { + this._validateERC20AssetParams(tokenOpts) + const suggested = this.getSuggestedTokens() + const { rawAddress, symbol, decimals, image } = tokenOpts + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals, image } + suggested[address] = newEntry + this.store.updateState({ suggestedTokens: suggested }) + } + + /** + * RPC engine middleware for requesting new asset added + * + * @param req + * @param res + * @param {Function} - next + * @param {Function} - end + */ + async requestWatchAsset (req, res, next, end) { + if (req.method === 'metamask_watchAsset') { + const { type, options } = req.params + switch (type) { + case 'ERC20': + const result = await this._handleWatchAssetERC20(options) + if (result instanceof Error) { + end(result) + } else { + res.result = result + end() + } + break + default: + end(new Error(`Asset of type ${type} not supported`)) + } + } else { + next() + } + } + /** * Getter for the `useBlockie` property * @@ -186,6 +256,13 @@ class PreferencesController { return selected } + removeSuggestedTokens () { + return new Promise((resolve, reject) => { + this.store.updateState({ suggestedTokens: {} }) + resolve({}) + }) + } + /** * Setter for the `selectedAddress` property * @@ -232,11 +309,11 @@ class PreferencesController { * @returns {Promise<array>} Promises the new array of AddedToken objects. * */ - async addToken (rawAddress, symbol, decimals) { + async addToken (rawAddress, symbol, decimals, image) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } - const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const previousEntry = tokens.find((token, index) => { return token.address === address }) @@ -247,7 +324,8 @@ class PreferencesController { } else { tokens.push(newEntry) } - this._updateAccountTokens(tokens) + assetImages[address] = image + this._updateAccountTokens(tokens, assetImages) return Promise.resolve(tokens) } @@ -260,8 +338,10 @@ class PreferencesController { */ removeToken (rawAddress) { const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const updatedTokens = tokens.filter(token => token.address !== rawAddress) - this._updateAccountTokens(updatedTokens) + delete assetImages[rawAddress] + this._updateAccountTokens(updatedTokens, assetImages) return Promise.resolve(updatedTokens) } @@ -322,7 +402,7 @@ class PreferencesController { /** * Returns an updated rpcList based on the passed url and the current list. - * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the + * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the * end of the list. The current list is modified and returned as a promise. * * @param {string} _url The rpc url to add to the frequentRpcList. @@ -338,7 +418,7 @@ class PreferencesController { if (_url !== 'http://localhost:8545') { rpcList.push(_url) } - if (rpcList.length > 2) { + if (rpcList.length > 3) { rpcList.shift() } return Promise.resolve(rpcList) @@ -387,6 +467,7 @@ class PreferencesController { // // PRIVATE METHODS // + /** * Subscription to network provider type. * @@ -405,10 +486,10 @@ class PreferencesController { * @param {array} tokens Array of tokens to be updated. * */ - _updateAccountTokens (tokens) { + _updateAccountTokens (tokens, assetImages) { const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() accountTokens[selectedAddress][providerType] = tokens - this.store.updateState({ accountTokens, tokens }) + this.store.updateState({ accountTokens, tokens, assetImages }) } /** @@ -438,6 +519,47 @@ class PreferencesController { const tokens = accountTokens[selectedAddress][providerType] return { tokens, accountTokens, providerType, selectedAddress } } + + /** + * Handle the suggestion of an ERC20 asset through `watchAsset` + * * + * @param {Promise} promise Promise according to addition of ERC20 token + * + */ + async _handleWatchAssetERC20 (options) { + const { address, symbol, decimals, image } = options + const rawAddress = address + try { + this._validateERC20AssetParams({ rawAddress, symbol, decimals }) + } catch (err) { + return err + } + const tokenOpts = { rawAddress, decimals, symbol, image } + this.addSuggestedERC20Asset(tokenOpts) + return this.showWatchAssetUi().then(() => { + const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress)) + return tokenAddresses.length > 0 + }) + } + + /** + * Validates that the passed options for suggested token have all required properties. + * + * @param {Object} opts The options object to validate + * @throws {string} Throw a custom error indicating that address, symbol and/or decimals + * doesn't fulfill requirements + * + */ + _validateERC20AssetParams (opts) { + const { rawAddress, symbol, decimals } = opts + if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`) + if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`) + const numDecimals = parseInt(decimals, 10) + if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { + throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`) + } + if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`) + } } module.exports = PreferencesController diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 926268691..d270f6f44 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -1,14 +1,14 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const BN = require('ethereumjs-util').BN const EthQuery = require('eth-query') const log = require('loglevel') +const pify = require('pify') class RecentBlocksController { /** * Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled - * upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event + * upon the controller's construction and then the list is updated when the given block tracker gets a 'latest' event * (indicating that there is a new block to process). * * @typedef {Object} RecentBlocksController @@ -16,7 +16,7 @@ class RecentBlocksController { * @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain * @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance. * @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction, - * listens for 'block' events so that new blocks can be processed and added to storage. + * listens for 'latest' events so that new blocks can be processed and added to storage. * @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider * @property {number} historyLength The maximum length of blocks to track * @property {object} store Stores the recentBlocks @@ -34,7 +34,13 @@ class RecentBlocksController { }, opts.initState) this.store = new ObservableStore(initState) - this.blockTracker.on('block', this.processBlock.bind(this)) + this.blockTracker.on('latest', async (newBlockNumberHex) => { + try { + await this.processBlock(newBlockNumberHex) + } catch (err) { + log.error(err) + } + }) this.backfill() } @@ -55,7 +61,11 @@ class RecentBlocksController { * @param {object} newBlock The new block to modify and add to the recentBlocks array * */ - processBlock (newBlock) { + async processBlock (newBlockNumberHex) { + const newBlockNumber = Number.parseInt(newBlockNumberHex, 16) + const newBlock = await this.getBlockByNumber(newBlockNumber, true) + if (!newBlock) return + const block = this.mapTransactionsToPrices(newBlock) const state = this.store.getState() @@ -108,9 +118,9 @@ class RecentBlocksController { } /** - * On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks + * On this.blockTracker's first 'latest' event after this RecentBlocksController's instantiation, the store.recentBlocks * array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first - * 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying + * 'latest' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying * the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest. * * Each iteration over the block numbers is delayed by 100 milliseconds. @@ -118,18 +128,17 @@ class RecentBlocksController { * @returns {Promise<void>} Promises undefined */ async backfill () { - this.blockTracker.once('block', async (block) => { - const currentBlockNumber = Number.parseInt(block.number, 16) + this.blockTracker.once('latest', async (blockNumberHex) => { + const currentBlockNumber = Number.parseInt(blockNumberHex, 16) const blocksToFetch = Math.min(currentBlockNumber, this.historyLength) const prevBlockNumber = currentBlockNumber - 1 const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index) await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => { try { - const newBlock = await this.getBlockByNumber(targetBlockNumber) + const newBlock = await this.getBlockByNumber(targetBlockNumber, true) + if (!newBlock) return - if (newBlock) { - this.backfillBlock(newBlock) - } + this.backfillBlock(newBlock) } catch (e) { log.error(e) } @@ -138,18 +147,6 @@ class RecentBlocksController { } /** - * A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await - * - * @returns {Promise<void>} Promises undefined - * - */ - async wait () { - return new Promise((resolve) => { - setTimeout(resolve, 100) - }) - } - - /** * Uses EthQuery to get a block that has a given block number. * * @param {number} number The number of the block to get @@ -157,13 +154,8 @@ class RecentBlocksController { * */ async getBlockByNumber (number) { - const bn = new BN(number) - return new Promise((resolve, reject) => { - this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => { - if (err) reject(err) - resolve(block) - }) - }) + const blockNumberHex = '0x' + number.toString(16) + return await pify(this.ethQuery.getBlockByNumber).call(this.ethQuery, blockNumberHex, true) } } diff --git a/app/scripts/controllers/transactions/enums.js b/app/scripts/controllers/transactions/enums.js new file mode 100644 index 000000000..be6f16e0d --- /dev/null +++ b/app/scripts/controllers/transactions/enums.js @@ -0,0 +1,12 @@ +const TRANSACTION_TYPE_CANCEL = 'cancel' +const TRANSACTION_TYPE_RETRY = 'retry' +const TRANSACTION_TYPE_STANDARD = 'standard' + +const TRANSACTION_STATUS_APPROVED = 'approved' + +module.exports = { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, + TRANSACTION_TYPE_STANDARD, + TRANSACTION_STATUS_APPROVED, +} diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 8e2288aed..e2965ceb6 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -11,6 +11,14 @@ const txUtils = require('./lib/util') const cleanErrorStack = require('../../lib/cleanErrorStack') const log = require('loglevel') const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker') +const { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, + TRANSACTION_TYPE_STANDARD, + TRANSACTION_STATUS_APPROVED, +} = require('./enums') + +const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util') /** Transaction Controller is an aggregate of sub-controllers and trackers @@ -65,6 +73,7 @@ class TransactionController extends EventEmitter { this.store = this.txStateManager.store this.nonceTracker = new NonceTracker({ provider: this.provider, + blockTracker: this.blockTracker, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) @@ -78,13 +87,17 @@ class TransactionController extends EventEmitter { }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) - this._setupListners() + this._setupListeners() // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) + + // request state update to finalize initialization + this._updatePendingTxsAfterFirstBlock() } + /** @returns {number} the chainId*/ getChainId () { const networkState = this.networkStore.getState() @@ -155,7 +168,10 @@ class TransactionController extends EventEmitter { const normalizedTxParams = txUtils.normalizeTxParams(txParams) txUtils.validateTxParams(normalizedTxParams) // construct txMeta - let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) + let txMeta = this.txStateManager.generateTxMeta({ + txParams: normalizedTxParams, + type: TRANSACTION_TYPE_STANDARD, + }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -209,6 +225,7 @@ class TransactionController extends EventEmitter { txParams: originalTxMeta.txParams, lastGasPrice, loadingDefaults: false, + type: TRANSACTION_TYPE_RETRY, }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -216,6 +233,40 @@ class TransactionController extends EventEmitter { } /** + * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The + * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to + * the sender's address, and has a higher gasPrice than that of the previous transaction. + * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel + * @param {string=} customGasPrice - the hex value to use for the cancel transaction + * @returns {txMeta} + */ + async createCancelTransaction (originalTxId, customGasPrice) { + const originalTxMeta = this.txStateManager.getTx(originalTxId) + const { txParams } = originalTxMeta + const { gasPrice: lastGasPrice, from, nonce } = txParams + + const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10)) + const newTxMeta = this.txStateManager.generateTxMeta({ + txParams: { + from, + to: from, + nonce, + gas: '0x5208', + value: '0x0', + gasPrice: newGasPrice, + }, + lastGasPrice, + loadingDefaults: false, + status: TRANSACTION_STATUS_APPROVED, + type: TRANSACTION_TYPE_CANCEL, + }) + + this.addTx(newTxMeta) + await this.approveTransaction(newTxMeta.id) + return newTxMeta + } + + /** updates the txMeta in the txStateManager @param txMeta {Object} - the updated txMeta */ @@ -311,6 +362,11 @@ class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } + confirmTransaction (txId) { + this.txStateManager.setTxStatusConfirmed(txId) + this._markNonceDuplicatesDropped(txId) + } + /** Convenience method for the ui thats sets the transaction to rejected @param txId {number} - the tx's Id @@ -354,6 +410,14 @@ class TransactionController extends EventEmitter { this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) } + // called once on startup + async _updatePendingTxsAfterFirstBlock () { + // wait for first block so we know we're ready + await this.blockTracker.getLatestBlock() + // get status update for all pending transactions (for the current network) + await this.pendingTxTracker.updatePendingTxs() + } + /** If transaction controller was rebooted with transactions that are uncompleted in steps of the transaction signing or user confirmation process it will either @@ -375,7 +439,7 @@ class TransactionController extends EventEmitter { }) this.txStateManager.getFilteredTxList({ - status: 'approved', + status: TRANSACTION_STATUS_APPROVED, }).forEach((txMeta) => { const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) @@ -386,14 +450,14 @@ class TransactionController extends EventEmitter { is called in constructor applies the listeners for pendingTxTracker txStateManager and blockTracker */ - _setupListners () { + _setupListeners () { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + this._setupBlockTrackerListener() this.pendingTxTracker.on('tx:warning', (txMeta) => { this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') }) - this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId)) - this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId)) this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { if (!txMeta.firstRetryBlockNumber) { txMeta.firstRetryBlockNumber = latestBlockNumber @@ -405,13 +469,6 @@ class TransactionController extends EventEmitter { txMeta.retryCount++ this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') }) - - this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) - // this is a little messy but until ethstore has been either - // removed or redone this is to guard against the race condition - this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) - this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) - } /** @@ -435,6 +492,40 @@ class TransactionController extends EventEmitter { }) } + _setupBlockTrackerListener () { + let listenersAreActive = false + const latestBlockHandler = this._onLatestBlock.bind(this) + const blockTracker = this.blockTracker + const txStateManager = this.txStateManager + + txStateManager.on('tx:status-update', updateSubscription) + updateSubscription() + + function updateSubscription () { + const pendingTxs = txStateManager.getPendingTransactions() + if (!listenersAreActive && pendingTxs.length > 0) { + blockTracker.on('latest', latestBlockHandler) + listenersAreActive = true + } else if (listenersAreActive && !pendingTxs.length) { + blockTracker.removeListener('latest', latestBlockHandler) + listenersAreActive = false + } + } + } + + async _onLatestBlock (blockNumber) { + try { + await this.pendingTxTracker.updatePendingTxs() + } catch (err) { + log.error(err) + } + try { + await this.pendingTxTracker.resubmitPendingTxs(blockNumber) + } catch (err) { + log.error(err) + } + } + /** Updates the memStore in transaction controller */ diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index 06f336eaa..421036368 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex */ class NonceTracker { - constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { + constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { this.provider = provider + this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.getConfirmedTransactions = getConfirmedTransactions @@ -34,7 +35,7 @@ class NonceTracker { * @typedef NonceDetails * @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction. * @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method. - * @property {number} highetSuggested - The maximum between the other two, the number returned. + * @property {number} highestSuggested - The maximum between the other two, the number returned. */ /** @@ -80,15 +81,6 @@ class NonceTracker { } } - async _getCurrentBlock () { - const blockTracker = this._getBlockTracker() - const currentBlock = blockTracker.getCurrentBlock() - if (currentBlock) return currentBlock - return await new Promise((reject, resolve) => { - blockTracker.once('latest', resolve) - }) - } - async _globalMutexFree () { const globalMutex = this._lookupMutex('global') const releaseLock = await globalMutex.acquire() @@ -114,9 +106,8 @@ class NonceTracker { // calculate next nonce // we need to make sure our base count // and pending count are from the same block - const currentBlock = await this._getCurrentBlock() - const blockNumber = currentBlock.blockNumber - const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest') + const blockNumber = await this.blockTracker.getLatestBlock() + const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber) const baseCount = baseCountBN.toNumber() assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nonceDetails = { blockNumber, baseCount } @@ -165,15 +156,6 @@ class NonceTracker { return { name: 'local', nonce: highest, details: { startPoint, highest } } } - // this is a hotfix for the fact that the blockTracker will - // change when the network changes - - /** - @returns {Object} the current blockTracker - */ - _getBlockTracker () { - return this.provider._blockTracker - } } module.exports = NonceTracker diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 4e41cdaf8..70cac096b 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,6 +1,7 @@ const EventEmitter = require('events') const log = require('loglevel') const EthQuery = require('ethjs-query') + /** Event emitter utility class for tracking the transactions as they<br> @@ -23,55 +24,26 @@ class PendingTransactionTracker extends EventEmitter { super() this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker - // default is one day this.getPendingTransactions = config.getPendingTransactions this.getCompletedTransactions = config.getCompletedTransactions this.publishTransaction = config.publishTransaction - this._checkPendingTxs() + this.confirmTransaction = config.confirmTransaction } /** - checks if a signed tx is in a block and - if it is included emits tx status as 'confirmed' - @param block {object}, a full block - @emits tx:confirmed - @emits tx:failed - */ - checkForTxInBlock (block) { - const signedTxList = this.getPendingTransactions() - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - const txHash = txMeta.hash - const txId = txMeta.id - - if (!txHash) { - const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') - noTxHashErr.name = 'NoTxHashError' - this.emit('tx:failed', txId, noTxHashErr) - return - } - - - block.transactions.forEach((tx) => { - if (tx.hash === txHash) this.emit('tx:confirmed', txId) - }) - }) - } - - /** - asks the network for the transaction to see if a block number is included on it - if we have skipped/missed blocks - @param object - oldBlock newBlock + checks the network for signed txs and releases the nonce global lock if it is */ - queryPendingTxs ({ oldBlock, newBlock }) { - // check pending transactions on start - if (!oldBlock) { - this._checkPendingTxs() - return + async updatePendingTxs () { + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() + try { + const pendingTxs = this.getPendingTransactions() + await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + log.error('PendingTransactionTracker - Error updating pending transactions') + log.error(err) } - // if we synced by more than one block, check for missed pending transactions - const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16) - if (diff > 1) this._checkPendingTxs() + nonceGlobalLock.releaseLock() } /** @@ -79,11 +51,11 @@ class PendingTransactionTracker extends EventEmitter { @param block {object} - a block object @emits tx:warning */ - resubmitPendingTxs (block) { + resubmitPendingTxs (blockNumber) { const pending = this.getPendingTransactions() // only try resubmitting if their are transactions to resubmit if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => { + pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => { /* Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce @@ -145,6 +117,7 @@ class PendingTransactionTracker extends EventEmitter { this.emit('tx:retry', txMeta) return txHash } + /** Ask the network for the transaction to see if it has been include in a block @param txMeta {Object} - the txMeta object @@ -174,9 +147,8 @@ class PendingTransactionTracker extends EventEmitter { } // get latest transaction status - let txParams try { - txParams = await this.query.getTransactionByHash(txHash) + const txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { this.emit('tx:confirmed', txId) @@ -191,26 +163,12 @@ class PendingTransactionTracker extends EventEmitter { } /** - checks the network for signed txs and releases the nonce global lock if it is - */ - async _checkPendingTxs () { - const signedTxList = this.getPendingTransactions() - // in order to keep the nonceTracker accurate we block it while updating pending transactions - const { releaseLock } = await this.nonceTracker.getGlobalLock() - try { - await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) - } catch (err) { - log.error('PendingTransactionWatcher - Error updating pending transactions') - log.error(err) - } - releaseLock() - } - - /** checks to see if a confirmed txMeta has the same nonce @param txMeta {Object} - txMeta object @returns {boolean} */ + + async _checkIfNonceIsTaken (txMeta) { const address = txMeta.txParams.from const completed = this.getCompletedTransactions(address) diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 5cd0f5407..3dd45507f 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -25,7 +25,7 @@ class TxGasUtil { @returns {object} the txMeta object with the gas written to the txParams */ async analyzeGasUsage (txMeta) { - const block = await this.query.getBlockByNumber('latest', true) + const block = await this.query.getBlockByNumber('latest', false) let estimatedGasHex try { estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 28a18ca2e..daa6cc388 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -353,6 +353,7 @@ class TransactionStateManager extends EventEmitter { const txMeta = this.getTx(txId) txMeta.err = { message: err.toString(), + rpc: err.value, stack: err.stack, } this.updateTx(txMeta) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 7dd7fda02..d924be516 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -4,11 +4,16 @@ require('web3/dist/web3.min.js') const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') const setupDappAutoReload = require('./lib/auto-reload.js') -const MetamaskInpageProvider = require('./lib/inpage-provider.js') +const MetamaskInpageProvider = require('metamask-inpage-provider') restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') +console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' + +'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' + +'Dapps should call provider.enable() in order to view and use accounts. Please see ' + +'https://bit.ly/2QQHXvF for complete information and up-to-date example code.') + // // setup plugin communication // @@ -22,6 +27,25 @@ var metamaskStream = new LocalMessageDuplexStream({ // compose the inpage provider var inpageProvider = new MetamaskInpageProvider(metamaskStream) +// Augment the provider with its enable method +inpageProvider.enable = function (options = {}) { + return new Promise((resolve, reject) => { + if (options.mockRejection) { + reject('User rejected account access') + } else { + inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { + if (error) { + reject(error) + } else { + resolve(response.result) + } + }) + } + }) +} + +window.ethereum = inpageProvider + // // setup web3 // @@ -33,6 +57,7 @@ if (typeof window.web3 !== 'undefined') { or MetaMask and another web3 extension. Please remove one and try again.`) } + var web3 = new Web3(inpageProvider) web3.setProvider = function () { log.debug('MetaMask - overrode web3.setProvider') diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 0f7b3d865..2e9340018 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -7,14 +7,13 @@ * on each new block. */ -const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') -const EventEmitter = require('events').EventEmitter -function noop () {} +const log = require('loglevel') +const pify = require('pify') -class AccountTracker extends EventEmitter { +class AccountTracker { /** * This module is responsible for tracking any number of accounts and caching their current balances & transaction @@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter { * */ constructor (opts = {}) { - super() - const initState = { accounts: {}, currentBlockGasLimit: '', @@ -44,12 +41,29 @@ class AccountTracker extends EventEmitter { this.store = new ObservableStore(initState) this._provider = opts.provider - this._query = new EthQuery(this._provider) + this._query = pify(new EthQuery(this._provider)) this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock + this._currentBlockNumber = this._blockTracker.getCurrentBlock() + this._blockTracker.once('latest', blockNumber => { + this._currentBlockNumber = blockNumber + }) + // bind function for easier listener syntax + this._updateForBlock = this._updateForBlock.bind(this) + } + + start () { + // remove first to avoid double add + this._blockTracker.removeListener('latest', this._updateForBlock) + // add listener + this._blockTracker.addListener('latest', this._updateForBlock) + // fetch account balances + this._updateAccounts() + } + + stop () { + // remove listener + this._blockTracker.removeListener('latest', this._updateForBlock) } /** @@ -67,49 +81,57 @@ class AccountTracker extends EventEmitter { const accounts = this.store.getState().accounts const locals = Object.keys(accounts) - const toAdd = [] + const accountsToAdd = [] addresses.forEach((upstream) => { if (!locals.includes(upstream)) { - toAdd.push(upstream) + accountsToAdd.push(upstream) } }) - const toRemove = [] + const accountsToRemove = [] locals.forEach((local) => { if (!addresses.includes(local)) { - toRemove.push(local) + accountsToRemove.push(local) } }) - toAdd.forEach(upstream => this.addAccount(upstream)) - toRemove.forEach(local => this.removeAccount(local)) - this._updateAccounts() + this.addAccounts(accountsToAdd) + this.removeAccount(accountsToRemove) } /** - * Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be + * Adds new addresses to track the balances of * given a balance as long this._currentBlockNumber is defined. * - * @param {string} address A hex address of a new account to store in this AccountTracker's accounts object + * @param {array} addresses An array of hex addresses of new accounts to track * */ - addAccount (address) { + addAccounts (addresses) { const accounts = this.store.getState().accounts - accounts[address] = {} + // add initial state for addresses + addresses.forEach(address => { + accounts[address] = {} + }) + // save accounts state this.store.updateState({ accounts }) + // fetch balances for the accounts if there is block number ready if (!this._currentBlockNumber) return - this._updateAccount(address) + addresses.forEach(address => this._updateAccount(address)) } /** - * Removes an account from this AccountTracker's accounts object + * Removes accounts from being tracked * - * @param {string} address A hex address of a the account to remove + * @param {array} an array of hex addresses to stop tracking * */ - removeAccount (address) { + removeAccount (addresses) { const accounts = this.store.getState().accounts - delete accounts[address] + // remove each state object + addresses.forEach(address => { + delete accounts[address] + }) + // save accounts state this.store.updateState({ accounts }) } @@ -118,71 +140,56 @@ class AccountTracker extends EventEmitter { * via EthQuery * * @private - * @param {object} block Data about the block that contains the data to update to. + * @param {number} blockNumber the block number to update to. * @fires 'block' The updated state, if all account updates are successful * */ - _updateForBlock (block) { - this._currentBlockNumber = block.number - const currentBlockGasLimit = block.gasLimit + async _updateForBlock (blockNumber) { + this._currentBlockNumber = blockNumber + // block gasLimit polling shouldn't be in account-tracker shouldn't be here... + const currentBlock = await this._query.getBlockByNumber(blockNumber, false) + if (!currentBlock) return + const currentBlockGasLimit = currentBlock.gasLimit this.store.updateState({ currentBlockGasLimit }) - async.parallel([ - this._updateAccounts.bind(this), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.store.getState()) - }) + try { + await this._updateAccounts() + } catch (err) { + log.error(err) + } } /** * Calls this._updateAccount for each account in this.store * - * @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated + * @returns {Promise} after all account balances updated * */ - _updateAccounts (cb = noop) { + async _updateAccounts () { const accounts = this.store.getState().accounts const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) + await Promise.all(addresses.map(this._updateAccount.bind(this))) } /** - * Updates the current balance of an account. Gets an updated balance via this._getAccount. + * Updates the current balance of an account. * * @private * @param {string} address A hex address of a the account to be updated - * @param {Function} cb A callback to call once the account at address is successfully update + * @returns {Promise} after the account balance is updated * */ - _updateAccount (address, cb = noop) { - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - const accounts = this.store.getState().accounts - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.store.updateState({ accounts }) - } - cb(null, result) - }) - } - - /** - * Gets the current balance of an account via EthQuery. - * - * @private - * @param {string} address A hex address of a the account to query - * @param {Function} cb A callback to call once the account at address is successfully update - * - */ - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - }, cb) + async _updateAccount (address) { + // query balance + const balance = await this._query.getBalance(address) + const result = { address, balance } + // update accounts state + const { accounts } = this.store.getState() + // only populate if the entry is still present + if (!accounts[address]) return + accounts[address] = result + this.store.updateState({ accounts }) } } diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index cce31c3d2..558391a06 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -2,18 +2,12 @@ module.exports = setupDappAutoReload function setupDappAutoReload (web3, observable) { // export web3 as a global, checking for usage - let hasBeenWarned = false let reloadInProgress = false let lastTimeUsed let lastSeenNetwork global.web3 = new Proxy(web3, { get: (_web3, key) => { - // show warning once on web3 access - if (!hasBeenWarned && key !== 'currentProvider') { - console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') - hasBeenWarned = true - } // get the time of use lastTimeUsed = Date.now() // return value normally diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js deleted file mode 100644 index 221746467..000000000 --- a/app/scripts/lib/config-manager.js +++ /dev/null @@ -1,254 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const normalize = require('eth-sig-util').normalize -const { - MAINNET_RPC_URL, - ROPSTEN_RPC_URL, - KOVAN_RPC_URL, - RINKEBY_RPC_URL, -} = require('../controllers/network/enums') - -/* The config-manager is a convenience object - * wrapping a pojo-migrator. - * - * It exists mostly to allow the creation of - * convenience methods to access and persist - * particular portions of the state. - */ -module.exports = ConfigManager -function ConfigManager (opts) { - // ConfigManager is observable and will emit updates - this._subs = [] - this.store = opts.store -} - -ConfigManager.prototype.setConfig = function (config) { - var data = this.getData() - data.config = config - this.setData(data) - this._emitUpdates(config) -} - -ConfigManager.prototype.getConfig = function () { - var data = this.getData() - return data.config -} - -ConfigManager.prototype.setData = function (data) { - this.store.putState(data) -} - -ConfigManager.prototype.getData = function () { - return this.store.getState() -} - -ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - data.forgottenPassword = passwordForgottenState - this.setData(data) -} - -ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - return data.forgottenPassword -} - -ConfigManager.prototype.setWallet = function (wallet) { - var data = this.getData() - data.wallet = wallet - this.setData(data) -} - -ConfigManager.prototype.setVault = function (encryptedString) { - var data = this.getData() - data.vault = encryptedString - this.setData(data) -} - -ConfigManager.prototype.getVault = function () { - var data = this.getData() - return data.vault -} - -ConfigManager.prototype.getKeychains = function () { - return this.getData().keychains || [] -} - -ConfigManager.prototype.setKeychains = function (keychains) { - var data = this.getData() - data.keychains = keychains - this.setData(data) -} - -ConfigManager.prototype.getSelectedAccount = function () { - var config = this.getConfig() - return config.selectedAccount -} - -ConfigManager.prototype.setSelectedAccount = function (address) { - var config = this.getConfig() - config.selectedAccount = ethUtil.addHexPrefix(address) - this.setConfig(config) -} - -ConfigManager.prototype.getWallet = function () { - return this.getData().wallet -} - -// Takes a boolean -ConfigManager.prototype.setShowSeedWords = function (should) { - var data = this.getData() - data.showSeedWords = should - this.setData(data) -} - - -ConfigManager.prototype.getShouldShowSeedWords = function () { - var data = this.getData() - return data.showSeedWords -} - -ConfigManager.prototype.setSeedWords = function (words) { - var data = this.getData() - data.seedWords = words - this.setData(data) -} - -ConfigManager.prototype.getSeedWords = function () { - var data = this.getData() - return data.seedWords -} -ConfigManager.prototype.setRpcTarget = function (rpcUrl) { - var config = this.getConfig() - config.provider = { - type: 'rpc', - rpcTarget: rpcUrl, - } - this.setConfig(config) -} - -ConfigManager.prototype.setProviderType = function (type) { - var config = this.getConfig() - config.provider = { - type: type, - } - this.setConfig(config) -} - -ConfigManager.prototype.useEtherscanProvider = function () { - var config = this.getConfig() - config.provider = { - type: 'etherscan', - } - this.setConfig(config) -} - -ConfigManager.prototype.getProvider = function () { - var config = this.getConfig() - return config.provider -} - -ConfigManager.prototype.getCurrentRpcAddress = function () { - var provider = this.getProvider() - if (!provider) return null - switch (provider.type) { - - case 'mainnet': - return MAINNET_RPC_URL - - case 'ropsten': - return ROPSTEN_RPC_URL - - case 'kovan': - return KOVAN_RPC_URL - - case 'rinkeby': - return RINKEBY_RPC_URL - - default: - return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL - } -} - -// -// Tx -// - -ConfigManager.prototype.getTxList = function () { - var data = this.getData() - if (data.transactions !== undefined) { - return data.transactions - } else { - return [] - } -} - -ConfigManager.prototype.setTxList = function (txList) { - var data = this.getData() - data.transactions = txList - this.setData(data) -} - - -// wallet nickname methods - -ConfigManager.prototype.getWalletNicknames = function () { - var data = this.getData() - const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {} - return nicknames -} - -ConfigManager.prototype.nicknameForWallet = function (account) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - return nicknames[address] -} - -ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - nicknames[address] = nickname - var data = this.getData() - data.walletNicknames = nicknames - this.setData(data) -} - -// observable - -ConfigManager.prototype.getSalt = function () { - var data = this.getData() - return data.salt -} - -ConfigManager.prototype.setSalt = function (salt) { - var data = this.getData() - data.salt = salt - this.setData(data) -} - -ConfigManager.prototype.subscribe = function (fn) { - this._subs.push(fn) - var unsubscribe = this.unsubscribe.bind(this, fn) - return unsubscribe -} - -ConfigManager.prototype.unsubscribe = function (fn) { - var index = this._subs.indexOf(fn) - if (index !== -1) this._subs.splice(index, 1) -} - -ConfigManager.prototype._emitUpdates = function (state) { - this._subs.forEach(function (handler) { - handler(state) - }) -} - -ConfigManager.prototype.setLostAccounts = function (lostAccounts) { - var data = this.getData() - data.lostAccounts = lostAccounts - this.setData(data) -} - -ConfigManager.prototype.getLostAccounts = function () { - var data = this.getData() - return data.lostAccounts || [] -} diff --git a/app/scripts/lib/createErrorMiddleware.js b/app/scripts/lib/createErrorMiddleware.js deleted file mode 100644 index 7f6a4bd73..000000000 --- a/app/scripts/lib/createErrorMiddleware.js +++ /dev/null @@ -1,67 +0,0 @@ -const log = require('loglevel') - -/** - * JSON-RPC error object - * - * @typedef {Object} RpcError - * @property {number} code - Indicates the error type that occurred - * @property {Object} [data] - Contains additional information about the error - * @property {string} [message] - Short description of the error - */ - -/** - * Middleware configuration object - * - * @typedef {Object} MiddlewareConfig - * @property {boolean} [override] - Use RPC_ERRORS message in place of provider message - */ - -/** - * Map of standard and non-standard RPC error codes to messages - */ -const RPC_ERRORS = { - 1: 'An unauthorized action was attempted.', - 2: 'A disallowed action was attempted.', - 3: 'An execution error occurred.', - [-32600]: 'The JSON sent is not a valid Request object.', - [-32601]: 'The method does not exist / is not available.', - [-32602]: 'Invalid method parameter(s).', - [-32603]: 'Internal JSON-RPC error.', - [-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', - internal: 'Internal server error.', - unknown: 'Unknown JSON-RPC error.', -} - -/** - * Modifies a JSON-RPC error object in-place to add a human-readable message, - * optionally overriding any provider-supplied message - * - * @param {RpcError} error - JSON-RPC error object - * @param {boolean} override - Use RPC_ERRORS message in place of provider message - */ -function sanitizeRPCError (error, override) { - if (error.message && !override) { return error } - const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code] - error.message = message || RPC_ERRORS.unknown -} - -/** - * json-rpc-engine middleware that both logs standard and non-standard error - * messages and ends middleware stack traversal if an error is encountered - * - * @param {MiddlewareConfig} [config={override:true}] - Middleware configuration - * @returns {Function} json-rpc-engine middleware function - */ -function createErrorMiddleware ({ override = true } = {}) { - return (req, res, next) => { - next(done => { - const { error } = res - if (!error) { return done() } - sanitizeRPCError(error) - log.error(`MetaMask - RPC Error: ${error.message}`, error) - done() - }) - } -} - -module.exports = createErrorMiddleware diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js deleted file mode 100644 index f83773ccc..000000000 --- a/app/scripts/lib/events-proxy.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Returns an EventEmitter that proxies events from the given event emitter - * @param {any} eventEmitter - * @param {object} listeners - The listeners to proxy to - * @returns {any} - */ -module.exports = function createEventEmitterProxy (eventEmitter, listeners) { - let target = eventEmitter - const eventHandlers = listeners || {} - const proxy = /** @type {any} */ (new Proxy({}, { - get: (_, name) => { - // intercept listeners - if (name === 'on') return addListener - if (name === 'setTarget') return setTarget - if (name === 'proxyEventHandlers') return eventHandlers - return (/** @type {any} */ (target))[name] - }, - set: (_, name, value) => { - target[name] = value - return true - }, - })) - function setTarget (/** @type {EventEmitter} */ eventEmitter) { - target = eventEmitter - // migrate listeners - Object.keys(eventHandlers).forEach((name) => { - /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler)) - }) - } - /** - * Attaches a function to be called whenever the specified event is emitted - * @param {string} name - * @param {Function} handler - */ - function addListener (name, handler) { - if (!eventHandlers[name]) eventHandlers[name] = [] - eventHandlers[name].push(handler) - target.on(name, handler) - } - if (listeners) proxy.setTarget(eventEmitter) - return proxy -} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js deleted file mode 100644 index 6ef511453..000000000 --- a/app/scripts/lib/inpage-provider.js +++ /dev/null @@ -1,125 +0,0 @@ -const pump = require('pump') -const RpcEngine = require('json-rpc-engine') -const createErrorMiddleware = require('./createErrorMiddleware') -const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') -const createStreamMiddleware = require('json-rpc-middleware-stream') -const LocalStorageStore = require('obs-store') -const asStream = require('obs-store/lib/asStream') -const ObjectMultiplex = require('obj-multiplex') - -module.exports = MetamaskInpageProvider - -function MetamaskInpageProvider (connectionStream) { - const self = this - - // setup connectionStream multiplexing - const mux = self.mux = new ObjectMultiplex() - pump( - connectionStream, - mux, - connectionStream, - (err) => logStreamDisconnectWarning('MetaMask', err) - ) - - // subscribe to metamask public config (one-way) - self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) - - pump( - mux.createStream('publicConfig'), - asStream(self.publicConfigStore), - (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) - ) - - // ignore phishing warning message (handled elsewhere) - mux.ignoreStream('phishing') - - // connect to async provider - const streamMiddleware = createStreamMiddleware() - pump( - streamMiddleware.stream, - mux.createStream('provider'), - streamMiddleware.stream, - (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) - ) - - // handle sendAsync requests via dapp-side rpc engine - const rpcEngine = new RpcEngine() - rpcEngine.push(createIdRemapMiddleware()) - rpcEngine.push(createErrorMiddleware()) - rpcEngine.push(streamMiddleware) - self.rpcEngine = rpcEngine -} - -// handle sendAsync requests via asyncProvider -// also remap ids inbound and outbound -MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { - const self = this - - if (payload.method === 'eth_signTypedData') { - console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.') - } - - self.rpcEngine.handle(payload, cb) -} - - -MetamaskInpageProvider.prototype.send = function (payload) { - const self = this - - let selectedAddress - let result = null - switch (payload.method) { - - case 'eth_accounts': - // read from localStorage - selectedAddress = self.publicConfigStore.getState().selectedAddress - result = selectedAddress ? [selectedAddress] : [] - break - - case 'eth_coinbase': - // read from localStorage - selectedAddress = self.publicConfigStore.getState().selectedAddress - result = selectedAddress || null - break - - case 'eth_uninstallFilter': - self.sendAsync(payload, noop) - result = true - break - - case 'net_version': - const networkVersion = self.publicConfigStore.getState().networkVersion - result = networkVersion || null - break - - // throw not-supported Error - default: - var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client' - var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.` - throw new Error(message) - - } - - // return the result - return { - id: payload.id, - jsonrpc: payload.jsonrpc, - result: result, - } -} - -MetamaskInpageProvider.prototype.isConnected = function () { - return true -} - -MetamaskInpageProvider.prototype.isMetaMask = true - -// util - -function logStreamDisconnectWarning (remoteLabel, err) { - let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` - if (err) warningMsg += '\n' + err.stack - console.warn(warningMsg) -} - -function noop () {} diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index 5db63f47d..38682b916 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -34,7 +34,7 @@ module.exports = function (provider) { return { cancel: true } } - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']}) return { remove () { diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 901367f04..47925b94b 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter { * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} after signature has been + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req) + // await finished + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the + * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object where the origin may be specificied * @returns {number} The id of the newly created message. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index e96ced1f2..fc2cccdf1 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter { * this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + if (!msgParams.from) { + reject(new Error('MetaMask Message Signature: from field is required.')) + } + const msgId = this.addUnapprovedMessage(msgParams, req) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to + * this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created PersonalMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = this.normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -257,4 +289,3 @@ module.exports = class PersonalMessageManager extends EventEmitter { } } - diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js deleted file mode 100644 index fd65d94f3..000000000 --- a/app/scripts/lib/port-stream.js +++ /dev/null @@ -1,80 +0,0 @@ -const Duplex = require('readable-stream').Duplex -const inherits = require('util').inherits -const noop = function () {} - -module.exports = PortDuplexStream - -inherits(PortDuplexStream, Duplex) - -/** - * Creates a stream that's both readable and writable. - * The stream supports arbitrary objects. - * - * @class - * @param {Object} port Remote Port object - */ -function PortDuplexStream (port) { - Duplex.call(this, { - objectMode: true, - }) - this._port = port - port.onMessage.addListener(this._onMessage.bind(this)) - port.onDisconnect.addListener(this._onDisconnect.bind(this)) -} - -/** - * Callback triggered when a message is received from - * the remote Port associated with this Stream. - * - * @private - * @param {Object} msg - Payload from the onMessage listener of Port - */ -PortDuplexStream.prototype._onMessage = function (msg) { - if (Buffer.isBuffer(msg)) { - delete msg._isBuffer - var data = new Buffer(msg) - this.push(data) - } else { - this.push(msg) - } -} - -/** - * Callback triggered when the remote Port - * associated with this Stream disconnects. - * - * @private - */ -PortDuplexStream.prototype._onDisconnect = function () { - this.destroy() -} - -/** - * Explicitly sets read operations to a no-op - */ -PortDuplexStream.prototype._read = noop - - -/** - * Called internally when data should be written to - * this writable stream. - * - * @private - * @param {*} msg Arbitrary object to write - * @param {string} encoding Encoding to use when writing payload - * @param {Function} cb Called when writing is complete or an error occurs - */ -PortDuplexStream.prototype._write = function (msg, encoding, cb) { - try { - if (Buffer.isBuffer(msg)) { - var data = msg.toJSON() - data._isBuffer = true - this._port.postMessage(data) - } else { - this._port.postMessage(msg) - } - } catch (err) { - return cb(new Error('PortDuplexStream - disconnected')) - } - cb() -} diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 3651524f1..e6e511640 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -70,11 +70,11 @@ function simplifyErrorMessages (report) { function rewriteErrorMessages (report, rewriteFn) { // rewrite top level message - if (report.message) report.message = rewriteFn(report.message) + if (typeof report.message === 'string') report.message = rewriteFn(report.message) // rewrite each exception message if (report.exception && report.exception.values) { report.exception.values.forEach(item => { - item.value = rewriteFn(item.value) + if (typeof item.value === 'string') item.value = rewriteFn(item.value) }) } } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index c58921610..b10145f3b 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -4,6 +4,7 @@ const createId = require('./random-id') const assert = require('assert') const sigUtil = require('eth-sig-util') const log = require('loglevel') +const jsonschema = require('jsonschema') /** * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a @@ -17,7 +18,7 @@ const log = require('loglevel') * @property {Object} msgParams.from The address that is making the signature request. * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored' * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will * always have a 'eth_signTypedData' type. * @@ -26,17 +27,10 @@ const log = require('loglevel') module.exports = class TypedMessageManager extends EventEmitter { /** * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. - * - * @typedef {Object} TypedMessage - * @param {Object} opts @deprecated - * @property {Object} memStore The observable store where TypedMessage are saved. - * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state - * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs - * @property {array} messages Holds all messages that have been created by this TypedMessage - * */ - constructor (opts) { + constructor ({ networkController }) { super() + this.networkController = networkController this.memStore = new ObservableStore({ unapprovedTypedMessages: {}, unapprovedTypedMessagesCount: 0, @@ -72,11 +66,43 @@ module.exports = class TypedMessageManager extends EventEmitter { * this.memStore. Before any of this is done, msgParams are validated * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req, version) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req, version) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + case 'errored': + return reject(new Error(`MetaMask Message Signature: ${data.error}`)) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to + * this.memStore. Before any of this is done, msgParams are validated + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created TypedMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req, version) { + msgParams.version = version this.validateParams(msgParams) + // add origin from request + if (req) msgParams.origin = req.origin log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // create txData obj with parameters and meta data @@ -103,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter { * */ validateParams (params) { - assert.equal(typeof params, 'object', 'Params should ben an object.') - assert.ok('data' in params, 'Params must include a data field.') - assert.ok('from' in params, 'Params must include a from field.') - assert.ok(Array.isArray(params.data), 'Data should be an array.') - assert.equal(typeof params.from, 'string', 'From field must be a string.') - assert.doesNotThrow(() => { - sigUtil.typedSignatureHash(params.data) - }, 'Expected EIP712 typed data') + switch (params.version) { + case 'V1': + assert.equal(typeof params, 'object', 'Params should ben an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.ok(Array.isArray(params.data), 'Data should be an array.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.doesNotThrow(() => { + sigUtil.typedSignatureHash(params.data) + }, 'Expected EIP712 typed data') + break + case 'V3': + let data + assert.equal(typeof params, 'object', 'Params should be an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.') + assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.') + const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) + assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`) + assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.') + const chainId = data.domain.chainId + const activeChainId = parseInt(this.networkController.getNetworkState()) + chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`) + break + } } /** @@ -185,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter { */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId + delete msgParams.version return Promise.resolve(msgParams) } @@ -198,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter { this._setMsgStatus(msgId, 'rejected') } + /** + * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the TypedMessage to error + * + */ + errorMessage (msgId, error) { + const msg = this.getMsg(msgId) + msg.error = error + this._updateMsg(msg) + this._setMsgStatus(msgId, 'errored') + } + // // PRIVATE METHODS // @@ -221,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter { msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) - if (status === 'rejected' || status === 'signed') { + if (status === 'rejected' || status === 'signed' || status === 'errored') { this.emit(`${msgId}:finished`, msg) } } diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index d7423f2ad..ea13b26be 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -127,7 +127,21 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) { return targetBN.mul(numBN).div(denomBN) } +function applyListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.on(key, listeners[key]) + }) +} + +function removeListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.removeListener(key, listeners[key]) + }) +} + module.exports = { + removeListeners, + applyListeners, getPlatform, getStack, getEnvironmentType, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index db323e3fe..123e17569 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -36,7 +36,6 @@ const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') -const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -46,9 +45,12 @@ const BN = require('ethereumjs-util').BN const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') -const cleanErrorStack = require('./lib/cleanErrorStack') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') +const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') +const EthQuery = require('eth-query') +const ethUtil = require('ethereumjs-util') +const sigUtil = require('eth-sig-util') module.exports = class MetamaskController extends EventEmitter { @@ -66,6 +68,10 @@ module.exports = class MetamaskController extends EventEmitter { const initState = opts.initState || {} this.recordFirstTimeInfo(initState) + // this keeps track of how many "controllerStream" connections are open + // the only thing that uses controller connections are open metamask UI instances + this.activeControllerConnections = 0 + // platform-specific api this.platform = opts.platform @@ -78,15 +84,11 @@ module.exports = class MetamaskController extends EventEmitter { // network store this.networkController = new NetworkController(initState.NetworkController) - // config manager - this.configManager = new ConfigManager({ - store: this.store, - }) - // preferences controller this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, + showWatchAssetUi: opts.showWatchAssetUi, network: this.networkController, }) @@ -107,8 +109,9 @@ module.exports = class MetamaskController extends EventEmitter { this.blacklistController.scheduleUpdates() // rpc provider - this.provider = this.initializeProvider() - this.blockTracker = this.provider._blockTracker + this.initializeProvider() + this.provider = this.networkController.getProviderAndBlockTracker().provider + this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ @@ -125,9 +128,17 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) + // start and stop polling for balances based on activeControllerConnections + this.on('controllerConnectionChanged', (activeControllerConnections) => { + if (activeControllerConnections > 0) { + this.accountTracker.start() + } else { + this.accountTracker.stop() + } + }) // key mgmt - const additionalKeyrings = [TrezorKeyring] + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, @@ -135,19 +146,7 @@ module.exports = class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, }) - // If only one account exists, make sure it is selected. - this.keyringController.memStore.subscribe((state) => { - const addresses = state.keyrings.reduce((res, keyring) => { - return res.concat(keyring.accounts) - }, []) - if (addresses.length === 1) { - const address = addresses[0] - this.preferencesController.setSelectedAddress(address) - } - // ensure preferences + identities controller know about all addresses - this.preferencesController.addAddresses(addresses) - this.accountTracker.syncWithAddresses(addresses) - }) + this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) // detect tokens controller this.detectTokensController = new DetectTokensController({ @@ -174,7 +173,7 @@ module.exports = class MetamaskController extends EventEmitter { blockTracker: this.blockTracker, getGasPrice: this.getGasPrice.bind(this), }) - this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) + this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx()) this.txController.on(`tx:status-update`, (txId, status) => { if (status === 'confirmed' || status === 'failed') { @@ -208,7 +207,7 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() - this.typedMessageManager = new TypedMessageManager() + this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) this.publicConfigStore = this.initPublicConfigStore() this.store.updateStructure({ @@ -252,30 +251,25 @@ module.exports = class MetamaskController extends EventEmitter { static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, - eth_sendTransaction: (payload, next, end) => { - const origin = payload.origin - const txParams = payload.params[0] - nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end) - }, }, + version, // account mgmt - getAccounts: (cb) => { + getAccounts: async () => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const result = [] const selectedAddress = this.preferencesController.getSelectedAddress() - // only show address if account is unlocked if (isUnlocked && selectedAddress) { - result.push(selectedAddress) + return [selectedAddress] + } else { + return [] } - cb(null, result) }, // tx signing - // old style msg signing - processMessage: this.newUnsignedMessage.bind(this), - // personal_sign msg signing + processTransaction: this.newUnapprovedTransaction.bind(this), + // msg signing + processEthSignMessage: this.newUnsignedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), + getPendingNonce: this.getPendingNonce.bind(this), } const providerProxy = this.networkController.initializeProvider(providerOpts) return providerProxy @@ -317,18 +311,15 @@ module.exports = class MetamaskController extends EventEmitter { * @returns {Object} status */ getState () { - const wallet = this.configManager.getWallet() const vault = this.keyringController.store.getState().vault - const isInitialized = (!!wallet || !!vault) + const isInitialized = !!vault return { ...{ isInitialized }, ...this.memStore.getFlatState(), - ...this.configManager.getConfig(), ...{ - lostAccounts: this.configManager.getLostAccounts(), - seedWords: this.configManager.getSeedWords(), - forgottenPassword: this.configManager.getPasswordForgotten(), + // TODO: Remove usages of lost accounts + lostAccounts: [], }, } } @@ -377,9 +368,7 @@ module.exports = class MetamaskController extends EventEmitter { connectHardware: nodeify(this.connectHardware, this), forgetDevice: nodeify(this.forgetDevice, this), checkHardwareStatus: nodeify(this.checkHardwareStatus, this), - - // TREZOR - unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this), // vault management submitPassword: nodeify(this.submitPassword, this), @@ -392,6 +381,7 @@ module.exports = class MetamaskController extends EventEmitter { setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController), removeToken: nodeify(preferencesController.removeToken, preferencesController), + removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), @@ -411,6 +401,7 @@ module.exports = class MetamaskController extends EventEmitter { updateTransaction: nodeify(txController.updateTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), retryTransaction: nodeify(this.retryTransaction, this), + createCancelTransaction: nodeify(this.createCancelTransaction, this), getFilteredTxList: nodeify(txController.getFilteredTxList, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController), estimateGas: nodeify(this.estimateGas, this), @@ -481,12 +472,32 @@ module.exports = class MetamaskController extends EventEmitter { async createNewVaultAndRestore (password, seed) { const releaseLock = await this.createVaultMutex.acquire() try { + let accounts, lastBalance + + const keyringController = this.keyringController + // clear known identities this.preferencesController.setAddresses([]) // create new vault - const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + const vault = await keyringController.createNewVaultAndRestore(password, seed) + + const ethQuery = new EthQuery(this.provider) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + + const primaryKeyring = keyringController.getKeyringsByType('HD Key Tree')[0] + if (!primaryKeyring) { + throw new Error('MetamaskController - No HD Key Tree found') + } + + // seek out the first zero balance + while (lastBalance !== '0x0') { + await keyringController.addNewAccount(primaryKeyring) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + } + // set new identities - const accounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(accounts) this.selectFirstIdentity() releaseLock() @@ -497,6 +508,30 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Get an account balance from the AccountTracker or request it directly from the network. + * @param {string} address - The account address + * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network + */ + getBalance (address, ethQuery) { + return new Promise((resolve, reject) => { + const cached = this.accountTracker.store.getState().accounts[address] + + if (cached && cached.balance) { + resolve(cached.balance) + } else { + ethQuery.getBalance(address, (error, balance) => { + if (error) { + reject(error) + log.error(error) + } else { + resolve(balance || '0x0') + } + }) + } + }) + } + /* * Submits the user's password and attempts to unlock the vault. * Also synchronizes the preferencesController, to ensure its schema @@ -540,45 +575,57 @@ module.exports = class MetamaskController extends EventEmitter { // Hardware // + async getKeyringForDevice (deviceName, hdPath = null) { + let keyringName = null + switch (deviceName) { + case 'trezor': + keyringName = TrezorKeyring.type + break + case 'ledger': + keyringName = LedgerBridgeKeyring.type + break + default: + throw new Error('MetamaskController:getKeyringForDevice - Unknown device') + } + let keyring = await this.keyringController.getKeyringsByType(keyringName)[0] + if (!keyring) { + keyring = await this.keyringController.addNewKeyring(keyringName) + } + if (hdPath && keyring.setHdPath) { + keyring.setHdPath(hdPath) + } + + keyring.network = this.networkController.getProviderConfig().type + + return keyring + + } + /** * Fetch account list from a trezor device. * * @returns [] accounts */ - async connectHardware (deviceName, page) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const oldAccounts = await keyringController.getAccounts() - let keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - keyring = await this.keyringController.addNewKeyring('Trezor Hardware') - } - let accounts = [] - - switch (page) { - case -1: - accounts = await keyring.getPreviousPage() - break - case 1: - accounts = await keyring.getNextPage() - break - default: - accounts = await keyring.getFirstPage() - } - - // Merge with existing accounts - // and make sure addresses are not repeated - const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] - this.accountTracker.syncWithAddresses(accountsToTrack) - return accounts - - default: - throw new Error('MetamaskController:connectHardware - Unknown device') + async connectHardware (deviceName, page, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + let accounts = [] + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } + + // Merge with existing accounts + // and make sure addresses are not repeated + const oldAccounts = await this.keyringController.getAccounts() + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) + return accounts } /** @@ -586,21 +633,9 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {Promise<boolean>} */ - async checkHardwareStatus (deviceName) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - return false - } - return keyring.isUnlocked() - default: - throw new Error('MetamaskController:checkHardwareStatus - Unknown device') - } + async checkHardwareStatus (deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + return keyring.isUnlocked() } /** @@ -610,20 +645,9 @@ module.exports = class MetamaskController extends EventEmitter { */ async forgetDevice (deviceName) { - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') - } - keyring.forgetDevice() - return true - default: - throw new Error('MetamaskController:forgetDevice - Unknown device') - } + const keyring = await this.getKeyringForDevice(deviceName) + keyring.forgetDevice() + return true } /** @@ -631,23 +655,19 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {} keyState */ - async unlockTrezorAccount (index) { - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') - } + async unlockHardwareWalletAccount (index, deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) keyring.setAccountToUnlock(index) - const oldAccounts = await keyringController.getAccounts() - const keyState = await keyringController.addNewAccount(keyring) - const newAccounts = await keyringController.getAccounts() + const oldAccounts = await this.keyringController.getAccounts() + const keyState = await this.keyringController.addNewAccount(keyring) + const newAccounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { - this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + // Set the account label to Trezor 1 / Ledger 1, etc + this.preferencesController.setAccountLabel(address, `${deviceName[0].toUpperCase()}${deviceName.slice(1)} ${parseInt(index, 10) + 1}`) + // Select the account this.preferencesController.setSelectedAddress(address) } }) @@ -701,7 +721,7 @@ module.exports = class MetamaskController extends EventEmitter { this.verifySeedPhrase() .then((seedWords) => { - this.configManager.setSeedWords(seedWords) + this.preferencesController.setSeedWords(seedWords) return cb(null, seedWords) }) .catch((err) => { @@ -750,7 +770,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {function} cb Callback function called with the current address. */ clearSeedWordCache (cb) { - this.configManager.setSeedWords(null) + this.preferencesController.setSeedWords(null) cb(null, this.preferencesController.getSelectedAddress()) } @@ -779,7 +799,8 @@ module.exports = class MetamaskController extends EventEmitter { // Remove account from the preferences controller this.preferencesController.removeAddress(address) // Remove account from the account tracker controller - this.accountTracker.removeAccount(address) + this.accountTracker.removeAccount([address]) + // Remove account from the keyring await this.keyringController.removeAccount(address) return address @@ -809,6 +830,18 @@ module.exports = class MetamaskController extends EventEmitter { // --------------------------------------------------------------------------- // Identity Management (signature operations) + /** + * Called when a Dapp suggests a new tx to be signed. + * this wrapper needs to exist so we can provide a reference to + * "newUnapprovedTransaction" before "txController" is instantiated + * + * @param {Object} msgParams - The params passed to eth_sign. + * @param {Object} req - (optional) the original request, containing the origin + */ + async newUnapprovedTransaction (txParams, req) { + return await this.txController.newUnapprovedTransaction(txParams, req) + } + // eth_sign methods: /** @@ -820,20 +853,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_sign. * @param {Function} cb = The callback function called with the signature. */ - newUnsignedMessage (msgParams, cb) { - const msgId = this.messageManager.addUnapprovedMessage(msgParams) + newUnsignedMessage (msgParams, req) { + const promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req) this.sendUpdate() this.opts.showUnconfirmedMessage() - this.messageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + return promise } /** @@ -887,24 +911,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - The callback function called with the signature. * Passed back to the requesting Dapp. */ - newUnsignedPersonalMessage (msgParams, cb) { - if (!msgParams.from) { - return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.'))) - } - - const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + async newUnsignedPersonalMessage (msgParams, req) { + const promise = this.personalMessageManager.addUnapprovedMessageAsync(msgParams, req) this.sendUpdate() this.opts.showUnconfirmedMessage() - this.personalMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + return promise } /** @@ -953,26 +964,11 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Function} cb - The callback function, called with the signature. */ - newUnsignedTypedMessage (msgParams, cb) { - let msgId - try { - msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - } catch (e) { - return cb(e) - } - - this.typedMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) - default: - return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) - } - }) + newUnsignedTypedMessage (msgParams, req) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + return promise } /** @@ -982,22 +978,31 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @returns {Object} Full state update. */ - signTypedMessage (msgParams) { - log.info('MetaMaskController - signTypedMessage') + async signTypedMessage (msgParams) { + log.info('MetaMaskController - eth_signTypedData') const msgId = msgParams.metamaskId - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - return this.typedMessageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { - // signs the message - return this.keyringController.signTypedMessage(cleanMsgParams) - }) - .then((rawSig) => { - // tells the listener that the message has been signed - // and can be returned to the dapp - this.typedMessageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) + const version = msgParams.version + try { + const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams) + const address = sigUtil.normalize(cleanMsgParams.from) + const keyring = await this.keyringController.getKeyringForAccount(address) + const wallet = keyring._getWalletForAccount(address) + const privKey = ethUtil.toBuffer(wallet.getPrivateKey()) + let signature + switch (version) { + case 'V1': + signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data }) + break + case 'V3': + signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) + break + } + this.typedMessageManager.setMsgStatusSigned(msgId, signature) + return this.getState() + } catch (error) { + log.info('MetaMaskController - eth_signTypedData failed.', error) + this.typedMessageManager.errorMessage(msgId, error) + } } /** @@ -1035,36 +1040,17 @@ module.exports = class MetamaskController extends EventEmitter { /** * A legacy method used to record user confirmation that they understand * that some of their accounts have been recovered but should be backed up. + * This function no longer does anything and will be removed. * * @deprecated * @param {Function} cb - A callback function called with a full state update. */ markAccountsFound (cb) { - this.configManager.setLostAccounts([]) - this.sendUpdate() + // TODO Remove me cb(null, this.getState()) } /** - * A legacy method (probably dead code) that was used when we swapped out our - * key management library that we depended on. - * - * Described in: - * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd - * - * @deprecated - * @param {} migratorOutput - */ - restoreOldLostAccounts (migratorOutput) { - const { lostAccounts } = migratorOutput - if (lostAccounts) { - this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) - return this.importLostAccounts(migratorOutput) - } - return Promise.resolve(migratorOutput) - } - - /** * An account object * @typedef Account * @property string privateKey - The private key of the account. @@ -1108,6 +1094,19 @@ module.exports = class MetamaskController extends EventEmitter { return state } + /** + * Allows a user to attempt to cancel a previously submitted transaction by creating a new + * transaction. + * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel + * @param {string=} customGasPrice - the hex value to use for the cancel transaction + * @returns {object} MetaMask state + */ + async createCancelTransaction (originalTxId, customGasPrice, cb) { + await this.txController.createCancelTransaction(originalTxId, customGasPrice) + const state = await this.getState() + return state + } + estimateGas (estimateGasParams) { return new Promise((resolve, reject) => { return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => { @@ -1129,7 +1128,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function called when complete. */ markPasswordForgotten (cb) { - this.configManager.setPasswordForgotten(true) + this.preferencesController.setPasswordForgotten(true) this.sendUpdate() cb() } @@ -1139,7 +1138,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function called when complete. */ unMarkPasswordForgotten (cb) { - this.configManager.setPasswordForgotten(false) + this.preferencesController.setPasswordForgotten(false) this.sendUpdate() cb() } @@ -1210,18 +1209,28 @@ module.exports = class MetamaskController extends EventEmitter { setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) + // report new active controller connection + this.activeControllerConnections++ + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // connect dnode api to remote connection pump( outStream, dnode, outStream, (err) => { + // report new active controller connection + this.activeControllerConnections-- + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // report any error if (err) log.error(err) } ) dnode.on('remote', (remote) => { // push updates to popup - const sendUpdate = remote.sendUpdate.bind(remote) + const sendUpdate = (update) => remote.sendUpdate(update) this.on('update', sendUpdate) + // remove update listener once the connection ends + dnode.on('end', () => this.removeListener('update', sendUpdate)) }) } @@ -1237,12 +1246,16 @@ module.exports = class MetamaskController extends EventEmitter { // create filter polyfill middleware const filterMiddleware = createFilterMiddleware({ provider: this.provider, - blockTracker: this.provider._blockTracker, + blockTracker: this.blockTracker, }) engine.push(createOriginMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin })) engine.push(filterMiddleware) + engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) + engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this)) + engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this)) + engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this)) engine.push(createProviderMiddleware({ provider: this.provider })) // setup connection @@ -1270,16 +1283,46 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide public config over. */ setupPublicConfig (outStream) { + const configStream = asStream(this.publicConfigStore) pump( - asStream(this.publicConfigStore), + configStream, outStream, (err) => { + configStream.destroy() if (err) log.error(err) } ) } /** + * Handle a KeyringController update + * @param {object} state the KC state + * @return {Promise<void>} + * @private + */ + async _onKeyringControllerUpdate (state) { + const {isUnlocked, keyrings} = state + const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), []) + + if (!addresses.length) { + return + } + + // Ensure preferences + identities controller know about all addresses + this.preferencesController.addAddresses(addresses) + this.accountTracker.syncWithAddresses(addresses) + + const wasLocked = !isUnlocked + if (wasLocked) { + const oldSelectedAddress = this.preferencesController.getSelectedAddress() + if (!addresses.includes(oldSelectedAddress)) { + const address = addresses[0] + await this.preferencesController.setSelectedAddress(address) + } + } + } + + /** * A method for emitting the full MetaMask state to all registered listeners. * @private */ @@ -1321,6 +1364,19 @@ module.exports = class MetamaskController extends EventEmitter { return '0x' + percentileNumBn.mul(GWEI_BN).toString(16) } + /** + * Returns the nonce that will be associated with a transaction once approved + * @param address {string} - The hex string address for the transaction + * @returns Promise<number> + */ + async getPendingNonce (address) { + const { nonceDetails, releaseLock} = await this.txController.nonceTracker.getNonceLock(address) + const pendingNonce = nonceDetails.params.highestSuggested + + releaseLock() + return pendingNonce + } + //============================================================================= // CONFIG //============================================================================= @@ -1425,6 +1481,7 @@ module.exports = class MetamaskController extends EventEmitter { } } + // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events. /** * A method for recording whether the MetaMask user interface is open or not. * @private @@ -1445,4 +1502,34 @@ module.exports = class MetamaskController extends EventEmitter { set isClientOpenAndUnlocked (active) { this.tokenRatesController.isActive = active } + + /** + * Creates RPC engine middleware for processing eth_signTypedData requests + * + * @param {Object} req - request object + * @param {Object} res - response object + * @param {Function} - next + * @param {Function} - end + */ + createTypedDataMiddleware (methodName, version, reverse) { + return async (req, res, next, end) => { + const { method, params } = req + if (method === methodName) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync({ + data: reverse ? params[1] : params[0], + from: reverse ? params[0] : params[1], + }, req, version) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + try { + res.result = await promise + end() + } catch (error) { + end(error) + } + } else { + next() + } + } + } } diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js deleted file mode 100644 index 7a4578ea7..000000000 --- a/app/scripts/migrations/_multi-keyring.js +++ /dev/null @@ -1,50 +0,0 @@ -const version = 5 - -/* - -This is an incomplete migration bc it requires post-decrypted data -which we dont have access to at the time of this writing. - -*/ - -const ObservableStore = require('obs-store') -const ConfigManager = require('../../app/scripts/lib/config-manager') -const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') -const KeyringController = require('eth-keyring-controller') - -const password = 'obviously not correct' - -module.exports = { - version, - - migrate: function (versionedData) { - versionedData.meta.version = version - - const store = new ObservableStore(versionedData.data) - const configManager = new ConfigManager({ store }) - const idStoreMigrator = new IdentityStoreMigrator({ configManager }) - const keyringController = new KeyringController({ - configManager: configManager, - }) - - // attempt to migrate to multiVault - return idStoreMigrator.migratedVaultForPassword(password) - .then((result) => { - // skip if nothing to migrate - if (!result) return Promise.resolve(versionedData) - delete versionedData.data.wallet - // create new keyrings - const privKeys = result.lostAccounts.map(acct => acct.privateKey) - return Promise.all([ - keyringController.restoreKeyring(result.serialized), - keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }), - ]).then(() => { - return keyringController.persistAllKeyrings(password) - }).then(() => { - // copy result on to state object - versionedData.data = store.get() - return Promise.resolve(versionedData) - }) - }) - }, -} diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js index 2def4371e..ce686d9d1 100644 --- a/app/scripts/notice-controller.js +++ b/app/scripts/notice-controller.js @@ -7,7 +7,7 @@ const uniqBy = require('lodash.uniqby') module.exports = class NoticeController extends EventEmitter { - constructor (opts) { + constructor (opts = {}) { super() this.noticePoller = null this.firstVersion = opts.firstVersion diff --git a/app/scripts/ui.js b/app/scripts/ui.js index da100f928..98a036338 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -2,7 +2,7 @@ const injectCss = require('inject-css') const OldMetaMaskUiCss = require('../../old-ui/css') const NewMetaMaskUiCss = require('../../ui/css') const startPopup = require('./popup-core') -const PortStream = require('./lib/port-stream.js') +const PortStream = require('extension-port-stream') const { getEnvironmentType } = require('./lib/util') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums') const extension = require('extensionizer') |
