├── .babelrc ├── .env.example ├── .gitignore ├── license ├── package-lock.json ├── package.json ├── readme.md ├── scatter.zip ├── src ├── background.js ├── components │ ├── ButtonComponent.vue │ ├── InputComponent.vue │ ├── KeyValue.vue │ ├── NavActionsComponent.vue │ ├── NavbarComponent.vue │ ├── SearchComponent.vue │ ├── SelectComponent.vue │ └── alerts │ │ └── Alert.vue ├── content.js ├── copied │ ├── assets │ │ └── fonts │ │ │ └── font-awesome │ │ │ ├── HELP-US-OUT.txt │ │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ ├── icon.png │ ├── index.html │ ├── manifest.json │ └── prompt.html ├── data │ └── Countries.js ├── inject.js ├── localization │ ├── keys.js │ ├── languages │ │ ├── chinese.js │ │ ├── english.js │ │ ├── french.js │ │ ├── german.js │ │ ├── hebrew.js │ │ ├── korean.js │ │ ├── portuguese.js │ │ ├── slovene.js │ │ └── spanish.js │ ├── locales.js │ └── readme.md ├── messages │ ├── InternalMessage.js │ ├── InternalMessageTypes.js │ ├── NetworkMessage.js │ ├── NetworkMessageTypes.js │ └── PairingTags.js ├── migrations │ ├── migrator.js │ └── versions │ │ ├── 4.0.1.js │ │ ├── 5.0.2.js │ │ ├── 5.0.4.js │ │ ├── 6.0.0.js │ │ ├── 6.0.4.js │ │ └── version.js ├── models │ ├── Account.js │ ├── Blockchains.js │ ├── Identity.js │ ├── KeyPair.js │ ├── Keychain.js │ ├── Meta.js │ ├── Network.js │ ├── Permission.js │ ├── Scatter.js │ ├── Settings.js │ ├── alerts │ │ ├── AlertMsg.js │ │ └── AlertTypes.js │ ├── errors │ │ ├── Error.js │ │ └── ErrorTypes.js │ ├── histories │ │ ├── HistoricEvent.js │ │ └── HistoricEventTypes.js │ └── prompts │ │ ├── Prompt.js │ │ └── PromptTypes.js ├── plugins │ ├── Plugin.js │ ├── PluginRepository.js │ ├── PluginTypes.js │ └── defaults │ │ ├── eos.js │ │ └── eth.js ├── popup.js ├── prompt.js ├── prompts │ ├── PromptBase.vue │ ├── RequestAddNetwork.vue │ ├── RequestArbitrarySignaturePrompt.vue │ ├── RequestIdentityPrompt.vue │ ├── RequestSignaturePrompt.vue │ ├── RequestUnlock.vue │ └── UpdateVersion.vue ├── scatterdapp.js ├── services │ ├── AccountService.js │ ├── AuthenticationService.js │ ├── IdentityService.js │ ├── KeyPairService.js │ ├── NotificationService.js │ ├── RIDLService.js │ ├── SignatureService.js │ └── StorageService.js ├── store │ ├── actions.js │ ├── constants.js │ ├── mutations.js │ └── store.js ├── styles.scss ├── util │ ├── BrowserApis.js │ ├── ContractHelpers.js │ ├── EOSKeygen.js │ ├── GenericTools.js │ ├── Hasher.js │ ├── IdGenerator.js │ ├── Mnemonic.js │ ├── ObjectHelpers.js │ ├── StringHelpers.js │ └── TimingHelpers.js ├── views │ ├── AutoLockView.vue │ ├── BackupView.vue │ ├── ChangePasswordView.vue │ ├── DestroyView.vue │ ├── DomainPermissionView.vue │ ├── EntryView.vue │ ├── HistoryView.vue │ ├── IdentitiesView.vue │ ├── IdentityView.vue │ ├── KeyPairView.vue │ ├── KeyPairsView.vue │ ├── LanguageView.vue │ ├── LoadFromBackup.vue │ ├── MainMenuView.vue │ ├── NetworkView.vue │ ├── NetworksView.vue │ ├── Onboarding.vue │ ├── PermissionsView.vue │ ├── SettingsView.vue │ ├── ShowMnemonicView.vue │ └── ViewBase.vue └── vue │ ├── Routing.js │ └── VueInitializer.js ├── tests ├── helpers │ └── chrome.js ├── localization │ └── locales.spec.js ├── migrations │ ├── 5.0.4.spec.js │ └── 6.0.4.spec.js ├── models │ └── Identity.spec.js ├── pk2pk.spec.js ├── services │ └── StorageService.spec.js ├── setup.js ├── signers │ └── eos.spec.js └── util │ ├── ContractHelpers.spec.js │ ├── IdGenerator.spec.js │ └── ObjectHelpers.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-3"], 3 | "plugins": [ 4 | ["transform-runtime", { 5 | "regenerator": true 6 | } 7 | ] 8 | ] 9 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SCATTER_ENV=production -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | build/ 4 | .env 5 | .DS_Store 6 | *.iml -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright 2018 EOS Essentials 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scatter", 3 | "version": "6.3.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "set SCATTER_ENV=testing&& mocha-webpack --timeout 1000000 --webpack-config webpack.config.js --require tests/setup.js \"tests/**/*.spec.js\"", 8 | "testfile": "set SCATTER_ENV=testing&& mocha-webpack --timeout 1000000 --webpack-config webpack.config.js --require tests/setup.js ", 9 | "start": "set SCATTER_ENV=development&& webpack --watch", 10 | "build": "set SCATTER_ENV=production&& webpack" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "aes-oop": "^1.0.4", 16 | "bip39": "^2.5.0", 17 | "dotenv-webpack": "^1.5.7", 18 | "eos-rc-parser": "^1.0.4", 19 | "eosjs": "^16.0.5", 20 | "eosjs-api": "^6.3.2", 21 | "eosjs-ecc": "^4.0.3", 22 | "ethereumjs-abi": "^0.6.5", 23 | "ethereumjs-tx": "^1.3.7", 24 | "ethereumjs-util": "^5.2.0", 25 | "extension-streams": "^1.0.7", 26 | "fcbuffer": "^2.2.1", 27 | "ridl": "^0.1.0", 28 | "scrypt-async": "^2.0.0", 29 | "secp256k1": "^3.5.0", 30 | "vue": "^2.5.17", 31 | "vue-moment": "^3.2.0", 32 | "vue-router": "^3.0.1", 33 | "vuex": "^3.0.1", 34 | "web3": "^1.0.0-beta2", 35 | "web3-provider-engine": "^14.1.0" 36 | }, 37 | "devDependencies": { 38 | "babel-core": "^6.26.3", 39 | "babel-loader": "^7.1.5", 40 | "babel-plugin-transform-runtime": "^6.23.0", 41 | "babel-preset-es2015": "^6.24.1", 42 | "babel-preset-stage-3": "^6.24.1", 43 | "chai": "^4.1.2", 44 | "copy-webpack-plugin": "^4.5.2", 45 | "css-loader": "^0.28.11", 46 | "extract-text-webpack-plugin": "^3.0.2", 47 | "file-loader": "^1.1.11", 48 | "ignore-emit-webpack-plugin": "^0.2.1", 49 | "jsdom": "^11.12.0", 50 | "jsdom-global": "^3.0.2", 51 | "mocha": "^5.2.0", 52 | "mocha-webpack": "^1.1.0", 53 | "node-sass": "^4.9.3", 54 | "sass-loader": "^6.0.7", 55 | "uglifyjs-webpack-plugin": "^1.3.0", 56 | "vue-loader": "^13.7.2", 57 | "vue-template-compiler": "^2.5.17", 58 | "webpack": "^3.12.0", 59 | "webpack-node-externals": "^1.7.2", 60 | "zip-webpack-plugin": "^2.1.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Scatter Classic 2 | 3 | Scatter is a browser extension that allows you to sign transactions for multiple blockchains and provide personal information to web applications without ever exposing your keys or filling out forms. 4 | 5 | # NOTICE: Scatter Classic is DEPRECATED! 6 | ## Do not build apps only for Scatter Classic ( Extension ). 7 | 8 | ### Visit https://get-scatter.com/docs/dev/setting-up-for-web-apps to find out how to make dapps that support Classic, Desktop and Mobile all at once. 9 | 10 | If you want more information about this decision please read this article: https://medium.com/@nsjames/the-blockchain-isnt-just-for-web-applications-silly-rabbit-926a4ea5ccd1 11 | 12 | 13 | -------------------------------------------------------------------------------- /scatter.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/scatter.zip -------------------------------------------------------------------------------- /src/components/ButtonComponent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/InputComponent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/KeyValue.vue: -------------------------------------------------------------------------------- 1 | 20 | 31 | -------------------------------------------------------------------------------- /src/components/NavActionsComponent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 78 | -------------------------------------------------------------------------------- /src/components/NavbarComponent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/SearchComponent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/SelectComponent.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | 54 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/assets/fonts/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/assets/fonts/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/copied/assets/fonts/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /src/copied/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetScatter/ScatterWebExtension/0b9b3818a7ca1a0fa6982f8d7f703f05e9f60d69/src/copied/icon.png -------------------------------------------------------------------------------- /src/copied/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/copied/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Scatter", 4 | "description": "Decentralized signature, identity and authentication system.", 5 | "version": "6.3.0", 6 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 7 | "browser_action": { 8 | "default_icon": "icon.png", 9 | "default_popup": "index.html" 10 | }, 11 | "permissions": [ 12 | "storage", 13 | "unlimitedStorage", 14 | "http://*/*", 15 | "https://*/*" 16 | ], 17 | "content_scripts": [ 18 | { 19 | "matches": [ 20 | "http://*/*", 21 | "https://*/*" 22 | ], 23 | "js": ["content.js"], 24 | "run_at": "document_end" 25 | } 26 | ], 27 | "web_accessible_resources": [ 28 | "inject.js" 29 | ], 30 | "background":{ 31 | "scripts":[ 32 | "background.js" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /src/copied/prompt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/inject.js: -------------------------------------------------------------------------------- 1 | import IdGenerator from './util/IdGenerator'; 2 | import {EncryptedStream} from 'extension-streams'; 3 | import * as PairingTags from './messages/PairingTags' 4 | import * as NetworkMessageTypes from './messages/NetworkMessageTypes' 5 | import Scatterdapp from './scatterdapp' 6 | 7 | /*** 8 | * This is the javascript which gets injected into 9 | * the application and facilitates communication between 10 | * Scatter and the web application. 11 | */ 12 | class Inject { 13 | 14 | constructor(){ 15 | // Injecting an encrypted stream into the 16 | // web application. 17 | // const stream = new EncryptedStream(PairingTags.INJECTED, IdGenerator.text(64)); 18 | // 19 | // // Waiting for scatter to push itself onto the application 20 | // stream.listenWith(msg => { 21 | // if(msg && msg.hasOwnProperty('type') && msg.type === NetworkMessageTypes.PUSH_SCATTER) 22 | // window.scatter = new Scatterdapp(stream, msg.payload); 23 | // }); 24 | // 25 | // // Syncing the streams between the 26 | // // extension and the web application 27 | // stream.sync(PairingTags.SCATTER, stream.key); 28 | } 29 | 30 | } 31 | 32 | new Inject(); 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/localization/locales.js: -------------------------------------------------------------------------------- 1 | import * as KEYS from './keys'; 2 | 3 | import english from './languages/english'; 4 | import french from './languages/french'; 5 | import portuguese from './languages/portuguese'; 6 | import spanish from './languages/spanish'; 7 | import hebrew from './languages/hebrew'; 8 | import slovene from './languages/slovene'; 9 | import german from './languages/german'; 10 | import korean from './languages/korean'; 11 | import chinese from './languages/chinese'; 12 | 13 | export const LANG = { 14 | ENGLISH: 'English', 15 | FRENCH: 'Français ( French )', 16 | PORTUGUESE: 'Português ( Portuguese )', 17 | SPANISH: 'Español ( Spanish )', 18 | SLOVENE: 'Slovensko ( Slovene )', 19 | HEBREW: 'עברית ( Hebrew )', 20 | GERMAN: 'Deutsch ( German )', 21 | KOREAN: '한국어 ( Korean )', 22 | CHINESE: '中文 ( Chinese )', 23 | }; 24 | 25 | const languages = { 26 | [LANG.ENGLISH]:english, 27 | [LANG.FRENCH]:french, 28 | [LANG.PORTUGUESE]:portuguese, 29 | [LANG.SPANISH]:spanish, 30 | [LANG.SLOVENE]:slovene, 31 | [LANG.HEBREW]:hebrew, 32 | [LANG.GERMAN]:german, 33 | [LANG.KOREAN]:korean, 34 | [LANG.CHINESE]:chinese, 35 | }; 36 | 37 | export const locales = () => { 38 | return Object.keys(LANG).reduce((langobj, lang) => { 39 | langobj[lang] = Object.keys(KEYS).reduce((keyobj, key) => { 40 | keyobj[KEYS[key]] = languages[LANG[lang]][KEYS[key]]; 41 | return keyobj; 42 | }, {}); 43 | return langobj; 44 | }, {}); 45 | }; 46 | 47 | export const getLangKey = lang => Object.keys(LANG).find(key => LANG[key] === lang); 48 | export const localized = (key, language) => locales()[language][key] || locales()['ENGLISH'][key] 49 | -------------------------------------------------------------------------------- /src/localization/readme.md: -------------------------------------------------------------------------------- 1 | # Help add languages. 2 | 3 | The first contributor of every new language gets their name in the main `readme.md` file. 4 | 5 | 6 | ### Adding a new language 7 | 8 | ----------- 9 | 10 | #### Before you do anything, 11 | Open a ticket in the `Issues` panel stating that you are adding a language so that multiple people don't work on the same thing. 12 | **Use the following format:** 13 | 14 | **Adding Language: {LANGUAGE NAME HERE}** 15 | 16 | --------- 17 | 18 | 19 | ##### First copy the `src/localization/languages/english.js` file into a new `.js` file in the same directory. 20 | Once you have copied it go to the `src/localization/locales.js` file and add it to the arrays/objects. 21 | 22 | For example, if we were adding chinese. 23 | ```js 24 | import english from './languages/english' 25 | import chinese from './languages/chinese' 26 | 27 | 28 | export const LANG = { 29 | ENGLISH:'English', 30 | CHINESE:'中文 ( Chinese )' 31 | }; 32 | 33 | const languages = { 34 | [LANG.ENGLISH]:english, 35 | [LANG.CHINESE]:chinese 36 | }; 37 | ``` 38 | 39 | **Always add an english version in parenthesis.** 40 | 41 | While you're translating the new language file try to keep string lengths the same or less. In some places the lengths of the strings are pushing the bounds of their containers and if you translate into longer strings there's a chance they will get cut off by a popup's bounding box. 42 | 43 | -------------------------------------------------------------------------------- /src/messages/InternalMessage.js: -------------------------------------------------------------------------------- 1 | import {LocalStream} from 'extension-streams'; 2 | 3 | export default class InternalMessage { 4 | 5 | constructor(){ 6 | this.type = ''; 7 | this.payload = ''; 8 | } 9 | 10 | static placeholder(){ return new InternalMessage(); } 11 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 12 | 13 | static payload(type, payload){ 14 | let p = this.placeholder(); 15 | p.type = type; 16 | p.payload = payload; 17 | return p; 18 | } 19 | 20 | static signal(type){ 21 | let p = this.placeholder(); 22 | p.type = type; 23 | return p; 24 | } 25 | 26 | send(){ 27 | return LocalStream.send(this); 28 | } 29 | } -------------------------------------------------------------------------------- /src/messages/InternalMessageTypes.js: -------------------------------------------------------------------------------- 1 | export const SET_SEED = 'setSeed'; 2 | export const GET_SEED = 'getSeed'; 3 | export const SET_TIMEOUT = 'setTimeout'; 4 | export const IS_UNLOCKED = 'isUnlocked'; 5 | export const LOAD = 'load'; 6 | export const UPDATE = 'update'; 7 | export const PUB_TO_PRIV = 'publicToPrivateKey'; 8 | export const DESTROY = 'destroy'; 9 | 10 | export const IDENTITY_FROM_PERMISSIONS = 'identityFromPermissions'; 11 | export const ABI_CACHE = 'abiCache'; 12 | export const GET_OR_REQUEST_IDENTITY = 'getOrRequestIdentity'; 13 | export const FORGET_IDENTITY = 'forgetIdentity'; 14 | export const REQUEST_SIGNATURE = 'requestSignature'; 15 | export const REQUEST_ARBITRARY_SIGNATURE = 'requestArbitrarySignature'; 16 | export const REQUEST_ADD_NETWORK = 'requestAddNetwork'; 17 | export const REQUEST_GET_VERSION = 'requestGetVersion'; 18 | export const REQUEST_VERSION_UPDATE = 'requestVersionUpdate'; 19 | export const AUTHENTICATE = 'authenticate'; 20 | 21 | export const SET_PROMPT = 'setPrompt'; 22 | export const GET_PROMPT = 'getPrompt'; 23 | -------------------------------------------------------------------------------- /src/messages/NetworkMessage.js: -------------------------------------------------------------------------------- 1 | import Network from '../models/Network' 2 | import * as NetworkMessageTypes from './NetworkMessageTypes'; 3 | 4 | export default class NetworkMessage { 5 | 6 | constructor(_type = '', _payload = {}, _resolver = '', _domain = ''){ 7 | this.type = _type; 8 | this.payload = _payload; 9 | this.resolver = _resolver; 10 | this.domain = _domain; 11 | } 12 | 13 | static placeholder(){ return new NetworkMessage(); } 14 | static fromJson(json){ 15 | let p = Object.assign(this.placeholder(), json); 16 | return p; 17 | } 18 | 19 | static payload(type, payload){ 20 | let p = this.placeholder(); 21 | p.type = type; 22 | p.payload = payload; 23 | return p; 24 | } 25 | 26 | static signal(type){ 27 | let p = this.placeholder(); 28 | p.type = type; 29 | return p; 30 | } 31 | 32 | respond(payload){ return new NetworkMessage(this.type, payload, this.resolver); } 33 | error(payload){ return new NetworkMessage(NetworkMessageTypes.ERROR, payload, this.resolver); } 34 | } -------------------------------------------------------------------------------- /src/messages/NetworkMessageTypes.js: -------------------------------------------------------------------------------- 1 | export const ERROR = 'error'; 2 | export const PUSH_SCATTER = 'pushScatter'; 3 | export const GET_OR_REQUEST_IDENTITY = 'getOrRequestIdentity'; 4 | export const IDENTITY_FROM_PERMISSIONS = 'identityFromPermissions'; 5 | export const FORGET_IDENTITY = 'forgetIdentity'; 6 | export const REQUEST_SIGNATURE = 'requestSignature'; 7 | export const ABI_CACHE = 'abiCache'; 8 | export const REQUEST_ARBITRARY_SIGNATURE = 'requestArbitrarySignature'; 9 | export const REQUEST_ADD_NETWORK = 'requestAddNetwork'; 10 | export const REQUEST_VERSION_UPDATE = 'requestVersionUpdate'; 11 | export const AUTHENTICATE = 'authenticate'; -------------------------------------------------------------------------------- /src/messages/PairingTags.js: -------------------------------------------------------------------------------- 1 | 2 | export const INJECTED = 'injected'; 3 | export const SCATTER = 'scatter'; -------------------------------------------------------------------------------- /src/migrations/migrator.js: -------------------------------------------------------------------------------- 1 | import * as migrators from './versions/version'; 2 | 3 | const mathematicalVersion = version => { 4 | if(version === '0') return 0; 5 | const parts = version.replace(/[.]/g,'_').replace(/[m]/g, '').split('_'); 6 | if(parts.length !== 3) throw new Error("Migration error, invalid version"); 7 | const zeroed = (x,z) => { let s = x.toString(); while(s.length < z) s += '0'; return s; }; 8 | return parseInt(parts.map((x,i) => i === 0 ? zeroed(x,4) : zeroed(x,2)).join('')); 9 | }; 10 | 11 | const fnToVersion = fnName => fnName.replace(/[m]/g, '').replace(/[_]/g,'.'); 12 | 13 | export default async scatter => { 14 | scatter.meta.regenerateVersion(); 15 | if(scatter.isEncrypted()) return false; 16 | if(!scatter.meta.needsUpdating()) return false; 17 | 18 | const lastVersion = mathematicalVersion(scatter.meta.lastVersion); 19 | const nextVersions = Object.keys(migrators).filter(v => mathematicalVersion(v) > lastVersion); 20 | if(nextVersions.length) { 21 | await Promise.all(nextVersions.map(async version => await migrators[version](scatter))); 22 | scatter.meta.lastVersion = fnToVersion(nextVersions[nextVersions.length-1]); 23 | } 24 | 25 | return nextVersions.length > 0; 26 | } -------------------------------------------------------------------------------- /src/migrations/versions/4.0.1.js: -------------------------------------------------------------------------------- 1 | export const m4_0_1 = scatter => { 2 | 3 | }; -------------------------------------------------------------------------------- /src/migrations/versions/5.0.2.js: -------------------------------------------------------------------------------- 1 | import PluginRepository from '../../plugins/PluginRepository'; 2 | import {Blockchains} from '../../models/Blockchains' 3 | 4 | export const m5_0_2 = async scatter => { 5 | const eos = PluginRepository.plugin(Blockchains.EOS); 6 | const endorsedNetwork = await eos.getEndorsedNetwork(); 7 | if(!scatter.settings.networks.find(network => network.host === endorsedNetwork.host)) 8 | scatter.settings.networks.push(endorsedNetwork); 9 | }; -------------------------------------------------------------------------------- /src/migrations/versions/5.0.4.js: -------------------------------------------------------------------------------- 1 | import PluginRepository from '../../plugins/PluginRepository'; 2 | import {Blockchains} from '../../models/Blockchains' 3 | 4 | export const m5_0_4 = async scatter => { 5 | const endorsedNetworks = [ 6 | await PluginRepository.plugin(Blockchains.EOS).getEndorsedNetwork(), 7 | await PluginRepository.plugin(Blockchains.ETH).getEndorsedNetwork() 8 | ]; 9 | 10 | scatter.settings.networks.map(network => { 11 | const endorsedNetwork = endorsedNetworks.find(endorsed => endorsed.host === network.host); 12 | if(endorsedNetwork) { 13 | const endorsedNetwork = endorsedNetworks.find(endorsed => endorsed.host === network.host); 14 | network.name = endorsedNetwork.name; 15 | network.port = endorsedNetwork.port; 16 | network.protocol = endorsedNetwork.protocol; 17 | } else { 18 | network.protocol = network.port === 443 ? 'https' : 'http' 19 | } 20 | }); 21 | }; -------------------------------------------------------------------------------- /src/migrations/versions/6.0.0.js: -------------------------------------------------------------------------------- 1 | import PluginRepository from '../../plugins/PluginRepository'; 2 | import {Blockchains} from '../../models/Blockchains' 3 | 4 | export const m6_0_0 = async scatter => { 5 | scatter.meta.acceptedTerms = false; 6 | return true; 7 | }; -------------------------------------------------------------------------------- /src/migrations/versions/6.0.4.js: -------------------------------------------------------------------------------- 1 | import PluginRepository from '../../plugins/PluginRepository'; 2 | import {Blockchains} from '../../models/Blockchains' 3 | 4 | export const m6_0_4 = async scatter => { 5 | const endorsedNetworks = [ 6 | await PluginRepository.plugin(Blockchains.EOS).getEndorsedNetwork(), 7 | await PluginRepository.plugin(Blockchains.ETH).getEndorsedNetwork() 8 | ]; 9 | 10 | scatter.settings.networks.map(network => { 11 | const endorsedNetwork = endorsedNetworks.find(endorsed => endorsed.host === network.host); 12 | if(endorsedNetwork) { 13 | const endorsedNetwork = endorsedNetworks.find(endorsed => endorsed.host === network.host); 14 | network.port = endorsedNetwork.port; 15 | } 16 | }); 17 | 18 | scatter.keychain.identities.map(id => { 19 | const filtered = {}; 20 | Object.keys(id.accounts).map(key => { 21 | if(scatter.settings.networks.find(net => net.unique() === key)) 22 | filtered[key] = id.accounts[key]; 23 | }) 24 | 25 | id.accounts = filtered; 26 | }); 27 | 28 | return true; 29 | }; -------------------------------------------------------------------------------- /src/migrations/versions/version.js: -------------------------------------------------------------------------------- 1 | export * from './4.0.1'; 2 | export * from './5.0.2'; 3 | export * from './5.0.4'; 4 | export * from './6.0.0'; 5 | export * from './6.0.4'; -------------------------------------------------------------------------------- /src/models/Account.js: -------------------------------------------------------------------------------- 1 | import KeyPair from './KeyPair'; 2 | import PluginRepository from '../plugins/PluginRepository'; 3 | 4 | export default class Account { 5 | constructor(){ 6 | this.keypairUnique = ''; 7 | this.publicKey = ''; 8 | this.name = ''; 9 | this.authority = ''; 10 | } 11 | 12 | formatted(){ 13 | const blockchain = this.blockchain(); 14 | if(!blockchain || !blockchain.length) return this.name; 15 | return PluginRepository.plugin(blockchain).accountFormatter(this); 16 | } 17 | 18 | blockchain(){ 19 | return this.keypairUnique.split(':')[0]; 20 | } 21 | 22 | static placeholder(){ return new Account(); } 23 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 24 | 25 | static nameIsValid(name){ 26 | return /(^[a-z1-5.]{1,11}[a-z1-5]$)|(^[a-z1-5.]{12}[a-j1-5]$)/g.test(name) 27 | } 28 | } -------------------------------------------------------------------------------- /src/models/Blockchains.js: -------------------------------------------------------------------------------- 1 | 2 | export const Blockchains = { 3 | EOS:'eos', 4 | ETH:'eth' 5 | }; 6 | 7 | export const BlockchainsArray = 8 | Object.keys(Blockchains).map(key => ({key, value:Blockchains[key]})); -------------------------------------------------------------------------------- /src/models/KeyPair.js: -------------------------------------------------------------------------------- 1 | import AES from 'aes-oop'; 2 | 3 | import {Blockchains} from './Blockchains'; 4 | 5 | export default class KeyPair { 6 | 7 | constructor(){ 8 | this.blockchain = Blockchains.EOS; 9 | this.name = ''; 10 | this.privateKey = ''; 11 | this.publicKey = ''; 12 | } 13 | 14 | static placeholder(){ return new KeyPair(); } 15 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 16 | 17 | unique(){ return `${this.blockchain}:${this.publicKey.toLowerCase()}`; } 18 | 19 | static blockchain(publicKey){ 20 | if(publicKey.indexOf('EOS') !== -1) return Blockchains.EOS; 21 | if(publicKey.indexOf('0x') !== -1 && publicKey.length === 42) return Blockchains.ETH; 22 | return null; 23 | } 24 | 25 | /*** 26 | * Checks whether a private key is encrypted 27 | * @returns {boolean} 28 | */ 29 | isEncrypted(){ switch(this.blockchain) { 30 | // EOS private keys are 51 chars long 31 | case Blockchains.EOS: return this.privateKey.length > 51; 32 | // ETH private keys are 64 chars long 33 | case Blockchains.ETH: return this.privateKey.length > 64; 34 | }} 35 | 36 | /*** 37 | * Encrypts this KeyPair's Private Key 38 | * @param seed - The seed to encrypt with 39 | */ 40 | encrypt(seed){ 41 | if(!this.isEncrypted()) 42 | this.privateKey = AES.encrypt(this.privateKey, seed); 43 | } 44 | 45 | /*** 46 | * Decrypts this KeyPair's Private Key 47 | * @param seed - The seed to decrypt with 48 | */ 49 | decrypt(seed){ 50 | if(this.isEncrypted()) 51 | this.privateKey = AES.decrypt(this.privateKey, seed); 52 | } 53 | } -------------------------------------------------------------------------------- /src/models/Keychain.js: -------------------------------------------------------------------------------- 1 | import Identity from './Identity'; 2 | import Permission from './Permission'; 3 | import KeyPair from './KeyPair'; 4 | import IdGenerator from '../util/IdGenerator'; 5 | import ObjectHelpers from '../util/ObjectHelpers'; 6 | import AES from 'aes-oop'; 7 | 8 | export default class Keychain { 9 | 10 | constructor(){ 11 | this.keypairs = []; 12 | this.identities = []; 13 | this.permissions = []; 14 | } 15 | 16 | static placeholder(){ return new Keychain(); } 17 | static fromJson(json){ 18 | let p = Object.assign(this.placeholder(), json); 19 | if(json.hasOwnProperty('keypairs')) p.keypairs = json.keypairs.map(x => KeyPair.fromJson(x)); 20 | if(json.hasOwnProperty('identities')) p.identities = json.identities.map(x => Identity.fromJson(x)); 21 | if(json.hasOwnProperty('permissions')) p.permissions = json.permissions.map(x => Permission.fromJson(x)); 22 | return p; 23 | } 24 | 25 | clone(){ return Keychain.fromJson(JSON.parse(JSON.stringify(this))) } 26 | 27 | removePermissionsByKeypair(keypair){ this.permissions = this.permissions.filter(perm => perm.keypair !== keypair.unique()); } 28 | removePermission(permission){ this.permissions = this.permissions.filter(perm => perm.checksum !== permission.checksum); } 29 | getPermission(checksum){ return this.permissions.find(permission => permission.checksum === checksum); } 30 | hasPermission(checksum, fields = []){ 31 | const fieldKeys = () => Array.isArray(fields) ? fields : Object.keys(fields); 32 | 33 | const permission = this.getPermission(checksum); 34 | console.log('checksum', checksum, permission); 35 | if(!permission) return false; 36 | 37 | // If no fields are supplied but permission exists | valid. 38 | if(fields === null || !fieldKeys().length) return true; 39 | 40 | let fieldsCloneA = Object.assign({}, fields); 41 | let fieldsCloneB = Object.assign({}, permission.fields); 42 | permission.mutableFields.map(field => { 43 | delete fieldsCloneA[field]; 44 | delete fieldsCloneB[field]; 45 | }); 46 | 47 | return ObjectHelpers.deepEqual(fieldsCloneA, fieldsCloneB); 48 | 49 | } 50 | 51 | findIdentity(publicKey){ return this.identities.find(id => id.publicKey === publicKey); } 52 | findIdentityFromDomain(domain){ 53 | const idFromPermissions = this.permissions.find(permission => permission.isIdentityOnly() && permission.domain === domain); 54 | if(idFromPermissions) return this.findIdentity(idFromPermissions.identity); 55 | else return null; 56 | } 57 | updateOrPushIdentity(identity){ 58 | this.identities.find(id => id.publicKey === identity.publicKey) 59 | ? this.identities = this.identities.map(id => id.publicKey === identity.publicKey ? identity : id) 60 | : this.identities.unshift(identity); 61 | } 62 | 63 | findAccountsWithPublicKey(publicKey){ 64 | return this.identities.map(id => id.getAccountFromPublicKey(publicKey)).filter(acc => !!acc); 65 | } 66 | 67 | forBackup(){ 68 | const clone = this.clone(); 69 | clone.keypairs = []; 70 | clone.permissions = []; 71 | return clone; 72 | 73 | } 74 | 75 | getKeyPair(keypair){ 76 | return this.getKeyPairByPublicKey(keypair.publicKey); 77 | // return this.keypairs.find(key => key.publicKey.toLowerCase() === keypair.publicKey.toLowerCase()) 78 | } 79 | 80 | getKeyPairByName(name){ 81 | return this.keypairs.find(key => key.name.toLowerCase() === name.toLowerCase()) 82 | } 83 | 84 | getKeyPairByPublicKey(publicKey){ 85 | return this.keypairs.find(key => key.publicKey.toLowerCase() === publicKey.toLowerCase()) 86 | } 87 | 88 | removeKeyPair(keypair){ 89 | this.keypairs = this.keypairs.filter(key => key.unique() !== keypair.unique()); 90 | } 91 | } -------------------------------------------------------------------------------- /src/models/Meta.js: -------------------------------------------------------------------------------- 1 | import {apis} from '../util/BrowserApis'; 2 | 3 | const extension = (apis && apis.app && typeof apis.app.getDetails === 'function') ? apis.app.getDetails() : {}; 4 | export default class Meta { 5 | 6 | constructor(){ 7 | this.version = extension.version || ''; 8 | this.extensionId = extension.id || ''; 9 | this.lastVersion = '0'; 10 | this.acceptedTerms = false; 11 | } 12 | 13 | regenerateVersion(){ 14 | this.version = extension.version || ''; 15 | } 16 | 17 | needsUpdating(){ 18 | return this.version !== this.lastVersion; 19 | } 20 | 21 | static placeholder(){ return new Meta(); } 22 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 23 | } -------------------------------------------------------------------------------- /src/models/Network.js: -------------------------------------------------------------------------------- 1 | import {Blockchains} from './Blockchains'; 2 | 3 | export default class Network { 4 | constructor(_name = '', _protocol = 'https', _host = '', _port = 0, blockchain = Blockchains.EOS, chainId = ''){ 5 | this.name = _name; 6 | this.protocol = _protocol; 7 | this.host = _host; 8 | this.port = _port; 9 | this.blockchain = blockchain; 10 | this.chainId = chainId.toString(); 11 | } 12 | 13 | static placeholder(){ return new Network(); } 14 | 15 | static fromJson(json){ 16 | const p = Object.assign(Network.placeholder(), json); 17 | p.chainId = p.chainId ? p.chainId.toString() : ''; 18 | return p; 19 | } 20 | 21 | static fromUnique(netString){ 22 | const blockchain = netString.split(':')[0]; 23 | if(netString.indexOf(':chain:') > -1) 24 | return new Network('', '', '','',blockchain, netString.replace(`${blockchain}:chain:`,'')); 25 | 26 | const splits = netString.replace(`${blockchain}:`, '').split(':'); 27 | return new Network('', '', splits[0], parseInt(splits[1] || 80), blockchain) 28 | } 29 | 30 | unique(){ return (`${this.blockchain}:` + (this.chainId.length ? `chain:${this.chainId}` : `${this.host}:${this.port}`)).toLowerCase(); } 31 | hostport(){ return `${this.host}${this.port ? ':' : ''}${this.port}` } 32 | fullhost(){ return `${this.protocol}://${this.host}${this.port ? ':' : ''}${this.port}` } 33 | clone(){ return Network.fromJson(JSON.parse(JSON.stringify(this))) } 34 | isEmpty(){ return !this.host.length; } 35 | isValid(){ return (this.host.length && this.port) || this.chainId.length } 36 | } -------------------------------------------------------------------------------- /src/models/Permission.js: -------------------------------------------------------------------------------- 1 | import Network from './Network'; 2 | import Identity from './Identity'; 3 | 4 | export default class Permission { 5 | 6 | constructor(){ 7 | // Mandatory 8 | this.domain = ''; 9 | this.network = ''; 10 | this.identity = ''; 11 | this.keypair = ''; 12 | 13 | // Optional 14 | this.contract = null; 15 | this.action = null; 16 | this.checksum = null; 17 | 18 | this.timestamp = 0; 19 | 20 | this.fields = []; 21 | this.mutableFields = []; 22 | } 23 | 24 | static placeholder(){ return new Permission(); } 25 | static fromJson(json){ 26 | let p = Object.assign(this.placeholder(), json); 27 | if(json.hasOwnProperty('network')) p.network = Network.fromJson(json.network); 28 | return p; 29 | } 30 | 31 | getIdentity(keychain){ 32 | return keychain.findIdentity(this.identity); 33 | } 34 | 35 | isIdentityOnly(){ 36 | return !this.contract && !this.action 37 | } 38 | 39 | isContractAction(){ 40 | return !this.isIdentityOnly() && this.contract.length && this.action.length 41 | } 42 | 43 | isIdentityFor(domain){ 44 | return this.isIdentityOnly() && this.domain === domain; 45 | } 46 | } -------------------------------------------------------------------------------- /src/models/Scatter.js: -------------------------------------------------------------------------------- 1 | import Meta from './Meta'; 2 | import Keychain from './Keychain'; 3 | import Settings from './Settings'; 4 | import AES from 'aes-oop'; 5 | import Hasher from '../util/Hasher' 6 | import IdGenerator from '../util/IdGenerator' 7 | 8 | export default class Scatter { 9 | 10 | constructor(){ 11 | this.meta = Meta.placeholder(); 12 | this.keychain = Keychain.placeholder(); 13 | this.settings = Settings.placeholder(); 14 | this.histories = []; 15 | this.hash = Hasher.insecureHash(IdGenerator.text(2048)); 16 | } 17 | 18 | static placeholder(){ return new Scatter(); } 19 | static fromJson(json){ 20 | let p = Object.assign(this.placeholder(), json); 21 | if(json.hasOwnProperty('meta')) p.meta = Meta.fromJson(json.meta); 22 | if(json.hasOwnProperty('settings')) p.settings = Settings.fromJson(json.settings); 23 | if(json.hasOwnProperty('keychain')) 24 | p.keychain = (typeof json.keychain === 'string') 25 | ? json.keychain : Keychain.fromJson(json.keychain); 26 | 27 | return p; 28 | } 29 | 30 | clone(){ return Scatter.fromJson(JSON.parse(JSON.stringify(this))) } 31 | 32 | isEncrypted(){ 33 | return typeof this.keychain !== 'object' 34 | } 35 | 36 | /*** 37 | * Encrypts the entire keychain 38 | * @param seed - The seed to encrypt with 39 | */ 40 | decrypt(seed){ 41 | if(this.isEncrypted()) this.keychain = Keychain.fromJson(AES.decrypt(this.keychain, seed)); 42 | } 43 | 44 | /*** 45 | * Decrypts the entire keychain 46 | * @param seed - The seed to decrypt with 47 | */ 48 | encrypt(seed){ 49 | if(!this.isEncrypted()) this.keychain = AES.encrypt(this.keychain, seed); 50 | } 51 | 52 | forBackup(){ 53 | const clone = this.clone(); 54 | clone.histories = []; 55 | return clone; 56 | } 57 | } -------------------------------------------------------------------------------- /src/models/Settings.js: -------------------------------------------------------------------------------- 1 | import Network from './Network'; 2 | import TimingHelpers from '../util/TimingHelpers'; 3 | import {LANG} from '../localization/locales'; 4 | 5 | export default class Settings { 6 | 7 | constructor(){ 8 | this.networks = []; 9 | this.hasEncryptionKey = false; 10 | this.inactivityInterval = 0; 11 | this.language = 'ENGLISH'; 12 | } 13 | 14 | static placeholder(){ return new Settings(); } 15 | static fromJson(json){ 16 | let p = Object.assign(this.placeholder(), json); 17 | if(json.hasOwnProperty('networks')) p.networks = json.networks.map(x => Network.fromJson(x)); 18 | return p; 19 | } 20 | } -------------------------------------------------------------------------------- /src/models/alerts/AlertTypes.js: -------------------------------------------------------------------------------- 1 | export const Error = 'error'; 2 | export const Prompt = 'prompt'; 3 | 4 | export const NamedAccount = 'named_account'; 5 | export const SelectAccount = 'select_authority'; 6 | export const ClaimIdentity = 'claim_identity'; 7 | 8 | -------------------------------------------------------------------------------- /src/models/errors/Error.js: -------------------------------------------------------------------------------- 1 | import * as ErrorTypes from './ErrorTypes' 2 | 3 | export const ErrorCodes = { 4 | NO_SIGNATURE:402, 5 | FORBIDDEN:403, 6 | TIMED_OUT:408, 7 | LOCKED:423, 8 | UPGRADE_REQUIRED:426, 9 | TOO_MANY_REQUESTS:429 10 | }; 11 | 12 | export default class Error { 13 | 14 | constructor(_type, _message, _code = ErrorCodes.LOCKED){ 15 | this.type = _type; 16 | this.message = _message; 17 | this.code = _code; 18 | this.isError = true; 19 | } 20 | 21 | static locked(){ 22 | return new Error(ErrorTypes.LOCKED, "The user's Scatter is locked. They have been notified and should unlock before continuing.") 23 | } 24 | 25 | static promptClosedWithoutAction(){ 26 | return new Error(ErrorTypes.PROMPT_CLOSED, "The user closed the prompt without any action.", ErrorCodes.TIMED_OUT) 27 | } 28 | 29 | static maliciousEvent(){ 30 | return new Error(ErrorTypes.MALICIOUS, "Malicious event discarded.", ErrorCodes.FORBIDDEN) 31 | } 32 | 33 | static signatureError(_type, _message){ 34 | return new Error(_type, _message, ErrorCodes.NO_SIGNATURE) 35 | } 36 | 37 | static requiresUpgrade(){ 38 | return new Error(ErrorTypes.UPGRADE_REQUIRED, "The required version is newer than the User's Scatter", ErrorCodes.UPGRADE_REQUIRED) 39 | } 40 | 41 | static identityMissing(){ 42 | return this.signatureError("identity_missing", "Identity no longer exists on the user's keychain"); 43 | } 44 | 45 | static signatureAccountMissing(){ 46 | return this.signatureError("account_missing", "Missing required accounts, repull the identity"); 47 | } 48 | 49 | static malformedRequiredFields(){ 50 | return this.signatureError("malformed_requirements", "The requiredFields you passed in were malformed"); 51 | } 52 | 53 | static noNetwork(){ 54 | return this.signatureError("no_network", "You must bind a network first"); 55 | } 56 | 57 | static usedKeyProvider(){ 58 | return new Error( 59 | ErrorTypes.MALICIOUS, 60 | "Do not use a `keyProvider` with a Scatter. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.", 61 | ErrorCodes.NO_SIGNATURE 62 | ) 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/models/errors/ErrorTypes.js: -------------------------------------------------------------------------------- 1 | export const MALICIOUS = 'malicious'; 2 | export const LOCKED = 'locked'; 3 | export const PROMPT_CLOSED = 'prompt_closed'; 4 | export const UPGRADE_REQUIRED = 'upgrade_required'; 5 | -------------------------------------------------------------------------------- /src/models/histories/HistoricEvent.js: -------------------------------------------------------------------------------- 1 | 2 | export default class HistoricEvent { 3 | 4 | constructor(_type = '', _data = null){ 5 | this.type = _type; 6 | this.data = _data; 7 | this.timestamp = + new Date(); 8 | } 9 | 10 | static placeholder(){ return new HistoricEvent(); } 11 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 12 | } -------------------------------------------------------------------------------- /src/models/histories/HistoricEventTypes.js: -------------------------------------------------------------------------------- 1 | export const PROVIDED_IDENTITY = "Provided Identity"; 2 | export const SIGNED_TRANSACTION = "Signed Transaction"; -------------------------------------------------------------------------------- /src/models/prompts/Prompt.js: -------------------------------------------------------------------------------- 1 | import {RouteNames, promptPrefix} from '../../vue/Routing' 2 | import * as PromptTypes from './PromptTypes' 3 | 4 | export default class Prompt { 5 | 6 | constructor(_type = '', _domain = '', _network = null, _data = {}, _responder = null){ 7 | this.type = _type; 8 | this.domain = _domain; 9 | this.network = _network; 10 | this.data = _data; 11 | this.responder = _responder; 12 | } 13 | 14 | static placeholder(){ return new Prompt(); } 15 | static fromJson(json){ return Object.assign(this.placeholder(), json); } 16 | 17 | routeName(){ 18 | return `${promptPrefix}${this.type}`; 19 | } 20 | 21 | static scatterIsLocked(){ 22 | return new Prompt(PromptTypes.REQUEST_UNLOCK, '', {host:'',port:0}, {}, function(){}); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/models/prompts/PromptTypes.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_IDENTITY = 'requestIdentity'; 2 | export const REQUEST_SIGNATURE = 'requestSignature'; 3 | export const REQUEST_ARBITRARY_SIGNATURE = 'signatureArbitrary'; 4 | export const REQUEST_ADD_NETWORK = 'requestAddNetwork'; 5 | export const REQUEST_UNLOCK = 'requestUnlock'; 6 | export const UPDATE_VERSION = 'updateVersion'; -------------------------------------------------------------------------------- /src/plugins/Plugin.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Plugin { 3 | 4 | constructor(_name = '', _type = ''){ 5 | this.name = _name; 6 | this.type = _type; 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /src/plugins/PluginRepository.js: -------------------------------------------------------------------------------- 1 | import * as PluginTypes from './PluginTypes'; 2 | import EOS from './defaults/eos'; 3 | import ETH from './defaults/eth'; 4 | 5 | /*** 6 | * Setting up for plugin based generators, 7 | * this will add more blockchain compatibility in the future. 8 | */ 9 | 10 | class PluginRepositorySingleton { 11 | 12 | constructor(){ 13 | this.plugins = []; 14 | this.loadPlugins(); 15 | } 16 | 17 | loadPlugins(){ 18 | this.plugins.push(new EOS()); 19 | this.plugins.push(new ETH()); 20 | } 21 | 22 | signatureProviders(){ 23 | return this.plugins.filter(plugin => plugin.type === PluginTypes.BLOCKCHAIN_SUPPORT); 24 | } 25 | 26 | supportedBlockchains(){ 27 | return this.signatureProviders().map(plugin => name) 28 | } 29 | 30 | plugin(name){ 31 | return this.plugins.find(plugin => plugin.name === name); 32 | } 33 | 34 | async endorsedNetworks(){ 35 | return await Promise.all(this.signatureProviders().map(async plugin => await plugin.getEndorsedNetwork())); 36 | } 37 | } 38 | 39 | const PluginRepository = new PluginRepositorySingleton(); 40 | export default PluginRepository; -------------------------------------------------------------------------------- /src/plugins/PluginTypes.js: -------------------------------------------------------------------------------- 1 | export const BLOCKCHAIN_SUPPORT = 'blockchain_support'; -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | import VueInitializer from './vue/VueInitializer'; 2 | import {Routing} from './vue/Routing'; 3 | import * as Actions from './store/constants' 4 | import {RouteNames} from './vue/Routing' 5 | 6 | import NavbarComponent from './components/NavbarComponent.vue' 7 | import InputComponent from './components/InputComponent.vue' 8 | import ButtonComponent from './components/ButtonComponent.vue' 9 | import SearchComponent from './components/SearchComponent.vue' 10 | import SelectComponent from './components/SelectComponent.vue' 11 | import NavActionsComponent from './components/NavActionsComponent.vue' 12 | import ViewBase from './views/ViewBase.vue' 13 | import Alert from './components/alerts/Alert.vue' 14 | 15 | class Popup { 16 | 17 | constructor(){ 18 | const components = [ 19 | {tag:'navbar', vue:NavbarComponent}, 20 | {tag:'nav-actions', vue:NavActionsComponent}, 21 | {tag:'cin', vue:InputComponent}, 22 | {tag:'btn', vue:ButtonComponent}, 23 | {tag:'search', vue:SearchComponent}, 24 | {tag:'sel', vue:SelectComponent}, 25 | {tag:'alert', vue:Alert}, 26 | {tag:'view-base', vue:ViewBase}, 27 | ]; 28 | 29 | const routes = Routing.routes(); 30 | 31 | const middleware = (to, next, store) => { 32 | if(Routing.isRestricted(to.name)) 33 | store.dispatch(Actions.IS_UNLOCKED) 34 | .then(unlocked => (unlocked) ? next() : next({name:RouteNames.ENTRY})); 35 | else next(); 36 | }; 37 | 38 | new VueInitializer(routes, components, middleware, (router, store) => { 39 | store.dispatch(Actions.IS_UNLOCKED) 40 | .then(unlocked => { 41 | if(!store.getters.meta.hasOwnProperty('acceptedTerms') || !store.getters.meta.acceptedTerms) 42 | router.push({name: RouteNames.ONBOARDING}); 43 | else router.push({name: RouteNames.MAIN_MENU}); 44 | }); 45 | }); 46 | } 47 | 48 | } 49 | 50 | const popup = new Popup(); 51 | -------------------------------------------------------------------------------- /src/prompt.js: -------------------------------------------------------------------------------- 1 | import VueInitializer from './vue/VueInitializer'; 2 | import {Routing} from './vue/Routing'; 3 | import * as Actions from './store/constants' 4 | import {RouteNames} from './vue/Routing' 5 | import Prompt from './models/prompts/Prompt' 6 | import PromptBase from './prompts/PromptBase.vue' 7 | import Network from './models/Network' 8 | 9 | import * as PromptTypes from './models/prompts/PromptTypes' 10 | import Alert from './components/alerts/Alert.vue' 11 | import ButtonComponent from './components/ButtonComponent.vue' 12 | import SearchComponent from './components/SearchComponent.vue' 13 | import InputComponent from './components/InputComponent.vue' 14 | import SelectComponent from './components/SelectComponent.vue' 15 | import KeyValue from './components/KeyValue.vue' 16 | import InternalMessage from './messages/InternalMessage' 17 | import * as InternalMessageTypes from './messages/InternalMessageTypes' 18 | import {apis} from './util/BrowserApis'; 19 | 20 | class PromptWindow { 21 | 22 | constructor(){ 23 | let prompt = window.data || apis.extension.getBackgroundPage().notification || null; 24 | 25 | // TODO: Pair prompt with a checksum from the state store so that 26 | // even if an attacker manages to open a clickjack/malicious prompt 27 | // we will be able to identify and reject it. 28 | 29 | // TODO: Error handling for prompt messages 30 | 31 | prompt = Prompt.fromJson(prompt); 32 | 33 | const components = [ 34 | {tag:'prompt-base', vue:PromptBase}, 35 | {tag:'btn', vue:ButtonComponent}, 36 | {tag:'search', vue:SearchComponent}, 37 | {tag:'cin', vue:InputComponent}, 38 | {tag:'sel', vue:SelectComponent}, 39 | {tag:'alert', vue:Alert}, 40 | {tag:'key-value', vue:KeyValue}, 41 | ]; 42 | 43 | const routes = Routing.routes(true); 44 | const middleware = (to, next, store) => next(); 45 | 46 | new VueInitializer(routes, components, middleware, (router, store) => { 47 | store.dispatch(Actions.PUSH_PROMPT, prompt); 48 | router.push({name:prompt.routeName()}); 49 | }); 50 | } 51 | 52 | } 53 | 54 | const popup = new PromptWindow(); 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/prompts/PromptBase.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | 50 | -------------------------------------------------------------------------------- /src/prompts/RequestAddNetwork.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 71 | 72 | -------------------------------------------------------------------------------- /src/prompts/RequestArbitrarySignaturePrompt.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 89 | 90 | -------------------------------------------------------------------------------- /src/prompts/RequestUnlock.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 44 | 45 | -------------------------------------------------------------------------------- /src/prompts/UpdateVersion.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 44 | 45 | -------------------------------------------------------------------------------- /src/services/AccountService.js: -------------------------------------------------------------------------------- 1 | import Account from '../models/Account' 2 | import PluginRepository from '../plugins/PluginRepository' 3 | 4 | // TODO: Only dependence on eosjs 5 | import * as Eos from 'eosjs'; 6 | 7 | export default class AccountService { 8 | 9 | /*** 10 | * Tries to import an account using an existing private key 11 | * @param keypair 12 | * @param network 13 | * @param context 14 | * @returns {Promise} 15 | */ 16 | static importFromKey(keypair, network, context){ 17 | return new Promise((resolve, reject) => { 18 | const accountSelected = (account) => resolve({keypair, account}); 19 | 20 | // Accounts for this blockchain need importation 21 | if(PluginRepository.plugin(network.blockchain).accountsAreImported()) 22 | PluginRepository.plugin(network.blockchain).importAccount(keypair, network, context, accountSelected); 23 | 24 | // Accounts for this blockchain are freebased. 25 | else accountSelected(Account.fromJson({ 26 | name:keypair.name, 27 | authority:'', 28 | publicKey:keypair.publicKey, 29 | keypairUnique:keypair.unique() 30 | })) 31 | 32 | 33 | }) 34 | } 35 | } -------------------------------------------------------------------------------- /src/services/AuthenticationService.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../store/constants'; 2 | import {RouteNames} from '../vue/Routing' 3 | import AlertMsg from '../models/alerts/AlertMsg' 4 | import Mnemonic from '../util/Mnemonic' 5 | import Hasher from '../util/Hasher'; 6 | import IdGenerator from '../util/IdGenerator' 7 | import StorageService from './StorageService'; 8 | 9 | export default class AuthenticationService { 10 | 11 | /*** 12 | * Verifies if the password provided matches the decryption seed 13 | * @param password 14 | * @param context 15 | */ 16 | static verifyPassword(password, context){ 17 | return new Promise((resolve, reject) => { 18 | const sendToEntry = () => { 19 | // context[Actions.PUSH_ALERT](AlertMsg.WrongPassword()); 20 | context[Actions.PUSH_ALERT](AlertMsg.WrongPassword()); 21 | context.$router.push({name:RouteNames.ENTRY}); 22 | reject(); 23 | }; 24 | 25 | // We're going to immediately take a wrong password here as a sign 26 | // of hostility and lock Scatter by overwriting the seed. 27 | context[Actions.SET_SEED](password).then(() => { 28 | context[Actions.IS_UNLOCKED]().then(unlocked => { 29 | if(!unlocked) { sendToEntry(); return false; } 30 | resolve(); 31 | }).catch(() => sendToEntry()) 32 | }).catch(() => sendToEntry()); 33 | }) 34 | } 35 | 36 | /*** 37 | * Changes a user's password. 38 | * -------------------------- 39 | * !!IMPORTANT!! 40 | * Should only be done after verification of the 41 | * old password or it will corrupt all private keys. 42 | * -------------------------- 43 | * @param oldPassword 44 | * @param newPassword 45 | * @param context 46 | */ 47 | static async changePassword(oldPassword, newPassword, context){ 48 | const [oldMnemonic, oldSeed] = await Mnemonic.generateMnemonic(oldPassword); 49 | 50 | // Setting a new salt every time the password is changed. 51 | await StorageService.setSalt(Hasher.insecureHash(IdGenerator.text(32))); 52 | 53 | const [newMnemonic, newSeed] = await Mnemonic.generateMnemonic(newPassword); 54 | 55 | const scatter = context.scatter.clone(); 56 | context[Actions.SET_MNEMONIC](newMnemonic); 57 | context[Actions.SET_SEED](newPassword).then(() => { 58 | // Replacing private key encryption 59 | scatter.keychain.keypairs.map(keypair => keypair.decrypt(oldSeed)); 60 | 61 | context[Actions.UPDATE_STORED_SCATTER](scatter).then(() => { 62 | context.$router.push({name:RouteNames.SHOW_MNEMONIC}); 63 | }) 64 | }); 65 | } 66 | 67 | /*** 68 | * Checks if a password is valid 69 | * @param password 70 | * @param confirmation 71 | * @param context 72 | * @returns {boolean} 73 | */ 74 | static validPassword(password, confirmation, context){ 75 | if(password.length < 8){ 76 | context[Actions.PUSH_ALERT](AlertMsg.BadPassword()); 77 | return false; 78 | } 79 | 80 | if(password !== confirmation){ 81 | context[Actions.PUSH_ALERT](AlertMsg.PasswordsDoNotMatch()); 82 | return false; 83 | } 84 | 85 | return true; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/services/IdentityService.js: -------------------------------------------------------------------------------- 1 | import Identity from '../models/Identity' 2 | import Prompt from '../models/prompts/Prompt'; 3 | import * as PromptTypes from '../models/prompts/PromptTypes' 4 | import NotificationService from '../services/NotificationService' 5 | 6 | export default class IdentityService { 7 | 8 | //TODO Mock 9 | static register(name, scatter){ 10 | return new Promise((resolve, reject) => { 11 | resolve(true) 12 | }) 13 | } 14 | 15 | //TODO Mock 16 | static nameExists(name, scatter){ 17 | return new Promise((resolve, reject) => { 18 | // Check if exists within another scatter 19 | resolve(false); 20 | }) 21 | } 22 | 23 | static identityPermission(domain, scatter){ 24 | return scatter.keychain.permissions.find(perm => perm.isIdentityFor(domain)); 25 | } 26 | 27 | static identityFromPermissionsOrNull(domain, scatter){ 28 | const identityFromPermission = IdentityService.identityPermission(domain, scatter); 29 | return identityFromPermission ? identityFromPermission.getIdentity(scatter.keychain) : null; 30 | } 31 | 32 | static getOrRequestIdentity(domain, fields, scatter, callback){ 33 | 34 | // Possibly getting an Identity that has been synced with this application. 35 | const identityFromPermission = IdentityService.identityFromPermissionsOrNull(domain, scatter); 36 | let identity = identityFromPermission; 37 | 38 | const sendBackIdentity = id => { 39 | if(!id || id.hasOwnProperty('isError')){ 40 | callback(null, null); 41 | return false; 42 | } 43 | 44 | callback(id.asOnlyRequiredFields(fields), !!identityFromPermission); 45 | }; 46 | 47 | if(identity){ 48 | // Even though there is a previous permission, 49 | // the identity might have changed and no longer 50 | // meets the requirements. 51 | if(identity.hasRequiredFields(fields)){ 52 | sendBackIdentity(identity); 53 | return false; 54 | } else { 55 | // TODO: Remove permission 56 | } 57 | } 58 | else NotificationService.open(new Prompt(PromptTypes.REQUEST_IDENTITY, domain, null, fields, sendBackIdentity)); 59 | } 60 | } -------------------------------------------------------------------------------- /src/services/KeyPairService.js: -------------------------------------------------------------------------------- 1 | import {BlockchainsArray, Blockchains} from '../models/Blockchains'; 2 | import PluginRepository from '../plugins/PluginRepository' 3 | import AlertMsg from '../models/alerts/AlertMsg' 4 | import * as Actions from '../store/constants'; 5 | 6 | export default class KeyPairService { 7 | 8 | /*** 9 | * Tries to make a keypair in place from a private key 10 | * @param keypair 11 | * @returns {Promise.} 12 | */ 13 | static async makePublicKey(keypair){ 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | if(keypair.privateKey.length < 50) { 17 | resolve(false); 18 | return false; 19 | } 20 | 21 | let publicKey = ''; 22 | 23 | BlockchainsArray.map(blockchainKV => { 24 | try { 25 | if(!publicKey.length) { 26 | const blockchain = blockchainKV.value; 27 | 28 | const plugin = PluginRepository.plugin(blockchain); 29 | if (plugin && plugin.validPrivateKey(keypair.privateKey)) { 30 | publicKey = plugin.privateToPublic(keypair.privateKey); 31 | keypair.blockchain = blockchain; 32 | } 33 | } 34 | } catch(e){} 35 | }); 36 | 37 | if(publicKey) keypair.publicKey = publicKey; 38 | resolve(true); 39 | },100) 40 | }) 41 | } 42 | 43 | static async generateKeyPair(keypair){ 44 | const plugin = PluginRepository.plugin(keypair.blockchain); 45 | if(!plugin) return false; 46 | 47 | plugin.randomPrivateKey().then(privateKey => { 48 | const publicKey = plugin.privateToPublic(privateKey); 49 | if(plugin.validPublicKey(publicKey) && plugin.validPrivateKey(privateKey)){ 50 | keypair.publicKey = publicKey; 51 | keypair.privateKey = privateKey; 52 | } 53 | }); 54 | 55 | return true; 56 | } 57 | 58 | static saveKeyPair(keypair, context, callback){ 59 | if(!keypair.name.length) return context[Actions.PUSH_ALERT](AlertMsg.BadKeyPairName()); 60 | 61 | const scatter = context.scatter.clone(); 62 | if(scatter.keychain.getKeyPair(keypair)) 63 | return context[Actions.PUSH_ALERT](AlertMsg.KeyPairExists()); 64 | if(scatter.keychain.getKeyPairByName(keypair.name)) 65 | return context[Actions.PUSH_ALERT](AlertMsg.KeyPairExists()); 66 | 67 | scatter.keychain.keypairs.push(keypair); 68 | context[Actions.UPDATE_STORED_SCATTER](scatter).then(() => callback()); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/services/NotificationService.js: -------------------------------------------------------------------------------- 1 | import Error from '../models/errors/Error' 2 | import {apis} from '../util/BrowserApis'; 3 | import InternalMessage from '../messages/InternalMessage' 4 | import * as InternalMessageTypes from '../messages/InternalMessageTypes' 5 | 6 | let openWindow = null; 7 | 8 | export default class NotificationService { 9 | 10 | /*** 11 | * Opens a prompt window outside of the extension 12 | * @param notification 13 | */ 14 | static async open(notification){ 15 | if(openWindow){ 16 | // For now we're just going to close the window to get rid of the error 17 | // that is caused by already open windows swallowing all further requests 18 | openWindow.close(); 19 | openWindow = null; 20 | 21 | // Alternatively we could focus the old window, but this would cause 22 | // urgent 1-time messages to be lost, such as after dying in a game and 23 | // uploading a high-score. That message will be lost. 24 | // openWindow.focus(); 25 | // return false; 26 | 27 | // A third option would be to add a queue, but this could cause 28 | // virus-like behavior as apps overflow the queue causing the user 29 | // to have to quit the browser to regain control. 30 | } 31 | 32 | 33 | const height = 600; 34 | const width = 700; 35 | let middleX = window.screen.availWidth/2 - (width/2); 36 | let middleY = window.screen.availHeight/2 - (height/2); 37 | 38 | const getPopup = async () => { 39 | try { 40 | const url = apis.runtime.getURL('/prompt.html'); 41 | 42 | // Notifications get bound differently depending on browser 43 | // as Firefox does not support opening windows from background. 44 | if(typeof browser !== 'undefined') { 45 | const created = await apis.windows.create({ 46 | url, 47 | height, 48 | width, 49 | type:'popup' 50 | }); 51 | 52 | window.notification = notification; 53 | return created; 54 | } 55 | else { 56 | const win = window.open(url, 'ScatterPrompt', `width=${width},height=${height},resizable=0,top=${middleY},left=${middleX},titlebar=0`); 57 | win.data = notification; 58 | openWindow = win; 59 | return win; 60 | } 61 | } catch (e) { 62 | console.log('notification error', e); 63 | return null; 64 | } 65 | } 66 | 67 | await InternalMessage.payload(InternalMessageTypes.SET_PROMPT, JSON.stringify(notification)).send(); 68 | 69 | let popup = await getPopup(); 70 | 71 | // Handles the user closing the popup without taking any action 72 | popup.onbeforeunload = () => { 73 | notification.responder(Error.promptClosedWithoutAction()); 74 | 75 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload 76 | // Must return undefined to bypass form protection 77 | openWindow = null; 78 | return undefined; 79 | }; 80 | 81 | } 82 | 83 | /*** 84 | * Always use this method for closing notification popups. 85 | * Otherwise you will double send responses and one will always be null. 86 | */ 87 | static async close(){ 88 | if(typeof browser !== 'undefined') { 89 | const {id: windowId,} = (await apis.windows.getCurrent()); 90 | apis.windows.remove(windowId); 91 | } else { 92 | window.onbeforeunload = () => {}; 93 | window.close(); 94 | } 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /src/services/RIDLService.js: -------------------------------------------------------------------------------- 1 | import ridl from 'ridl'; 2 | import * as Actions from '../store/constants'; 3 | import AlertMsg from '../models/alerts/AlertMsg' 4 | import PluginRepository from '../plugins/PluginRepository' 5 | import {Blockchains} from '../models/Blockchains' 6 | 7 | const enabled = false; 8 | 9 | setTimeout(() => { 10 | PluginRepository.plugin(Blockchains.EOS) 11 | .getEndorsedNetwork() 12 | .then(network => ridl.setNetwork(network)); 13 | }, 50); 14 | 15 | export default class RIDLService { 16 | 17 | constructor(){} 18 | 19 | static async claimIdentity(newName, identity, context){ 20 | return new Promise(async(resolve,reject) => { 21 | 22 | if(!newName.length) return reject(null); 23 | const hash = await ridl.identity.getHash(newName); 24 | if(!hash) return reject(context[Actions.PUSH_ALERT](AlertMsg.NoSuchIdentityName())); 25 | 26 | context[Actions.PUSH_ALERT](AlertMsg.ClaimIdentity(newName)).then(async res => { 27 | if(!res || !res.hasOwnProperty('text')) return reject(null); 28 | 29 | if(!PluginRepository.plugin(Blockchains.EOS).validPrivateKey(res.text)) 30 | return reject(context[Actions.PUSH_ALERT](AlertMsg.InvalidPrivateKey())); 31 | 32 | const signedHash = ridl.sign(hash, res.text); 33 | delete res.text; 34 | 35 | const claimed = await ridl.identity.claim(newName, signedHash, identity.publicKey); 36 | if(!claimed) return reject(context[Actions.PUSH_ALERT](AlertMsg.NoSuchIdentityName())); 37 | 38 | // Removing now unused randomized RIDL account 39 | if(!await ridl.identity.registered(identity.name)) { 40 | const previousHash = await ridl.identity.getHash(identity.name); 41 | const signedStaleHash = previousHash ? await context[Actions.SIGN_RIDL]({hash:previousHash, publicKey:identity.publicKey}) : false; 42 | if(signedStaleHash) await ridl.identity.release(identity.name, signedStaleHash); 43 | } 44 | 45 | identity.name = newName; 46 | identity.ridl = parseInt(claimed.registered); 47 | resolve(identity); 48 | //5KjbZQLH3EAfgXF3jejYM2WZjzJCUQH7NEkT1mVcBy2xoFdSWro 49 | }) 50 | 51 | }); 52 | 53 | 54 | } 55 | 56 | static async identify(publicKey){ 57 | if(!enabled) return ridl.identity.randomName(); 58 | const name = await ridl.identity.randomUniqueName(); 59 | if(!await ridl.identity.identify(name, publicKey)) return null; 60 | return name; 61 | } 62 | } -------------------------------------------------------------------------------- /src/services/StorageService.js: -------------------------------------------------------------------------------- 1 | import Scatter from '../models/Scatter' 2 | import {apis} from '../util/BrowserApis'; 3 | 4 | export default class StorageService { 5 | 6 | constructor(){} 7 | 8 | /*** 9 | * Saves an instance of Scatter in the extension's local storage 10 | * The keychain will always be encrypted when in storage 11 | * @param scatter 12 | * @returns {Promise} 13 | */ 14 | static save(scatter){ 15 | return new Promise(resolve => { 16 | apis.storage.local.set({scatter}, () => { 17 | resolve(scatter); 18 | }); 19 | }) 20 | }; 21 | 22 | /*** 23 | * Gets an instance of Scatter from the extension's local storage 24 | * Will return a new Scatter instance if not found. 25 | * @returns {Promise} 26 | */ 27 | static get() { 28 | return new Promise(resolve => { 29 | apis.storage.local.get('scatter', (possible) => { 30 | (possible && Object.keys(possible).length && possible.hasOwnProperty('scatter')) 31 | ? resolve(Scatter.fromJson(possible.scatter)) 32 | : resolve(Scatter.placeholder()); 33 | }); 34 | }) 35 | } 36 | 37 | /*** 38 | * Removes the instance of Scatter. 39 | * This will delete all user data. 40 | * @returns {Promise} 41 | */ 42 | static remove(){ 43 | return new Promise(resolve => { 44 | apis.storage.local.remove('scatter', () => { 45 | resolve(); 46 | }); 47 | }) 48 | } 49 | 50 | /*** 51 | * Caches an ABI 52 | * @param contractName 53 | * @param chainId 54 | * @param abi 55 | * @returns {Promise} 56 | */ 57 | static cacheABI(contractName, chainId, abi){ 58 | return new Promise(resolve => { 59 | apis.storage.local.set({[`abi:${contractName}:${chainId}`]:abi}, () => { 60 | resolve(abi); 61 | }); 62 | }); 63 | } 64 | 65 | /*** 66 | * Fetches an ABI from cache 67 | * @param contractName 68 | * @param chainId 69 | * @returns {Promise} 70 | */ 71 | static getABI(contractName, chainId){ 72 | return new Promise(resolve => { 73 | const prop = `abi:${contractName}:${chainId}`; 74 | apis.storage.local.get(prop, (possible) => { 75 | if(JSON.stringify(possible) !== '{}') resolve(possible[prop]); 76 | else resolve('no cache'); 77 | }); 78 | }) 79 | } 80 | 81 | static getSalt(){ 82 | return new Promise(resolve => { 83 | apis.storage.local.get('salt', (possible) => { 84 | if(JSON.stringify(possible) !== '{}') resolve(possible.salt); 85 | else resolve('SALT_ME'); 86 | }); 87 | }) 88 | } 89 | 90 | static setSalt(salt){ 91 | return new Promise(resolve => { 92 | apis.storage.local.set({salt}, () => { 93 | resolve(salt); 94 | }); 95 | }) 96 | } 97 | } -------------------------------------------------------------------------------- /src/store/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const LOAD_SCATTER = 'loadScatter'; 3 | export const SET_SCATTER = 'setScatter'; 4 | export const IS_UNLOCKED = 'isUnlocked'; 5 | export const UPDATE_STORED_SCATTER = 'updateStoredScatter'; 6 | export const CREATE_NEW_SCATTER = 'createNewScatter'; 7 | export const IMPORT_SCATTER = 'importScatter'; 8 | export const SET_MNEMONIC = 'setMnemonic'; 9 | export const SET_SEED = 'setSeed'; 10 | export const SET_AUTO_LOCK = 'setAutoLock'; 11 | export const LOCK = 'lock'; 12 | export const DESTROY = 'destroy'; 13 | export const SIGN_RIDL = 'signRIDL'; 14 | 15 | export const PUSH_ALERT = 'pushAlert'; 16 | export const PULL_ALERT = 'pullAlert'; 17 | export const PUSH_ALERT_RESULT = 'pushAlertResult'; 18 | export const CLEAR_ALERT_RESULT = 'clearAlertResult'; 19 | 20 | export const PUSH_PROMPT = 'pushPrompt'; 21 | export const PULL_PROMPT = 'pullPrompt'; -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as Mutations from './constants' 2 | import TimingHelpers from '../util/TimingHelpers'; 3 | 4 | export const mutations = { 5 | [Mutations.SET_SCATTER]:(state, scatter) => state.scatter = scatter, 6 | [Mutations.SET_MNEMONIC]:(state, mnemonic) => state.mnemonic = mnemonic, 7 | [Mutations.PUSH_ALERT]:(state, error) => state.alerts.push(error), 8 | [Mutations.PULL_ALERT]:(state, error) => state.alerts.shift(), 9 | [Mutations.PUSH_ALERT_RESULT]:(state, alertResult) => state.alertResult = alertResult, 10 | [Mutations.CLEAR_ALERT_RESULT]:(state) => state.alertResult = null, 11 | [Mutations.PUSH_PROMPT]:(state, prompt) => state.prompt = prompt, 12 | [Mutations.SET_AUTO_LOCK]:(state, inactivityInterval) => 13 | state.scatter.settings.inactivityInterval = TimingHelpers.minutes(inactivityInterval), 14 | }; -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex'; 3 | 4 | import {mutations} from './mutations'; 5 | import {actions} from './actions'; 6 | import * as LANG_KEYS from '../localization/keys'; 7 | 8 | import {IdentityRequiredFields} from '../models/Identity' 9 | 10 | Vue.use(Vuex); 11 | 12 | const state = { 13 | scatter:null, 14 | mnemonic:null, 15 | 16 | alerts:[], 17 | alertResult:null, 18 | 19 | prompt:null, 20 | }; 21 | 22 | const getters = { 23 | meta:state => state.scatter.meta, 24 | identities:state => state.scatter.keychain.identities, 25 | permissions:state => state.scatter.keychain.permissions, 26 | keypairs:state => state.scatter.keychain.keypairs, 27 | networks:state => state.scatter.settings.networks, 28 | histories:state => state.scatter.histories, 29 | autoLockInterval:state => state.scatter.settings.inactivityInterval, 30 | language:state => state.scatter.settings.language, 31 | 32 | // FOR PROMPTS ONLY 33 | identityFields:state => IdentityRequiredFields.fromJson(state.prompt.data), 34 | requiredFields:state => IdentityRequiredFields.fromJson(state.prompt.data.requiredFields || {}), 35 | messages:state => state.prompt.data.messages || [], 36 | }; 37 | 38 | export const store = new Vuex.Store({ 39 | state, 40 | getters, 41 | mutations, 42 | actions 43 | }) -------------------------------------------------------------------------------- /src/util/BrowserApis.js: -------------------------------------------------------------------------------- 1 | 2 | const swallow = fn => {try { fn() } catch(e){}}; 3 | 4 | class ApiGenerator { 5 | constructor(){ 6 | [ 7 | 'app', 8 | 'storage', 9 | 'extension', 10 | 'runtime', 11 | 'windows' 12 | ] 13 | .map(api => { 14 | if(typeof chrome !== 'undefined') swallow(() => {if(chrome[api]) this[api] = chrome[api]}); 15 | if(typeof browser !== 'undefined') swallow(() => {if(browser[api]) this[api] = browser[api]}); 16 | }); 17 | 18 | if(typeof browser !== 'undefined') swallow(() => {if (browser && browser.runtime) this.runtime = browser.runtime}); 19 | } 20 | } 21 | 22 | export const apis = new ApiGenerator(); -------------------------------------------------------------------------------- /src/util/ContractHelpers.js: -------------------------------------------------------------------------------- 1 | import Hasher from './Hasher' 2 | import ObjectHelpers from './ObjectHelpers' 3 | import {Blockchains} from '../models/Blockchains'; 4 | 5 | export default class ContractHelpers { 6 | 7 | static getContractAndActionNames(transaction){ 8 | return {contract:transaction.code, action:transaction.type}; 9 | } 10 | 11 | static contractActionChecksum(contract, action, domain, network){ 12 | return Hasher.insecureHash(JSON.stringify(Object.assign({}, 13 | {contract, action, domain, network}) 14 | )); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/util/EOSKeygen.js: -------------------------------------------------------------------------------- 1 | import KeyPair from '../models/KeyPair'; 2 | import Mnemonic from './Mnemonic'; 3 | import {PrivateKey} from 'eosjs-ecc'; 4 | 5 | export default class EOSKeygen { 6 | 7 | /*** 8 | * Generates a KeyPair 9 | * @returns {KeyPair} 10 | */ 11 | static generateKeys(){ 12 | let [mnemonic, seed] = Mnemonic.generateDanglingMnemonic(); 13 | let privateKey = EOSKeygen.generatePrivateKey(seed); 14 | let publicKey = EOSKeygen.privateToPublic(privateKey); 15 | return KeyPair.fromJson({publicKey, privateKey}) 16 | } 17 | 18 | /*** 19 | * Generates only a private key 20 | * @param seed - The seed to build the key from 21 | * @returns {wif} 22 | */ 23 | static generatePrivateKey(seed) { 24 | return PrivateKey.fromSeed(seed).toWif() 25 | } 26 | 27 | /*** 28 | * Converts a private key to a public key 29 | * @param privateKey - The private key to convert 30 | */ 31 | static privateToPublic(privateKey) { 32 | return PrivateKey.fromWif(privateKey).toPublic().toString() 33 | } 34 | 35 | /*** 36 | * Checks if a private key is a valid EOS private key 37 | * @param privateKey - The private key to check 38 | * @returns {boolean} 39 | */ 40 | static validPrivateKey(privateKey){ 41 | return PrivateKey.isValid(privateKey); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/util/GenericTools.js: -------------------------------------------------------------------------------- 1 | 2 | export const strippedHost = () => { 3 | let host = location.hostname; 4 | 5 | // Replacing www. only if the domain starts with it. 6 | if(host.indexOf('www.') === 0) host = host.replace('www.', ''); 7 | 8 | return host; 9 | }; -------------------------------------------------------------------------------- /src/util/Hasher.js: -------------------------------------------------------------------------------- 1 | const ecc = require('eosjs-ecc'); 2 | const scrypt = require('scrypt-async'); 3 | import StorageService from '../services/StorageService'; 4 | 5 | export default class Hasher { 6 | 7 | /*** 8 | * Hashes a cleartext using the SHA-256 algorithm. 9 | * This is INSECURE and should only be used for fingerprinting. 10 | * @param cleartext 11 | */ 12 | static insecureHash(cleartext) { 13 | return ecc.sha256(cleartext); 14 | } 15 | 16 | /*** 17 | * Hashes a cleartext using scrypt. 18 | * @param cleartext 19 | */ 20 | static async secureHash(cleartext) { 21 | return new Promise(async resolve => { 22 | // We don't need a salt here, because this is a non-saved(!) hash, 23 | // which is used to create a seed that is used to encrypt 24 | // the keychain using AES which has it's own salt. 25 | // const salt = this.insecureHash(cleartext) + this.insecureHash(cleartext).slice(0,16); 26 | const salt = await StorageService.getSalt(); 27 | scrypt(cleartext, salt, { 28 | N: 16384, 29 | r: 8, 30 | p: 1, 31 | dkLen: 16, 32 | encoding: 'hex' 33 | }, (derivedKey) => { 34 | resolve(derivedKey); 35 | }) 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /src/util/IdGenerator.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default class IdGenerator { 4 | 5 | static rand(){ 6 | const arr = new Uint32Array(1); 7 | window.crypto.getRandomValues(arr); 8 | return arr[0]/(0xffffffff + 1); 9 | } 10 | 11 | /*** 12 | * Generates a random string of specified size 13 | * @param size - The length of the string to generate 14 | * @returns {string} - The generated random string 15 | */ 16 | static text(size){ 17 | let text = ""; 18 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 19 | for(let i=0; i max ) return IdGenerator.numeric(max) + IdGenerator.numeric(size - max); 33 | 34 | max = Math.pow(10, size+add); 35 | const min = max / 10, 36 | number = Math.floor(IdGenerator.rand() * (max - min + 1)) + min; 37 | 38 | return ("" + number).substring(add); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/util/Mnemonic.js: -------------------------------------------------------------------------------- 1 | import Hasher from "./Hasher"; 2 | import bip39 from 'bip39' 3 | 4 | export class Mnemonic { 5 | 6 | /*** 7 | * Generates a mnemonic from a password 8 | * @param password 9 | * @returns {[string,string]} 10 | */ 11 | static async generateMnemonic(password) { 12 | const hash = await Hasher.secureHash(password); 13 | let mnemonic = bip39.entropyToMnemonic(hash); 14 | return [mnemonic, bip39.mnemonicToSeedHex(mnemonic)]; 15 | } 16 | 17 | static async mnemonicToSeed(mnemonic){ 18 | return bip39.mnemonicToSeedHex(mnemonic); 19 | } 20 | 21 | /*** 22 | * Generates a random mnemonic 23 | * @returns {[string,string]} 24 | */ 25 | static generateDanglingMnemonic() { 26 | let mnemonic = bip39.generateMnemonic(); 27 | return [mnemonic, bip39.mnemonicToSeedHex(mnemonic)]; 28 | } 29 | } 30 | 31 | export default Mnemonic; -------------------------------------------------------------------------------- /src/util/ObjectHelpers.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * A set of helpers for Objects/Arrays 3 | */ 4 | export default class ObjectHelpers { 5 | 6 | /*** 7 | * Groups an array by key 8 | * @param array 9 | * @param key 10 | * @returns {*} 11 | */ 12 | static groupBy(array, key){ 13 | return array.reduce((acc, item) => { 14 | (acc[item[key]] = acc[item[key]] || []).push(item); 15 | return acc; 16 | }, {}); 17 | } 18 | 19 | /*** 20 | * Makes a single level array distinct 21 | * @param array 22 | * @returns {*} 23 | */ 24 | static distinct(array){ 25 | return array.reduce((a,b) => (a.includes(b)) ? a : a.concat(b), []); 26 | } 27 | 28 | /*** 29 | * Makes an object array distinct ( uses deep checking ) 30 | * @param array 31 | * @returns {*} 32 | */ 33 | static distinctObjectArray(array){ 34 | return array.reduce((a,b) => (!!a.find(x => this.deepEqual(x, b))) ? a : a.concat(b), []); 35 | } 36 | 37 | /*** 38 | * Checks deep equality for objects 39 | * @param objA 40 | * @param objB 41 | * @returns {boolean} 42 | */ 43 | static deepEqual(objA, objB) { 44 | const keys = Object.keys, typeA = typeof objA, typeB = typeof objB; 45 | return objA && objB && typeA === 'object' && typeA === typeB ? ( 46 | keys(objA).length === keys(objB).length && 47 | keys(objA).every(key => this.deepEqual(objA[key], objB[key])) 48 | ) : (objA === objB); 49 | } 50 | 51 | /*** 52 | * Flattens an array into a single dimension 53 | * @param array 54 | * @returns {*} 55 | */ 56 | static flatten(array){ 57 | return array.reduce( 58 | (a, b) => a.concat(Array.isArray(b) ? this.flatten(b) : b), [] 59 | ); 60 | } 61 | 62 | /*** 63 | * Flattens an objects keys into a single dimension 64 | * @param object 65 | * @returns {*} 66 | */ 67 | static objectToFlatKeys(object){ 68 | return this.flatten(Object.keys(object).map(key => { 69 | if(object[key] !== null && typeof object[key] === 'object') return this.objectToFlatKeys(object[key]) 70 | else return key; 71 | })) 72 | } 73 | 74 | /*** 75 | * Gets a field from an object by string dot notation, such as `location.country.code` 76 | * @param object 77 | * @param dotNotation 78 | * @returns {*} 79 | */ 80 | static getFieldFromObjectByDotNotation(object, dotNotation){ 81 | let props = dotNotation.split("."); 82 | return props.reduce((obj,key)=> obj[key], object) 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /src/util/StringHelpers.js: -------------------------------------------------------------------------------- 1 | export default class StringHelpers { 2 | 3 | static camelToTitle(camel){ 4 | return camel 5 | .replace(/([A-Z])/g, (match) => ` ${match}`) 6 | .replace(/^./, (match) => match.toUpperCase()); 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /src/util/TimingHelpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A set of helpers for Timing operations 3 | */ 4 | export default class TimingHelpers { 5 | 6 | /*** 7 | * Returns the milliseconds that has passed since the given time (unix epoch) 8 | * @param time - past time in unix epoch format 9 | * @returns {number} - difference from now (unix epoch) 10 | */ 11 | static since(time) { 12 | return Date.now() - time; 13 | } 14 | 15 | /*** 16 | * Returns the milliseconds of the given minutes 17 | * @param min - minutes, integer format 18 | * @returns {number} - minutes in milliseconds 19 | */ 20 | static minutes(min) { 21 | return min * 60000; 22 | } 23 | 24 | /*** 25 | * Returns the minutes based on the given milliseconds 26 | * @param millis - milliseconds 27 | * @returns {number} - minutes, integer 28 | */ 29 | static minutesFromMillis(millis) { 30 | return millis / 60000; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/views/AutoLockView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 70 | 71 | -------------------------------------------------------------------------------- /src/views/BackupView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 84 | 85 | -------------------------------------------------------------------------------- /src/views/ChangePasswordView.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 71 | 72 | -------------------------------------------------------------------------------- /src/views/DestroyView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 71 | 72 | -------------------------------------------------------------------------------- /src/views/EntryView.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 85 | 86 | 99 | -------------------------------------------------------------------------------- /src/views/KeyPairsView.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 105 | 106 | -------------------------------------------------------------------------------- /src/views/LanguageView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/LoadFromBackup.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 76 | 77 | 101 | -------------------------------------------------------------------------------- /src/views/NetworkView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 100 | 101 | -------------------------------------------------------------------------------- /src/views/NetworksView.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 97 | 98 | -------------------------------------------------------------------------------- /src/views/PermissionsView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/SettingsView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | 35 | -------------------------------------------------------------------------------- /src/views/ShowMnemonicView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 57 | 58 | -------------------------------------------------------------------------------- /src/views/ViewBase.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 48 | 49 | -------------------------------------------------------------------------------- /src/vue/VueInitializer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import VueRouter from 'vue-router' 4 | import VueMoment from 'vue-moment'; 5 | import {Routing} from './Routing'; 6 | import {store} from '../store/store' 7 | import * as Actions from '../store/constants' 8 | import {localized} from '../localization/locales' 9 | import * as LANG_KEYS from '../localization/keys' 10 | 11 | /*** 12 | * Sets up an instance of Vue. 13 | * This is shared between the popup.js and prompt.js scripts. 14 | */ 15 | export default class VueInitializer { 16 | 17 | constructor(routes, 18 | components, 19 | middleware = () => {}, 20 | routerCallback = () => {}){ 21 | this.setupVuePlugins(); 22 | this.registerComponents(components); 23 | const router = this.setupRouting(routes, middleware); 24 | 25 | store.dispatch(Actions.LOAD_SCATTER).then(() => { 26 | 27 | Vue.mixin({ 28 | data(){ return { 29 | langKeys:LANG_KEYS 30 | }}, 31 | methods: { 32 | locale:(key) => localized(key, store.getters.language), 33 | } 34 | }) 35 | 36 | this.setupVue(router); 37 | 38 | routerCallback(router, store); 39 | }); 40 | 41 | return router; 42 | } 43 | 44 | setupVuePlugins(){ 45 | Vue.use(VueRouter); 46 | Vue.use(VueMoment); 47 | } 48 | 49 | registerComponents(components){ 50 | components.map(component => { 51 | Vue.component(component.tag, component.vue); 52 | }); 53 | } 54 | 55 | setupRouting(routes, middleware){ 56 | const router = new VueRouter({routes}); 57 | router.beforeEach((to, from, next) => middleware(to, next, store)); 58 | return router; 59 | } 60 | 61 | setupVue(router){ 62 | const app = new Vue({router, store}); 63 | app.$mount('#scatter'); 64 | 65 | // This removes the browser console's ability to 66 | // gain access to vuex store. ( for instance `scatter.__vue__.$store.state` ) 67 | document.getElementById('scatter').removeAttribute('id') 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /tests/helpers/chrome.js: -------------------------------------------------------------------------------- 1 | const chrome = { 2 | storage: { 3 | local: { 4 | stored:{}, 5 | set:function(keyobj, cb){ 6 | Object.keys(keyobj).map(key => chrome.storage.local.stored[key] = keyobj[key]) 7 | cb(true); 8 | }, 9 | get:function(key, cb){ 10 | cb( 11 | chrome.storage.local.stored.hasOwnProperty(key) ? 12 | {[key]:chrome.storage.local.stored[key]} 13 | : {} 14 | ) 15 | }, 16 | remove:function(key, cb){ 17 | delete chrome.storage.local.stored[key]; 18 | cb(true); 19 | } 20 | } 21 | }, 22 | runtime:{ 23 | id:'TEST', 24 | sendMessage:function(msg, cb){ 25 | this.onMessage.trigger(msg, cb) 26 | }, 27 | onMessage:{ 28 | messageQueue:[], 29 | trigger:function(data, cb) { this.messageQueue.push({data, cb}) }, 30 | addListener:function(cb){ 31 | setInterval(() => { 32 | if(this.messageQueue.length){ 33 | let message = this.messageQueue.pop(); 34 | cb(message.data, {id:'TEST'}, message.cb) 35 | } 36 | }, 100) 37 | } 38 | } 39 | } 40 | }; 41 | 42 | export default chrome; -------------------------------------------------------------------------------- /tests/localization/locales.spec.js: -------------------------------------------------------------------------------- 1 | import {locales} from '../../src/localization/locales' 2 | import chrome from '../helpers/chrome'; 3 | 4 | import Eos from 'eosjs'; 5 | 6 | import { expect, assert } from 'chai'; 7 | import 'mocha'; 8 | 9 | 10 | describe('Signature Request', async testDone => { 11 | 12 | global.chrome = chrome; 13 | 14 | it('should work', done => { 15 | console.log('locales', locales()); 16 | done(); 17 | }); 18 | 19 | 20 | 21 | }); -------------------------------------------------------------------------------- /tests/migrations/5.0.4.spec.js: -------------------------------------------------------------------------------- 1 | import Network from '../../src/models/Network' 2 | 3 | const {m5_0_4} = require('../../src/migrations/versions/5.0.4'); 4 | 5 | import { expect, assert } from 'chai'; 6 | import 'mocha'; 7 | 8 | describe('Identity', () => { 9 | 10 | it('should upgrade a scatter instances endorsed networks', done => { 11 | new Promise(async () => { 12 | const fakeScatter = { 13 | settings:{ 14 | networks:[ 15 | Network.fromJson({host:'nodes.get-scatter.com', port:80}), 16 | Network.fromJson({host:'tester.com', port:80}), 17 | Network.fromJson({host:'tester2.com', port:443}) 18 | ] 19 | } 20 | }; 21 | 22 | await m5_0_4(fakeScatter); 23 | 24 | assert(fakeScatter.settings.networks[0].name === 'EOS Mainnet', 'EOS Endorsed network was not named'); 25 | assert(fakeScatter.settings.networks[1].protocol === 'http', 'Port 80 got the wrong protocol'); 26 | assert(fakeScatter.settings.networks[2].protocol === 'https', 'Port 443 got the wrong protocol'); 27 | 28 | done(); 29 | }); 30 | }) 31 | }); -------------------------------------------------------------------------------- /tests/migrations/6.0.4.spec.js: -------------------------------------------------------------------------------- 1 | import Network from '../../src/models/Network' 2 | import PluginRepository from '../../src/plugins/PluginRepository'; 3 | import {Blockchains} from '../../src/models/Blockchains' 4 | 5 | const {m6_0_4} = require('../../src/migrations/versions/6.0.4'); 6 | 7 | import { expect, assert } from 'chai'; 8 | import 'mocha'; 9 | 10 | describe('Identity', () => { 11 | 12 | it('should upgrade a scatter instances endorsed networks', done => { 13 | new Promise(async () => { 14 | const eosEnd = await PluginRepository.plugin(Blockchains.EOS).getEndorsedNetwork(); 15 | const eodEndWrongPort = eosEnd; 16 | eosEnd.port = 80; 17 | const fakeScatter = { 18 | keychain:{ 19 | identities:[ 20 | { 21 | accounts:{ 22 | 'unlinkednetwork':{shouldBe:'deleted'}, 23 | [eosEnd.unique()]:{shouldBe:'left'} 24 | } 25 | } 26 | ] 27 | }, 28 | settings:{ 29 | networks:[eodEndWrongPort] 30 | } 31 | }; 32 | 33 | await m6_0_4(fakeScatter); 34 | 35 | assert(fakeScatter.settings.networks[0].port === 443, 'EOS Endorsed network had a bad port'); 36 | assert(Object.keys(fakeScatter.keychain.identities[0].accounts).length === 1, 'Too many accounts'); 37 | assert(JSON.stringify(fakeScatter.keychain.identities[0].accounts[eosEnd.unique()]) === JSON.stringify({shouldBe:'left'}), 'Did not delete the right accounts'); 38 | 39 | done(); 40 | }); 41 | }) 42 | }); -------------------------------------------------------------------------------- /tests/pk2pk.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import 'mocha'; 3 | 4 | import PluginRepository from '../src/plugins/PluginRepository'; 5 | import {Blockchains} from '../src/models/Blockchains' 6 | 7 | import ecc from 'eosjs-ecc'; 8 | import secp256k1 from 'secp256k1'; 9 | import utils from 'ethereumjs-util'; 10 | 11 | describe('Ethereum key to EOS key', () => { 12 | 13 | const eth = PluginRepository.plugin(Blockchains.ETH); 14 | const eos = PluginRepository.plugin(Blockchains.EOS); 15 | 16 | let ethPrivateKey = ''; 17 | let ethPublicKey = ''; 18 | let eosPublicKey = ''; 19 | 20 | it('should generate base keys', done => { 21 | new Promise(async(resolve, reject) => { 22 | ethPrivateKey = await eth.randomPrivateKey(); 23 | ethPublicKey = '0x'+utils.privateToPublic(utils.toBuffer(utils.addHexPrefix(ethPrivateKey))).toString('hex') 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should convert publicKeys', () => { 29 | let buffer = Buffer.from(ethPublicKey.slice(2), 'hex'), 30 | converted = secp256k1.publicKeyConvert(Buffer.concat([ Buffer.from([4]), buffer ]), true); 31 | eosPublicKey = ecc.PublicKey.fromBuffer(converted).toString(); 32 | assert(eos.validPublicKey(eosPublicKey), "Eos public key was not valid") 33 | }); 34 | 35 | it('should convert keys', () => { 36 | let ethBuffer = Buffer.from(ethPrivateKey, 'hex'); 37 | const p = ecc.PrivateKey.fromHex(ethBuffer); 38 | const pk = ecc.privateToPublic(p); 39 | assert(eosPublicKey === pk, "Converted public and converted private -> public key does not match"); 40 | }) 41 | 42 | }); -------------------------------------------------------------------------------- /tests/services/StorageService.spec.js: -------------------------------------------------------------------------------- 1 | import StorageService from '../../src/services/StorageService'; 2 | import chrome from '../helpers/chrome' 3 | import { expect, assert } from 'chai'; 4 | import 'mocha'; 5 | 6 | 7 | 8 | describe('StorageService', () => { 9 | 10 | global.chrome = chrome; 11 | 12 | let scatter = null; 13 | 14 | it('should be able to get an instance of scatter from the storage even if one does not exist', (done) => { 15 | StorageService.get().then(stored => { 16 | scatter = stored; 17 | done(); 18 | }) 19 | }); 20 | 21 | it('should be able to save an instance of scatter to the storage', (done) => { 22 | scatter.meta.version = '1.2'; 23 | StorageService.save(scatter).then(saved => { 24 | StorageService.get().then(stored => { 25 | assert(stored.meta.version === scatter.meta.version, "Saved scatter does not match"); 26 | done(); 27 | }) 28 | }) 29 | }); 30 | 31 | it('should be able to remove the instance of scatter from the storage', (done) => { 32 | StorageService.remove().then(saved => { 33 | StorageService.get().then(stored => { 34 | assert(stored.meta.version !== scatter.meta.version, "Saved scatter does not match"); 35 | done(); 36 | }) 37 | }) 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | 2 | require('jsdom-global')() 3 | global.chrome = { 4 | storage: { 5 | local: { 6 | stored:{}, 7 | set:function(keyobj, cb){ 8 | Object.keys(keyobj).map(key => chrome.storage.local.stored[key] = keyobj[key]) 9 | cb(true); 10 | }, 11 | get:function(key, cb){ 12 | cb( 13 | chrome.storage.local.stored.hasOwnProperty(key) ? 14 | {[key]:chrome.storage.local.stored[key]} 15 | : {} 16 | ) 17 | }, 18 | remove:function(key, cb){ 19 | delete chrome.storage.local.stored[key]; 20 | cb(true); 21 | } 22 | } 23 | }, 24 | runtime:{ 25 | id:'TEST', 26 | sendMessage:function(msg, cb){ 27 | this.onMessage.trigger(msg, cb) 28 | }, 29 | onMessage:{ 30 | messageQueue:[], 31 | trigger:function(data, cb) { this.messageQueue.push({data, cb}) }, 32 | addListener:function(cb){ 33 | setInterval(() => { 34 | if(this.messageQueue.length){ 35 | let message = this.messageQueue.pop(); 36 | cb(message.data, {id:'TEST'}, message.cb) 37 | } 38 | }, 100) 39 | } 40 | } 41 | } 42 | }; -------------------------------------------------------------------------------- /tests/signers/eos.spec.js: -------------------------------------------------------------------------------- 1 | import Scatter from '../../src/models/Scatter' 2 | import Scatterdapp from '../../src/scatterdapp' 3 | import KeyPair from '../../src/models/KeyPair' 4 | import Account from '../../src/models/Account' 5 | import Identity from '../../src/models/Identity' 6 | import {PersonalInformation, LocationInformation} from '../../src/models/Identity' 7 | import Network from '../../src/models/Network' 8 | import chrome from '../helpers/chrome'; 9 | import PluginRepository from '../../src/plugins/PluginRepository' 10 | import {EncryptedStream} from 'extension-streams'; 11 | import {Blockchains} from '../../src/models/Blockchains' 12 | 13 | import Eos from 'eosjs'; 14 | 15 | import { expect, assert } from 'chai'; 16 | import 'mocha'; 17 | 18 | 19 | describe('Signature Request', async testDone => { 20 | 21 | global.chrome = chrome; 22 | 23 | const network = Network.fromJson({ 24 | host:'x.x.x.x', 25 | port:'0000' 26 | }); 27 | 28 | // Using a pre-generated keypair to evade entropy gathering 29 | const keypair = KeyPair.fromJson({ 30 | publicKey:'EOS68fyx6VBXvk4TtEC8GL5GP5qJcod9f7ZcZWBTVSbAYhAwkWkQu', 31 | privateKey:'5KLkgBvMi1Ed9x2xWkiNdZNYZR1DSfJxVqK9fhRtDuKucn1bcUY' 32 | }); 33 | 34 | const account = Account.fromJson({ 35 | publicKey:keypair.publicKey, 36 | name:'test123', 37 | authority:'active' 38 | }); 39 | 40 | const identity = new Identity(); 41 | identity.name = 'e2eTest'; 42 | identity.personal = PersonalInformation.fromJson({ 43 | firstname:'hello', 44 | lastname:'world', 45 | email:'asdf@asdf.com' 46 | }); 47 | identity.locations = [LocationInformation.fromJson({ 48 | name:'Home', 49 | isDefault:true, 50 | phone:'47912559', 51 | address:'1561 Dylan Dr', 52 | city:'Virginia Beach', 53 | state:'VA', 54 | country:{code:'US', name:'United States'}, 55 | zipcode:'23464' 56 | })]; 57 | identity.setAccount(network, account); 58 | 59 | 60 | const scatter = new Scatter(); 61 | scatter.keychain.keypairs.push(keypair); 62 | scatter.keychain.identities.push(identity); 63 | 64 | // TODO: Update to dawn 3 once eosjs is ready 65 | let unsignedTransaction = { 66 | scope: [account.name, 'inita'], 67 | messages: [ 68 | { 69 | code: 'eos', 70 | type: 'transfer', 71 | authorization: [{ 72 | account: account.name, 73 | permission: 'active' 74 | }], 75 | data: { 76 | from: account.name, 77 | to: 'inita', 78 | amount: 1, 79 | memo: 'Test' 80 | } 81 | } 82 | ] 83 | }; 84 | 85 | const eos = Eos({ 86 | keyProvider: [keypair.privateKey], 87 | mockTransactions: () => 'pass', 88 | broadcast: false, 89 | sign: true 90 | }); 91 | 92 | // it('should initialize deferred mock vars', done => { 93 | // identity.initialize().then(done()); 94 | // }); 95 | 96 | it('should be able to set up a transaction to be signed', done => { 97 | 98 | const signProvider = PluginRepository.plugin(Blockchains.EOS).signatureProvider(); 99 | eos.transfer('inita', 'initb', '10.000 EOS', '', {signProvider}).then(done => { 100 | 101 | }); 102 | 103 | done(); 104 | }); 105 | 106 | 107 | 108 | }); -------------------------------------------------------------------------------- /tests/util/ContractHelpers.spec.js: -------------------------------------------------------------------------------- 1 | import ContractHelpers from '../../src/util/ContractHelpers' 2 | import { expect, assert } from 'chai'; 3 | import 'mocha'; 4 | 5 | describe('ContractHelpers', () => { 6 | 7 | const domain = 'test.com'; 8 | const network = { host:'test.com', port:8080 }; 9 | 10 | const message = { 11 | authorization:[{ account:'test', permission:'active' }], 12 | code:'eos', 13 | type:'transfer', 14 | data:{ 15 | from:'test', 16 | to:'inita', 17 | amount:'10', 18 | memo:'' 19 | } 20 | }; 21 | 22 | const differentMessages = [ 23 | { 24 | authorization:[{ account:'inita', permission:'active' }], 25 | code:'eos', 26 | type:'transfer', 27 | data:{ 28 | from:'inita', 29 | to:'test', 30 | amount:'10', 31 | memo:'' 32 | } 33 | }, 34 | { 35 | authorization:[{ account:'test', permission:'active' }], 36 | code:'eos', 37 | type:'transfer', 38 | data:{ 39 | from:'test', 40 | to:'inita', 41 | amount:'11', 42 | memo:'' 43 | } 44 | }, 45 | { 46 | authorization:[{ account:'test', permission:'active' }], 47 | code:'eos', 48 | type:'something_else', 49 | data:{ 50 | from:'test', 51 | to:'inita', 52 | amount:'10', 53 | memo:'' 54 | } 55 | }, 56 | { 57 | authorization:[{ account:'test', permission:'active' }], 58 | code:'eos2', 59 | type:'transfer', 60 | data:{ 61 | from:'test', 62 | to:'inita', 63 | amount:'10', 64 | memo:'' 65 | } 66 | } 67 | ]; 68 | 69 | let checksum = ''; 70 | 71 | it('should be able to create a checksum', () => { 72 | checksum = ContractHelpers.messageChecksum(message, 'test', domain, network); 73 | }); 74 | 75 | it('should know that a contract itself or the message conditions have changed and should fail the checksum validation', () => { 76 | differentMessages.map(msg => { 77 | assert(!ContractHelpers.validChecksum(checksum, msg, 'test', domain, network), "checksum came back valid but should not have"); 78 | }) 79 | }); 80 | 81 | it('should know that an exact same message is valid by it\'s checksum', () => { 82 | assert(ContractHelpers.validChecksum(checksum, message, 'test', domain, network)); 83 | }); 84 | 85 | }); -------------------------------------------------------------------------------- /tests/util/IdGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import IdGenerator from '../../src/util/IdGenerator' 2 | import { expect, assert } from 'chai'; 3 | import 'mocha'; 4 | 5 | describe('IdGenerator', () => { 6 | 7 | it('should be able to generate a random textual id', () => { 8 | const random32 = IdGenerator.text(32); 9 | const random32_2 = IdGenerator.text(32); 10 | const random64 = IdGenerator.text(64); 11 | const random128 = IdGenerator.text(128); 12 | assert(random32.length === 32); 13 | assert(random32 !== random32_2); 14 | assert(random64.length === 64); 15 | assert(random128.length === 128); 16 | }); 17 | 18 | it('should be able to generate a random numeric id', () => { 19 | const random32 = IdGenerator.numeric(32); 20 | const random32_2 = IdGenerator.numeric(32); 21 | const random64 = IdGenerator.numeric(64); 22 | const random128 = IdGenerator.numeric(128); 23 | assert(random32.length === 32); 24 | assert(random32 !== random32_2); 25 | assert(random64.length === 64); 26 | assert(random128.length === 128); 27 | assert(!isNaN(random32)); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /tests/util/ObjectHelpers.spec.js: -------------------------------------------------------------------------------- 1 | import ObjectHelpers from '../../src/util/ObjectHelpers' 2 | import {LocationFields, LocationInformation} from '../../src/models/Identity' 3 | import { expect, assert } from 'chai'; 4 | import 'mocha'; 5 | 6 | describe('ObjectHelpers', () => { 7 | 8 | const location = LocationInformation.fromJson({ 9 | name:'test', 10 | country:{code:'US', name:'United States'} 11 | }); 12 | 13 | it('should be able to get a property from a field using dot notation', () => { 14 | assert(ObjectHelpers.getFieldFromObjectByDotNotation(location, 'country.code') === 'US', 15 | "Could not get `country.code` property from the object."); 16 | 17 | assert(ObjectHelpers.getFieldFromObjectByDotNotation(location, 'name') === 'test', 18 | "Could not get `name` property from the object."); 19 | }); 20 | 21 | it('should return undefined from non-existent properties', () => { 22 | assert(ObjectHelpers.getFieldFromObjectByDotNotation(location, 'noprop') === undefined, 23 | "Trying to get non existant prop was not undefined"); 24 | }); 25 | 26 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack') 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const IgnoreEmitPlugin = require('ignore-emit-webpack-plugin'); 6 | const ZipPlugin = require('zip-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const nodeExternals = require('webpack-node-externals') 9 | const Dotenv = require('dotenv-webpack'); 10 | 11 | console.log(process.env.SCATTER_ENV); 12 | 13 | const production = process.env.SCATTER_ENV === 'production'; 14 | const vueAlias = `vue/dist/vue${production ? '.min' : ''}.js`; 15 | 16 | const devPlugins = [ 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: '"production"' 20 | } 21 | }) 22 | ]; 23 | const prodPlugins = devPlugins.concat([ 24 | new ZipPlugin({ path: '../', filename: 'scatter.zip' }), 25 | new UglifyJsPlugin(), 26 | ]) 27 | 28 | const productionPlugins = !production ? devPlugins : prodPlugins; 29 | 30 | const runningTests = process.env.SCATTER_ENV === 'testing'; 31 | const externals = runningTests ? [nodeExternals()] : []; 32 | 33 | function replaceSuffixes(file){ 34 | return file 35 | .replace('scss', 'css') 36 | } 37 | 38 | const filesToPack = [ 39 | 'background.js', 40 | 'popup.js', 41 | 'content.js', 42 | 'inject.js', 43 | 'prompt.js', 44 | 'scatterdapp.js', 45 | 'styles.scss', 46 | ]; 47 | const entry = filesToPack.reduce((o, file) => Object.assign(o, {[replaceSuffixes(file)]: `./src/${file}`}), {}); 48 | 49 | module.exports = { 50 | entry, 51 | output: { 52 | path: path.resolve(__dirname, './build'), 53 | filename: '[name]' 54 | }, 55 | resolve: { 56 | alias: { 57 | vue: vueAlias, 58 | 'extension-streams': 'extension-streams/dist/index.js', 59 | 'aes-oop': 'aes-oop/dist/AES.js', 60 | }, 61 | modules: [ path.join(__dirname, "node_modules") ] 62 | }, 63 | module: { 64 | loaders: [ 65 | { test: /\.js$/, loader: 'babel-loader', query: { presets: ['es2015', 'stage-3'] }, exclude: /node_modules/ } 66 | ], 67 | rules:[ 68 | { test: /\.(sass|scss)$/, loader: ExtractTextPlugin.extract(['css-loader', 'sass-loader']) }, 69 | { test: /\.(html|png)$/, loader: 'file-loader', options: { name: '[name].[ext]' } }, 70 | { test: /\.vue$/, loader: 'vue-loader', options: { 71 | loaders: { 72 | js: 'babel-loader', 73 | scss: 'vue-style-loader!css-loader!sass-loader' 74 | } 75 | } } 76 | ] 77 | }, 78 | plugins: [ 79 | new ExtractTextPlugin({ filename: '[name]', allChunks: true }), 80 | new IgnoreEmitPlugin(/\.omit$/), 81 | new CopyWebpackPlugin([`./src/copied`]), 82 | new Dotenv({ 83 | path: './.env', 84 | safe: true 85 | }) 86 | ].concat(productionPlugins), 87 | stats: { colors: true }, 88 | devtool: 'source-map', //inline- 89 | externals 90 | } --------------------------------------------------------------------------------