├── babel-mocha.js ├── src ├── utils │ ├── extensionApi.js │ ├── localStorage.js │ ├── cryptoUtils.js │ ├── PortStream.js │ └── setupDnode.js ├── ui │ ├── index.jsx │ ├── components │ │ ├── Unlock.jsx │ │ ├── Initialize.jsx │ │ ├── Sign.jsx │ │ └── Keys.jsx │ └── App.jsx ├── copied │ ├── popup.html │ └── manifest.json ├── inpage.js ├── contentscript.js ├── popup.js ├── background.js └── SignerApp.js ├── for_article ├── screenshots │ ├── 6.0.ui-sign.png │ ├── 1.0.load-app.png │ ├── 6.1.ui-signed.png │ ├── 5.0.tx-resolve.png │ ├── 1.2.popup-dev-tools.png │ ├── 2.2.page-hello-async.png │ ├── 3.0.add-remove-keys.png │ ├── 1.1.background-dev-tools.png │ ├── 1.3.contentscript-console.png │ ├── 2.0.back-onConnect-console.png │ ├── 2.1.page-hello-invocation.png │ └── 3.1.mobx-add-remove-keys.png └── code │ ├── 2.7.async-api.js │ ├── 2.8.async-remote.js │ ├── 6.4.ui-background.js │ ├── 6.1.ui-index.jsx │ ├── 0.2.messages-example.js │ ├── 3.0.state-SignerApp.js │ ├── 0.1.messages-example.js │ ├── 3.2.mobx-SignerApp.js │ ├── 2.6.dnode-inpage.js │ ├── 4.1.secure-cryptoUtils.js │ ├── 5.2.tx-SignerApp-2.js │ ├── 2.0.dnode-example.js │ ├── 0.3.messages-example.js │ ├── 3.1.state-background.js │ ├── 2.2.dnode-background.js │ ├── 2.4.dnode-popup.js │ ├── 2.1.dnode-SignerApp.js │ ├── 2.5.dnode-contentscript.js │ ├── 2.3.PortStream.js │ ├── 6.0.ui-popup.js │ ├── 1.0.manifest.json │ ├── 3.3.mobx-background.js │ ├── 6.3.ui-SignerApp.js │ ├── 4.2.secure-background.js │ ├── 6.2.ui-app.jsx │ ├── 4.0.secure-SignerApp.js │ └── 5.1.tx-SignerApp-1.js ├── .babelrc ├── test └── app.spec.js ├── package.json ├── webpack.config.js └── .gitignore /babel-mocha.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")({ 2 | presets: [ "@babel/env" ], 3 | }); -------------------------------------------------------------------------------- /src/utils/extensionApi.js: -------------------------------------------------------------------------------- 1 | export const extensionApi = global.chrome !== 'undefined' 2 | ? global.chrome 3 | : global.browser -------------------------------------------------------------------------------- /for_article/screenshots/6.0.ui-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/6.0.ui-sign.png -------------------------------------------------------------------------------- /for_article/screenshots/1.0.load-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/1.0.load-app.png -------------------------------------------------------------------------------- /for_article/screenshots/6.1.ui-signed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/6.1.ui-signed.png -------------------------------------------------------------------------------- /for_article/screenshots/5.0.tx-resolve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/5.0.tx-resolve.png -------------------------------------------------------------------------------- /for_article/screenshots/1.2.popup-dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/1.2.popup-dev-tools.png -------------------------------------------------------------------------------- /for_article/screenshots/2.2.page-hello-async.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/2.2.page-hello-async.png -------------------------------------------------------------------------------- /for_article/screenshots/3.0.add-remove-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/3.0.add-remove-keys.png -------------------------------------------------------------------------------- /for_article/screenshots/1.1.background-dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/1.1.background-dev-tools.png -------------------------------------------------------------------------------- /for_article/screenshots/1.3.contentscript-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/1.3.contentscript-console.png -------------------------------------------------------------------------------- /for_article/screenshots/2.0.back-onConnect-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/2.0.back-onConnect-console.png -------------------------------------------------------------------------------- /for_article/screenshots/2.1.page-hello-invocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/2.1.page-hello-invocation.png -------------------------------------------------------------------------------- /for_article/screenshots/3.1.mobx-add-remove-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemarell/extension-demo/HEAD/for_article/screenshots/3.1.mobx-add-remove-keys.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 5 | "@babel/plugin-proposal-class-properties" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /for_article/code/2.7.async-api.js: -------------------------------------------------------------------------------- 1 | export class SignerApp { 2 | 3 | popupApi() { 4 | return { 5 | hello: async () => "world" 6 | } 7 | } 8 | 9 | ... 10 | 11 | } -------------------------------------------------------------------------------- /src/ui/index.jsx: -------------------------------------------------------------------------------- 1 | import {render} from 'react-dom' 2 | import App from './App' 3 | import React from "react"; 4 | 5 | export async function initApp(background){ 6 | render( 7 | , 8 | document.getElementById('app-content') 9 | ); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /for_article/code/2.8.async-remote.js: -------------------------------------------------------------------------------- 1 | import {cbToPromise, transformMethods} from "../../src/utils/setupDnode"; 2 | 3 | const pageApi = await new Promise(resolve => { 4 | dnode.once('remote', remoteApi => { 5 | // С помощью утилит меняем все callback на promise 6 | resolve(transformMethods(cbToPromise, remoteApi)) 7 | }) 8 | }); -------------------------------------------------------------------------------- /for_article/code/6.4.ui-background.js: -------------------------------------------------------------------------------- 1 | function setupApp() { 2 | ... 3 | 4 | // Reaction на выставление текста беджа. 5 | reaction( 6 | () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '', 7 | text => extensionApi.browserAction.setBadgeText({text}), 8 | {fireImmediately: true} 9 | ); 10 | 11 | ... 12 | } -------------------------------------------------------------------------------- /for_article/code/6.1.ui-index.jsx: -------------------------------------------------------------------------------- 1 | import {render} from 'react-dom' 2 | import App from './App' 3 | import React from "react"; 4 | 5 | // Инициализируем приложение с background объектом в качест ве props 6 | export async function initApp(background){ 7 | render( 8 | , 9 | document.getElementById('app-content') 10 | ); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/utils/localStorage.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const state = JSON.parse(localStorage.getItem('store')) 4 | return state || undefined 5 | } catch (error) { 6 | console.log(error); 7 | return undefined 8 | } 9 | } 10 | export const saveState = state => { 11 | localStorage.setItem('store', JSON.stringify(state)) 12 | } -------------------------------------------------------------------------------- /src/copied/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Extension demo popup 7 | 8 | 9 | 10 |
Hello world
11 | 12 | 13 | -------------------------------------------------------------------------------- /for_article/code/0.2.messages-example.js: -------------------------------------------------------------------------------- 1 | // Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать 2 | const port = chrome.runtime.connect({name: "knockknock"}); 3 | port.postMessage({joke: "Knock knock"}); 4 | port.onMessage.addListener(function(msg) { 5 | if (msg.question === "Who's there?") 6 | port.postMessage({answer: "Madame"}); 7 | else if (msg.question === "Madame who?") 8 | port.postMessage({answer: "Madame... Bovary"}); 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /src/ui/components/Unlock.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export default class Unlock extends Component { 4 | state = { 5 | pass: '' 6 | } 7 | 8 | onChange = (e) => this.setState({pass: e.target.value}) 9 | 10 | render(){ 11 | const {pass} = this.state; 12 | const {onUnlock} = this.props; 13 | return
14 | 15 | 16 |
17 | } 18 | } -------------------------------------------------------------------------------- /for_article/code/3.0.state-SignerApp.js: -------------------------------------------------------------------------------- 1 | import {setupDnode} from "./utils/setupDnode"; 2 | 3 | export class SignerApp { 4 | 5 | constructor(){ 6 | this.store = { 7 | keys: [], 8 | }; 9 | } 10 | 11 | addKey(key){ 12 | this.store.keys.push(key) 13 | } 14 | 15 | removeKey(index){ 16 | this.store.keys.splice(index,1) 17 | } 18 | 19 | popupApi(){ 20 | return { 21 | addKey: async (key) => this.addKey(key), 22 | removeKey: async (index) => this.removeKey(index) 23 | } 24 | } 25 | 26 | ... 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | // Используется для осложнения подбора пароля перебором 4 | function strengthenPassword(pass, rounds = 5000) { 5 | while (rounds-- > 0){ 6 | pass = CryptoJS.SHA256(pass).toString() 7 | } 8 | return pass 9 | } 10 | 11 | export function encrypt(str, pass){ 12 | const strongPass = strengthenPassword(pass); 13 | return CryptoJS.AES.encrypt(str, strongPass).toString() 14 | } 15 | 16 | export function decrypt(str, pass){ 17 | const strongPass = strengthenPassword(pass) 18 | const decrypted = CryptoJS.AES.decrypt(str, strongPass); 19 | return decrypted.toString(CryptoJS.enc.Utf8) 20 | } -------------------------------------------------------------------------------- /test/app.spec.js: -------------------------------------------------------------------------------- 1 | import {observable, autorun} from 'mobx' 2 | import {assert, expect} from 'chai' 3 | import {SignerApp} from '../src/SignerApp' 4 | 5 | describe('app store', () => { 6 | it('should add and remove keys', () => { 7 | const app = new SignerApp() 8 | 9 | let autoruns = 0 10 | autorun(() => { 11 | console.log(app.store.keys) 12 | autoruns++ 13 | }) 14 | 15 | app.addKey('key0') 16 | app.addKey('key1') 17 | app.removeKey(0) 18 | expect(app.store.keys.length).to.equal(1) 19 | expect(app.store.keys[0]).to.equal('key1') 20 | expect(autoruns).to.equal(4) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/inpage.js: -------------------------------------------------------------------------------- 1 | import PostMessageStream from 'post-message-stream'; 2 | import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode"; 3 | 4 | 5 | setupInpageApi().catch(console.error); 6 | 7 | 8 | async function setupInpageApi() { 9 | const connectionStream = new PostMessageStream({ 10 | name: 'page', 11 | target: 'content', 12 | }); 13 | 14 | const api = {}; 15 | const dnode = setupDnode(connectionStream, api); 16 | 17 | const pageApi = await new Promise(resolve => { 18 | dnode.once('remote', remoteApi => { 19 | resolve(transformMethods(cbToPromise, remoteApi)) 20 | }) 21 | }); 22 | 23 | global.SignerApp = pageApi; 24 | } -------------------------------------------------------------------------------- /for_article/code/0.1.messages-example.js: -------------------------------------------------------------------------------- 1 | // Сообщением может быть любой JSON сериализуемый объект 2 | const msg = {a:'foo', b: 'bar'}; 3 | 4 | // extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта) 5 | chrome.runtime.sendMessage(extensionId, msg); 6 | 7 | // Вот так выглядит обработчик 8 | chrome.runtime.onMessage.addListener((msg)=>console.log(msg)) 9 | 10 | // Можно слать сообщения вкладкам зная их id 11 | chrome.tabs.sendMessage(tabId, msg) 12 | 13 | // Получить к вкладкам и их id можно например вот так 14 | chrome.tabs.query( 15 | {currentWindow: true, active : true}, 16 | function(tabArray){ 17 | tabArray.forEach(tab => console.log(tab.id)) 18 | } 19 | ) -------------------------------------------------------------------------------- /for_article/code/3.2.mobx-SignerApp.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx'; 2 | import {setupDnode} from "./utils/setupDnode"; 3 | 4 | export class SignerApp { 5 | 6 | constructor(initState = {}) { 7 | // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним 8 | this.store = observable.object({ 9 | keys: initState.keys || [], 10 | }); 11 | } 12 | 13 | // Методы, которые меняют observable принято оборачивать декоратором 14 | @action 15 | addKey(key) { 16 | this.store.keys.push(key) 17 | } 18 | 19 | @action 20 | removeKey(index) { 21 | this.store.keys.splice(index, 1) 22 | } 23 | 24 | ... 25 | 26 | } -------------------------------------------------------------------------------- /for_article/code/2.6.dnode-inpage.js: -------------------------------------------------------------------------------- 1 | import PostMessageStream from 'post-message-stream'; 2 | import Dnode from 'dnode/browser'; 3 | 4 | 5 | setupInpageApi().catch(console.error); 6 | 7 | 8 | async function setupInpageApi() { 9 | // Стрим к контентскрипту 10 | const connectionStream = new PostMessageStream({ 11 | name: 'page', 12 | target: 'content', 13 | }); 14 | 15 | const dnode = Dnode(); 16 | 17 | connectionStream.pipe(dnode).pipe(connectionStream); 18 | 19 | // Получаем объект API 20 | const pageApi = await new Promise(resolve => { 21 | dnode.once('remote', api => { 22 | resolve(api) 23 | }) 24 | }); 25 | 26 | // Доступ через window 27 | global.SignerApp = pageApi; 28 | } -------------------------------------------------------------------------------- /for_article/code/4.1.secure-cryptoUtils.js: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | // Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей 4 | function strengthenPassword(pass, rounds = 5000) { 5 | while (rounds-- > 0){ 6 | pass = CryptoJS.SHA256(pass).toString() 7 | } 8 | return pass 9 | } 10 | 11 | export function encrypt(str, pass){ 12 | const strongPass = strengthenPassword(pass); 13 | return CryptoJS.AES.encrypt(str, strongPass).toString() 14 | } 15 | 16 | export function decrypt(str, pass){ 17 | const strongPass = strengthenPassword(pass) 18 | const decrypted = CryptoJS.AES.decrypt(str, strongPass); 19 | return decrypted.toString(CryptoJS.enc.Utf8) 20 | } -------------------------------------------------------------------------------- /src/ui/components/Initialize.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | const MIN_SYMBOLS = 6 4 | export default class Initialize extends Component { 5 | state = { 6 | pass: '' 7 | } 8 | 9 | onChange = (e) => this.setState({pass: e.target.value}) 10 | 11 | handleInit = () => this.props.onInit(this.state.pass) 12 | 13 | render(){ 14 | return
15 |

App is not initialized. Enter new password and init app. Password should contain more than {MIN_SYMBOLS} symbols

16 | 17 | 18 |
19 | } 20 | } -------------------------------------------------------------------------------- /for_article/code/5.2.tx-SignerApp-2.js: -------------------------------------------------------------------------------- 1 | 2 | export class SignerApp { 3 | ... 4 | 5 | popupApi() { 6 | return { 7 | addKey: async (key) => this.addKey(key), 8 | removeKey: async (index) => this.removeKey(index), 9 | 10 | lock: async () => this.lock(), 11 | unlock: async (password) => this.unlock(password), 12 | initVault: async (password) => this.initVault(password), 13 | 14 | approve: async (id, keyIndex) => this.approve(id, keyIndex), 15 | reject: async (id) => this.reject(id) 16 | } 17 | } 18 | 19 | pageApi(origin) { 20 | return { 21 | signTransaction: async (txParams) => this.newMessage(txParams, origin) 22 | } 23 | } 24 | 25 | ... 26 | 27 | } 28 | -------------------------------------------------------------------------------- /for_article/code/2.0.dnode-example.js: -------------------------------------------------------------------------------- 1 | import Dnode from "dnode/browser"; 2 | 3 | // В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным 4 | 5 | // Cервер 6 | // API, которое мы хотим предоставить 7 | const dnode = Dnode({ 8 | hello: (cb) => cb(null, "world") 9 | }) 10 | // Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream' 11 | connectionStream.pipe(dnode).pipe(connectionStream) 12 | 13 | 14 | 15 | // Клиент 16 | const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне 17 | 18 | // Выведет в консоль world 19 | dnodeClient.once('remote', remote => { 20 | remote.hello(((err, value) => console.log(value))) 21 | }) 22 | -------------------------------------------------------------------------------- /for_article/code/0.3.messages-example.js: -------------------------------------------------------------------------------- 1 | // Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения 2 | chrome.runtime.onConnect.addListener(function(port) { 3 | console.assert(port.name === "knockknock"); 4 | port.onMessage.addListener(function(msg) { 5 | if (msg.joke === "Knock knock") 6 | port.postMessage({question: "Who's there?"}); 7 | else if (msg.answer === "Madame") 8 | port.postMessage({question: "Madame who?"}); 9 | else if (msg.answer === "Madame... Bovary") 10 | port.postMessage({question: "I don't get it."}); 11 | }); 12 | }); 13 | 14 | // Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте 15 | chrome.runtime.onConnectExternal.addListener(function(port) { 16 | ... 17 | }); -------------------------------------------------------------------------------- /for_article/code/3.1.state-background.js: -------------------------------------------------------------------------------- 1 | import {extensionApi} from "./utils/extensionApi"; 2 | import {PortStream} from "./utils/PortStream"; 3 | import {SignerApp} from "./SignerApp"; 4 | 5 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 6 | 7 | setupApp(); 8 | 9 | function setupApp() { 10 | const app = new SignerApp(); 11 | 12 | if (DEV_MODE) { 13 | global.app = app; 14 | } 15 | 16 | extensionApi.runtime.onConnect.addListener(connectRemote); 17 | 18 | function connectRemote(remotePort) { 19 | const processName = remotePort.name; 20 | const portStream = new PortStream(remotePort); 21 | if (processName === 'contentscript') { 22 | const origin = remotePort.sender.url; 23 | app.connectPage(portStream, origin) 24 | } else { 25 | app.connectPopup(portStream) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /for_article/code/2.2.dnode-background.js: -------------------------------------------------------------------------------- 1 | import {extensionApi} from "./utils/extensionApi"; 2 | import {PortStream} from "./utils/PortStream"; 3 | import {SignerApp} from "./SignerApp"; 4 | 5 | const app = new SignerApp(); 6 | 7 | // onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения) 8 | extensionApi.runtime.onConnect.addListener(connectRemote); 9 | 10 | 11 | function connectRemote(remotePort) { 12 | const processName = remotePort.name; 13 | const portStream = new PortStream(remotePort); 14 | // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui 15 | if (processName === 'contentscript'){ 16 | const origin = remotePort.sender.url 17 | app.connectPage(portStream, origin) 18 | }else{ 19 | app.connectPopup(portStream) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /for_article/code/2.4.dnode-popup.js: -------------------------------------------------------------------------------- 1 | import {extensionApi} from "./utils/extensionApi"; 2 | import {PortStream} from "./utils/PortStream"; 3 | import Dnode from 'dnode/browser'; 4 | 5 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 6 | 7 | setupUi().catch(console.error); 8 | 9 | async function setupUi(){ 10 | // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем dnode 11 | const backgroundPort = extensionApi.runtime.connect({name: 'popup'}); 12 | const connectionStream = new PortStream(backgroundPort); 13 | 14 | const dnode = Dnode(); 15 | 16 | connectionStream.pipe(dnode).pipe(connectionStream); 17 | 18 | const background = await new Promise(resolve => { 19 | dnode.once('remote', api => { 20 | resolve(api) 21 | }) 22 | }); 23 | 24 | // Делаем объект API доступным из консоли 25 | if (DEV_MODE){ 26 | global.background = background; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/ui/components/Sign.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {libs} from '@waves/waves-transactions' 3 | 4 | export default class Sign extends Component { 5 | state = { 6 | selectedAccount: 0 7 | } 8 | 9 | render(){ 10 | const {message, onApprove, onReject} = this.props; 11 | const {selectedAccount} = this.state; 12 | 13 | return
14 |

15 | Transaction from {message.origin} 16 |

17 |
{JSON.stringify(message.data, null, 2)}
18 | 21 |
22 | 23 | 24 |
25 |
26 | } 27 | } -------------------------------------------------------------------------------- /src/contentscript.js: -------------------------------------------------------------------------------- 1 | import {extensionApi} from "./utils/extensionApi"; 2 | import {PortStream} from "./utils/PortStream"; 3 | import PostMessageStream from 'post-message-stream'; 4 | 5 | setupConnection(); 6 | injectScript(); 7 | 8 | function setupConnection(){ 9 | const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'}); 10 | const backgroundStream = new PortStream(backgroundPort); 11 | 12 | const pageStream = new PostMessageStream({ 13 | name: 'content', 14 | target: 'page', 15 | }); 16 | 17 | pageStream.pipe(backgroundStream).pipe(pageStream); 18 | } 19 | 20 | 21 | function injectScript(){ 22 | try { 23 | // inject in-page script 24 | let script = document.createElement('script'); 25 | script.src = extensionApi.extension.getURL('inpage.js'); 26 | const container = document.head || document.documentElement; 27 | container.insertBefore(script, container.children[0]); 28 | script.onload = () => script.remove(); 29 | } catch (e) { 30 | console.error('Injection failed.', e); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/components/Keys.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {libs} from '@waves/waves-transactions' 3 | 4 | export default class Keys extends Component { 5 | state = { 6 | newSeed: '' 7 | } 8 | 9 | deleteSeed = (index) => () => this.props.onRemove(index) 10 | 11 | addSeed = () => { 12 | this.props.onAdd(this.state.newSeed) 13 | this.setState({newSeed: ''}) 14 | } 15 | 16 | onChange = (e) => this.setState({newSeed: e.target.value}) 17 | 18 | render(){ 19 | return
20 |
    21 | {this.props.keys.map((seed, index) => ( 22 |
  • 23 | {libs.crypto.address(seed)} 24 | 25 |
  • 26 | ))} 27 |
28 | 29 | 30 |
31 | } 32 | } -------------------------------------------------------------------------------- /for_article/code/2.1.dnode-SignerApp.js: -------------------------------------------------------------------------------- 1 | import Dnode from 'dnode/browser'; 2 | 3 | export class SignerApp { 4 | 5 | // Возвращает объект API для ui 6 | popupApi(){ 7 | return { 8 | hello: cb => cb(null, 'world') 9 | } 10 | } 11 | 12 | // Возвращает объет API для страницы 13 | pageApi(){ 14 | return { 15 | hello: cb => cb(null, 'world') 16 | } 17 | } 18 | 19 | // Подключает popup ui 20 | connectPopup(connectionStream){ 21 | const api = this.popupApi(); 22 | const dnode = Dnode(api); 23 | 24 | connectionStream.pipe(dnode).pipe(connectionStream); 25 | 26 | dnode.on('remote', (remote) => { 27 | console.log(remote) 28 | }) 29 | } 30 | 31 | // Подключает страницу 32 | connectPage(connectionStream, origin){ 33 | const api = this.popupApi(); 34 | const dnode = Dnode(api); 35 | 36 | connectionStream.pipe(dnode).pipe(connectionStream); 37 | 38 | dnode.on('remote', (remote) => { 39 | console.log(origin); 40 | console.log(remote) 41 | }) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /for_article/code/2.5.dnode-contentscript.js: -------------------------------------------------------------------------------- 1 | import PostMessageStream from 'post-message-stream'; 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | 5 | setupConnection(); 6 | injectScript(); 7 | 8 | function setupConnection(){ 9 | // Стрим к бекграунду 10 | const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'}); 11 | const backgroundStream = new PortStream(backgroundPort); 12 | 13 | // Стрим к странице 14 | const pageStream = new PostMessageStream({ 15 | name: 'content', 16 | target: 'page', 17 | }); 18 | 19 | pageStream.pipe(backgroundStream).pipe(pageStream); 20 | } 21 | 22 | 23 | function injectScript(){ 24 | try { 25 | // inject in-page script 26 | let script = document.createElement('script'); 27 | script.src = extensionApi.extension.getURL('inpage.js'); 28 | const container = document.head || document.documentElement; 29 | container.insertBefore(script, container.children[0]); 30 | script.onload = () => script.remove(); 31 | } catch (e) { 32 | console.error('Injection failed.', e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/PortStream.js: -------------------------------------------------------------------------------- 1 | import {Duplex} from 'readable-stream'; 2 | 3 | 4 | export class PortStream extends Duplex{ 5 | constructor(port){ 6 | super({objectMode: true}); 7 | this._port = port; 8 | port.onMessage.addListener(this._onMessage.bind(this)); 9 | port.onDisconnect.addListener(this._onDisconnect.bind(this)) 10 | } 11 | 12 | _onMessage(msg) { 13 | if (Buffer.isBuffer(msg)) { 14 | delete msg._isBuffer; 15 | const data = new Buffer(msg); 16 | this.push(data) 17 | } else { 18 | this.push(msg) 19 | } 20 | } 21 | 22 | _onDisconnect() { 23 | this.destroy() 24 | } 25 | 26 | _read(){} 27 | 28 | _write(msg, encoding, cb) { 29 | try { 30 | if (Buffer.isBuffer(msg)) { 31 | const data = msg.toJSON(); 32 | data._isBuffer = true; 33 | this._port.postMessage(data) 34 | } else { 35 | this._port.postMessage(msg) 36 | } 37 | } catch (err) { 38 | return cb(new Error('PortStream - disconnected')) 39 | } 40 | cb() 41 | } 42 | } -------------------------------------------------------------------------------- /for_article/code/2.3.PortStream.js: -------------------------------------------------------------------------------- 1 | import {Duplex} from 'readable-stream'; 2 | 3 | export class PortStream extends Duplex{ 4 | constructor(port){ 5 | super({objectMode: true}); 6 | this._port = port; 7 | port.onMessage.addListener(this._onMessage.bind(this)); 8 | port.onDisconnect.addListener(this._onDisconnect.bind(this)) 9 | } 10 | 11 | _onMessage(msg) { 12 | if (Buffer.isBuffer(msg)) { 13 | delete msg._isBuffer; 14 | const data = new Buffer(msg); 15 | this.push(data) 16 | } else { 17 | this.push(msg) 18 | } 19 | } 20 | 21 | _onDisconnect() { 22 | this.destroy() 23 | } 24 | 25 | _read(){} 26 | 27 | _write(msg, encoding, cb) { 28 | try { 29 | if (Buffer.isBuffer(msg)) { 30 | const data = msg.toJSON(); 31 | data._isBuffer = true; 32 | this._port.postMessage(data) 33 | } else { 34 | this._port.postMessage(msg) 35 | } 36 | } catch (err) { 37 | return cb(new Error('PortStream - disconnected')) 38 | } 39 | cb() 40 | } 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack --watch", 8 | "dist": "NODE_ENV=production webpack", 9 | "test": "mocha --require babel-mocha.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.2.2", 15 | "@babel/plugin-proposal-class-properties": "^7.3.0", 16 | "@babel/plugin-proposal-decorators": "^7.2.3", 17 | "@babel/preset-env": "^7.2.3", 18 | "@babel/preset-react": "^7.0.0", 19 | "@babel/register": "^7.0.0", 20 | "@types/react": "^16.7.20", 21 | "babel-loader": "^8.0.4", 22 | "chai": "^4.2.0", 23 | "copy-webpack-plugin": "^4.6.0", 24 | "mocha": "^5.2.0", 25 | "webpack": "^4.28.2", 26 | "webpack-cli": "^3.1.2" 27 | }, 28 | "dependencies": { 29 | "@waves/waves-transactions": "^3.0.19", 30 | "crypto-js": "^3.1.9-1", 31 | "dnode": "^1.2.2", 32 | "mobx": "^5.8.0", 33 | "mobx-react": "^5.4.3", 34 | "post-message-stream": "^3.0.0", 35 | "react": "^16.7.0", 36 | "react-dom": "^16.7.0", 37 | "readable-stream": "^3.1.1", 38 | "uuid": "^3.3.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/copied/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id= 3 | "name": "Signer", 4 | "description": "Extension demo", 5 | "version": "0.0.1", 6 | "manifest_version": 2, 7 | 8 | // Скрипты, которые будут исполнятся в background 9 | "background": { 10 | "scripts": ["background.js"] 11 | }, 12 | 13 | // Указание на html для popup 14 | "browser_action": { 15 | "default_title": "My Extension", 16 | "default_popup": "popup.html" 17 | }, 18 | 19 | // Контент скрипты. Здесь у нас одно правило: параллельно всему, что открыто через http или https мы запускаем 20 | // contenscript context page с одним скриптом внутри. Запускать сразу по получении документа. 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "http://*/*", 25 | "https://*/*" 26 | ], 27 | "js": [ 28 | "contentscript.js" 29 | ], 30 | "run_at": "document_start", 31 | "all_frames": true 32 | } 33 | ], 34 | // Разрешен доступ к localStorage, вкладкам 35 | "permissions": [ 36 | "storage", 37 | // "unlimitedStorage", 38 | //"clipboardWrite", 39 | "idle" 40 | //"activeTab", 41 | //"webRequest", 42 | //"notifications", 43 | //"tabs" 44 | ], 45 | // Здесь указываются ресурсы, к которым будет иметь доступ веб страница 46 | "web_accessible_resources": ["inpage.js"] 47 | } -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx' 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode"; 5 | import {initApp} from "./ui/index"; 6 | 7 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 8 | 9 | setupUi().catch(console.error); 10 | 11 | async function setupUi() { 12 | // Подключаемся к порту, создаем из него стрим 13 | const backgroundPort = extensionApi.runtime.connect({name: 'popup'}); 14 | const connectionStream = new PortStream(backgroundPort); 15 | 16 | // Создаем пустой observable для состояния background'a 17 | let backgroundState = observable.object({}); 18 | const api = { 19 | //Отдаем бекграунду функцию, которая будет обновлять observable 20 | updateState: async state => { 21 | Object.assign(backgroundState, state) 22 | } 23 | }; 24 | 25 | // Делаем RPC объект 26 | const dnode = setupDnode(connectionStream, api); 27 | const background = await new Promise(resolve => { 28 | dnode.once('remote', remoteApi => { 29 | resolve(transformMethods(cbToPromise, remoteApi)) 30 | }) 31 | }); 32 | 33 | // Добавляем в background observable со стейтом 34 | background.state = backgroundState; 35 | 36 | if (DEV_MODE) { 37 | global.background = background; 38 | } 39 | 40 | // Запуск интерфейса 41 | await initApp(background) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /for_article/code/6.0.ui-popup.js: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx' 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode"; 5 | import {initApp} from "./ui/index"; 6 | 7 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 8 | 9 | setupUi().catch(console.error); 10 | 11 | async function setupUi() { 12 | // Подключаемся к порту, создаем из него стрим 13 | const backgroundPort = extensionApi.runtime.connect({name: 'popup'}); 14 | const connectionStream = new PortStream(backgroundPort); 15 | 16 | // Создаем пустой observable для состояния background'a 17 | let backgroundState = observable.object({}); 18 | const api = { 19 | //Отдаем бекграунду функцию, которая будет обновлять observable 20 | updateState: async state => { 21 | Object.assign(backgroundState, state) 22 | } 23 | }; 24 | 25 | // Делаем RPC объект 26 | const dnode = setupDnode(connectionStream, api); 27 | const background = await new Promise(resolve => { 28 | dnode.once('remote', remoteApi => { 29 | resolve(transformMethods(cbToPromise, remoteApi)) 30 | }) 31 | }); 32 | 33 | // Добавляем в background observable со стейтом 34 | background.state = backgroundState; 35 | 36 | if (DEV_MODE) { 37 | global.background = background; 38 | } 39 | 40 | // Запуск интерфейса 41 | await initApp(background) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /for_article/code/1.0.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id= 3 | "name": "Signer", 4 | "description": "Extension demo", 5 | "version": "0.0.1", 6 | "manifest_version": 2, 7 | 8 | // Скрипты, которые будут исполнятся в background, их может быть несколько 9 | "background": { 10 | "scripts": ["background.js"] 11 | }, 12 | 13 | // Какой html использовать для popup 14 | "browser_action": { 15 | "default_title": "My Extension", 16 | "default_popup": "popup.html" 17 | }, 18 | 19 | // Контент скрипты. 20 | // У нас один объект: для всех url начинающихся с http или https мы запускаем 21 | // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов 22 | "content_scripts": [ 23 | { 24 | "matches": [ 25 | "http://*/*", 26 | "https://*/*" 27 | ], 28 | "js": [ 29 | "contentscript.js" 30 | ], 31 | "run_at": "document_start", 32 | "all_frames": true 33 | } 34 | ], 35 | // Разрешен доступ к localStorage и idle api 36 | "permissions": [ 37 | "storage", 38 | // "unlimitedStorage", 39 | //"clipboardWrite", 40 | "idle" 41 | //"activeTab", 42 | //"webRequest", 43 | //"notifications", 44 | //"tabs" 45 | ], 46 | // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr 47 | "web_accessible_resources": ["inpage.js"] 48 | } -------------------------------------------------------------------------------- /for_article/code/3.3.mobx-background.js: -------------------------------------------------------------------------------- 1 | import {reaction, toJS} from 'mobx'; 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | import {SignerApp} from "./SignerApp"; 5 | // Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу 'store' 6 | import {loadState, saveState} from "./utils/localStorage"; 7 | 8 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 9 | 10 | setupApp(); 11 | 12 | function setupApp() { 13 | const initState = loadState(); 14 | const app = new SignerApp(initState); 15 | 16 | if (DEV_MODE) { 17 | global.app = app; 18 | } 19 | 20 | // Setup state persistence 21 | 22 | // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера 23 | const localStorageReaction = reaction( 24 | () => toJS(app.store), // Функция-селектор данных 25 | saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор 26 | ); 27 | 28 | extensionApi.runtime.onConnect.addListener(connectRemote); 29 | 30 | function connectRemote(remotePort) { 31 | const processName = remotePort.name; 32 | const portStream = new PortStream(remotePort); 33 | if (processName === 'contentscript') { 34 | const origin = remotePort.sender.url 35 | app.connectPage(portStream, origin) 36 | } else { 37 | app.connectPopup(portStream) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/ui/App.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react' 2 | import {observer} from "mobx-react"; 3 | import Init from './components/Initialize' 4 | import Keys from './components/Keys' 5 | import Sign from './components/Sign' 6 | import Unlock from './components/Unlock' 7 | 8 | @observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается 9 | export default class App extends Component { 10 | 11 | render() { 12 | const {keys, messages, initialized, locked} = this.props.background.state; 13 | const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background; 14 | 15 | return 16 | {!initialized 17 | ? 18 | 19 | : 20 | locked 21 | ? 22 | 23 | : 24 | messages.length > 0 25 | ? 26 | 27 | : 28 | 29 | } 30 |
31 | {!locked && } 32 | {initialized && } 33 |
34 |
35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /for_article/code/6.3.ui-SignerApp.js: -------------------------------------------------------------------------------- 1 | import {action, observable, reaction} from 'mobx'; 2 | import uuid from 'uuid/v4'; 3 | import {signTx} from '@waves/waves-transactions' 4 | import {setupDnode} from "./utils/setupDnode"; 5 | import {decrypt, encrypt} from "./utils/cryptoUtils"; 6 | 7 | export class SignerApp { 8 | 9 | ... 10 | 11 | // public 12 | getState() { 13 | return { 14 | keys: this.store.keys, 15 | messages: this.store.newMessages, 16 | initialized: this.store.initialized, 17 | locked: this.store.locked 18 | } 19 | } 20 | 21 | ... 22 | 23 | // 24 | connectPopup(connectionStream) { 25 | const api = this.popupApi(); 26 | const dnode = setupDnode(connectionStream, api); 27 | 28 | dnode.once('remote', (remote) => { 29 | // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе 30 | const updateStateReaction = reaction( 31 | () => this.getState(), 32 | (state) => remote.updateState(state), 33 | // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу. 34 | // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce 35 | {fireImmediately: true, delay: 500} 36 | ); 37 | // Удалим подписку при отключении клиента 38 | dnode.once('end', () => updateStateReaction.dispose()) 39 | 40 | }) 41 | } 42 | 43 | ... 44 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = () => { 5 | 6 | const mode = process.env.NODE_ENV || 'development'; 7 | const SOURCE_FOLDER = path.resolve(__dirname ,'src'); 8 | const DIST_FOLDER = path.resolve(__dirname, 'dist'); 9 | 10 | const COPY = [{ 11 | from: path.join(SOURCE_FOLDER, 'copied'), 12 | to: DIST_FOLDER, 13 | ignore: [] 14 | }]; 15 | 16 | const plugins = []; 17 | 18 | plugins.push(new CopyWebpackPlugin(COPY)); 19 | 20 | return { 21 | mode, 22 | entry: { 23 | popup: path.resolve(SOURCE_FOLDER, 'popup.js'), 24 | background: path.resolve(SOURCE_FOLDER, 'background.js'), 25 | contentscript: path.resolve(SOURCE_FOLDER, 'contentscript.js'), 26 | inpage: path.resolve(SOURCE_FOLDER, 'inpage.js'), 27 | }, 28 | output: { 29 | filename: '[name].js', 30 | path: DIST_FOLDER, 31 | publicPath: './' 32 | }, 33 | devtool: 'inline-source-map', 34 | resolve: { 35 | extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".styl", ".css",".png", ".jpg", ".gif", ".svg", ".woff", ".woff2", ".ttf", ".otf"] 36 | }, 37 | 38 | plugins, 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.(jsx?)$/, 44 | exclude: /node_modules/, 45 | loader: 'babel-loader' 46 | } 47 | ] 48 | }, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /for_article/code/4.2.secure-background.js: -------------------------------------------------------------------------------- 1 | import {reaction, toJS} from 'mobx'; 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | import {SignerApp} from "./SignerApp"; 5 | import {loadState, saveState} from "./utils/localStorage"; 6 | 7 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 8 | const IDLE_INTERVAL = 30; 9 | 10 | setupApp(); 11 | 12 | function setupApp() { 13 | const initState = loadState(); 14 | const app = new SignerApp(initState); 15 | 16 | if (DEV_MODE) { 17 | global.app = app; 18 | } 19 | 20 | // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально 21 | reaction( 22 | () => ({ 23 | vault: app.store.vault 24 | }), 25 | saveState 26 | ); 27 | 28 | // Таймаут бездействия, когда сработает событие 29 | extensionApi.idle.setDetectionInterval(IDLE_INTERVAL); 30 | // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение 31 | extensionApi.idle.onStateChanged.addListener(state => { 32 | if (['locked', 'idle'].indexOf(state) > -1) { 33 | app.lock() 34 | } 35 | }); 36 | 37 | // Connect to other contexts 38 | extensionApi.runtime.onConnect.addListener(connectRemote); 39 | 40 | function connectRemote(remotePort) { 41 | const processName = remotePort.name; 42 | const portStream = new PortStream(remotePort); 43 | if (processName === 'contentscript') { 44 | const origin = remotePort.sender.url 45 | app.connectPage(portStream, origin) 46 | } else { 47 | app.connectPopup(portStream) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import {reaction} from 'mobx'; 2 | import {extensionApi} from "./utils/extensionApi"; 3 | import {PortStream} from "./utils/PortStream"; 4 | import {SignerApp} from "./SignerApp"; 5 | import {loadState, saveState} from "./utils/localStorage"; 6 | 7 | const DEV_MODE = process.env.NODE_ENV !== 'production'; 8 | const IDLE_INTERVAL = 30; 9 | 10 | setupApp(); 11 | 12 | function setupApp() { 13 | const initState = loadState(); 14 | const app = new SignerApp(initState); 15 | 16 | if (DEV_MODE) { 17 | global.app = app; 18 | } 19 | 20 | // Setup state persistence 21 | reaction( 22 | () => ({ 23 | vault: app.store.vault 24 | }), 25 | saveState 26 | ); 27 | 28 | // update badge 29 | reaction( 30 | () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '', 31 | text => extensionApi.browserAction.setBadgeText({text}), 32 | {fireImmediately: true} 33 | ); 34 | 35 | // Lock on idle 36 | extensionApi.idle.setDetectionInterval(IDLE_INTERVAL); 37 | extensionApi.idle.onStateChanged.addListener(state => { 38 | if (['locked', 'idle'].indexOf(state) > -1) { 39 | app.lock() 40 | } 41 | }); 42 | 43 | // Connect to other contexts 44 | extensionApi.runtime.onConnect.addListener(connectRemote); 45 | 46 | function connectRemote(remotePort) { 47 | const processName = remotePort.name; 48 | const portStream = new PortStream(remotePort); 49 | if (processName === 'contentscript') { 50 | const origin = remotePort.sender.url 51 | app.connectPage(portStream, origin) 52 | } else { 53 | app.connectPopup(portStream) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/utils/setupDnode.js: -------------------------------------------------------------------------------- 1 | import Dnode from "dnode/browser"; 2 | 3 | export function setupDnode(connectionStream, api) { 4 | const dnode = Dnode(transformMethods(promiseToCb, api)); 5 | connectionStream.pipe(dnode).pipe(connectionStream); 6 | return dnode 7 | } 8 | 9 | 10 | export function transformMethods(transformation, obj, target = {}) { 11 | Object.keys(obj).forEach(key => { 12 | if (typeof obj[key] === 'object') { 13 | target[key] = {} 14 | transformMethods(transformation, obj[key], target[key]) 15 | } else if (typeof obj[key] === 'function') { 16 | target[key] = transformation(obj[key], obj) 17 | } else { 18 | target[key] = obj[key] 19 | } 20 | }); 21 | return target 22 | } 23 | 24 | 25 | export function cbToPromise(fn, context) { 26 | return (...args) => { 27 | return new Promise((resolve, reject) => { 28 | fn.call(context, ...args, (err, val) => { 29 | if (err) { 30 | reject(err) 31 | } else { 32 | resolve(val) 33 | } 34 | }) 35 | }) 36 | } 37 | } 38 | 39 | export function promiseToCb(fn, context) { 40 | return (...args) => { 41 | 42 | const lastArg = args[args.length - 1]; 43 | const lastArgIsCallback = typeof lastArg === 'function'; 44 | let callback; 45 | if (lastArgIsCallback) { 46 | callback = lastArg; 47 | args.pop() 48 | } else { 49 | callback = () => {}; 50 | } 51 | fn.apply(context, args) 52 | .then(result => setImmediate(callback, null, result)) 53 | .catch(error => setImmediate(callback, error)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /for_article/code/6.2.ui-app.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react' 2 | import {observer} from "mobx-react"; 3 | import Init from './components/Initialize' 4 | import Keys from './components/Keys' 5 | import Sign from './components/Sign' 6 | import Unlock from './components/Unlock' 7 | 8 | @observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается 9 | export default class App extends Component { 10 | 11 | // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы, 12 | // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют 13 | render() { 14 | const {keys, messages, initialized, locked} = this.props.background.state; 15 | const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background; 16 | 17 | return 18 | {!initialized 19 | ? 20 | 21 | : 22 | locked 23 | ? 24 | 25 | : 26 | messages.length > 0 27 | ? 28 | 29 | : 30 | 31 | } 32 |
33 | {!locked && } 34 | {initialized && } 35 |
36 |
37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /for_article/code/4.0.secure-SignerApp.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx'; 2 | import {setupDnode} from "./utils/setupDnode"; 3 | // Утилиты для безопасного шифрования строк. Используют crypto-js 4 | import {encrypt, decrypt} from "./utils/cryptoUtils"; 5 | 6 | export class SignerApp { 7 | constructor(initState = {}) { 8 | this.store = observable.object({ 9 | // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked 10 | password: null, 11 | vault: initState.vault, 12 | 13 | // Геттеры для вычислимых полей. Можно провести аналогию с view в бд. 14 | get locked(){ 15 | return this.password == null 16 | }, 17 | get keys(){ 18 | return this.locked ? 19 | undefined : 20 | SignerApp._decryptVault(this.vault, this.password) 21 | }, 22 | get initialized(){ 23 | return this.vault !== undefined 24 | } 25 | }) 26 | } 27 | // Инициализация пустого хранилища новым паролем 28 | @action 29 | initVault(password){ 30 | this.store.vault = SignerApp._encryptVault([], password) 31 | } 32 | @action 33 | lock() { 34 | this.store.password = null 35 | } 36 | @action 37 | unlock(password) { 38 | this._checkPassword(password); 39 | this.store.password = password 40 | } 41 | @action 42 | addKey(key) { 43 | this._checkLocked(); 44 | this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password) 45 | } 46 | @action 47 | removeKey(index) { 48 | this._checkLocked(); 49 | this.store.vault = SignerApp._encryptVault([ 50 | ...this.store.keys.slice(0, index), 51 | ...this.store.keys.slice(index + 1) 52 | ], 53 | this.store.password 54 | ) 55 | } 56 | 57 | ... // код подключения и api 58 | 59 | // private 60 | _checkPassword(password) { 61 | SignerApp._decryptVault(this.store.vault, password); 62 | } 63 | 64 | _checkLocked() { 65 | if (this.store.locked){ 66 | throw new Error('App is locked') 67 | } 68 | } 69 | 70 | // Методы для шифровки/дешифровки хранилища 71 | static _encryptVault(obj, pass){ 72 | const jsonString = JSON.stringify(obj) 73 | return encrypt(jsonString, pass) 74 | } 75 | 76 | static _decryptVault(str, pass){ 77 | if (str === undefined){ 78 | throw new Error('Vault not initialized') 79 | } 80 | try { 81 | const jsonString = decrypt(str, pass) 82 | return JSON.parse(jsonString) 83 | }catch (e) { 84 | throw new Error('Wrong password') 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /for_article/code/5.1.tx-SignerApp-1.js: -------------------------------------------------------------------------------- 1 | import {action, observable, reaction} from 'mobx'; 2 | import uuid from 'uuid/v4'; 3 | import {signTx} from '@waves/waves-transactions' 4 | import {setupDnode} from "./utils/setupDnode"; 5 | import {decrypt, encrypt} from "./utils/cryptoUtils"; 6 | 7 | export class SignerApp { 8 | 9 | ... 10 | 11 | @action 12 | newMessage(data, origin) { 13 | // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд. 14 | const message = observable.object({ 15 | id: uuid(), // Идентификатор, используюю uuid 16 | origin, // Origin будем впоследствии показывать в интерфейсе 17 | data, // 18 | status: 'new', // Статусов будет четыре: new, signed, rejected и failed 19 | timestamp: Date.now() 20 | }); 21 | console.log(`new message: ${JSON.stringify(message, null, 2)}`); 22 | 23 | this.store.messages.push(message); 24 | 25 | // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его 26 | return new Promise((resolve, reject) => { 27 | reaction( 28 | () => message.status, //Будем обсервить статус сообщеня 29 | (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова 30 | switch (status) { 31 | case 'signed': 32 | resolve(message.data); 33 | break; 34 | case 'rejected': 35 | reject(new Error('User rejected message')); 36 | break; 37 | case 'failed': 38 | reject(new Error(message.err.message)); 39 | break; 40 | default: 41 | return 42 | } 43 | reaction.dispose() 44 | } 45 | ) 46 | }) 47 | } 48 | @action 49 | approve(id, keyIndex = 0) { 50 | const message = this.store.messages.find(msg => msg.id === id); 51 | if (message == null) throw new Error(`No msg with id:${id}`); 52 | try { 53 | message.data = signTx(message.data, this.store.keys[keyIndex]); 54 | message.status = 'signed' 55 | } catch (e) { 56 | message.err = { 57 | stack: e.stack, 58 | message: e.message 59 | }; 60 | message.status = 'failed' 61 | throw e 62 | } 63 | } 64 | @action 65 | reject(id) { 66 | const message = this.store.messages.find(msg => msg.id === id); 67 | if (message == null) throw new Error(`No msg with id:${id}`); 68 | message.status = 'rejected' 69 | } 70 | 71 | ... 72 | } -------------------------------------------------------------------------------- /src/SignerApp.js: -------------------------------------------------------------------------------- 1 | import {action, observable, reaction} from 'mobx'; 2 | import uuid from 'uuid/v4'; 3 | import {signTx} from '@waves/waves-transactions' 4 | import {setupDnode} from "./utils/setupDnode"; 5 | import {decrypt, encrypt} from "./utils/cryptoUtils"; 6 | 7 | export class SignerApp { 8 | 9 | constructor(initState = {}) { 10 | this.store = observable.object({ 11 | password: null, 12 | vault: initState.vault, 13 | messages: [], 14 | 15 | //Computed properties. Можно провести аналогию с view в бд 16 | get locked() { 17 | return this.password == null 18 | }, 19 | get keys() { 20 | if (this.locked) { 21 | return [] 22 | } 23 | return SignerApp._decryptVault(this.vault, this.password) 24 | }, 25 | get initialized() { 26 | return this.vault != null 27 | }, 28 | get newMessages(){ 29 | return this.messages.filter(msg => msg.status === 'new') 30 | } 31 | }) 32 | 33 | } 34 | 35 | // actions 36 | 37 | @action 38 | initVault(password) { 39 | this.store.vault = SignerApp._encryptVault([], password) 40 | this.store.password = password 41 | } 42 | 43 | @action 44 | deleteVault(){ 45 | this.store.vault = null 46 | } 47 | 48 | @action 49 | lock() { 50 | this.store.password = null 51 | } 52 | 53 | @action 54 | unlock(password) { 55 | this._checkPassword(password); 56 | this.store.password = password 57 | } 58 | 59 | @action 60 | addKey(key) { 61 | this._checkLocked() 62 | this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password) 63 | } 64 | 65 | @action 66 | removeKey(index) { 67 | this._checkLocked() 68 | this.store.vault = SignerApp._encryptVault([ 69 | ...this.store.keys.slice(0, index), 70 | ...this.store.keys.slice(index + 1) 71 | ], 72 | this.store.password 73 | ) 74 | } 75 | 76 | @action 77 | newMessage(data, origin) { 78 | // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд. 79 | const message = observable.object({ 80 | id: uuid(), //Идентификатор, используюю uuid, но подошел бы и число с автоинкрементом 81 | origin, // Origin будем впоследствии показывать в интерфейсе 82 | data, // 83 | status: 'new', // Статусов будет четыре: new, signed, rejected и failed 84 | timestamp: Date.now() 85 | }); 86 | console.log(`new message: ${JSON.stringify(message, null, 2)}`); 87 | 88 | this.store.messages.push(message); 89 | 90 | // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его 91 | return new Promise((resolve, reject) => { 92 | reaction( 93 | () => message.status, //Будем обсервить статус сообщеня 94 | (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова 95 | switch (status) { 96 | case 'signed': 97 | resolve(message.data); 98 | break; 99 | case 'rejected': 100 | reject(new Error('User rejected message')); 101 | break; 102 | case 'failed': 103 | reject(new Error(message.err.message)); 104 | break; 105 | default: 106 | return 107 | } 108 | reaction.dispose() 109 | } 110 | ) 111 | }) 112 | } 113 | 114 | @action 115 | approve(id, keyIndex = 0) { 116 | this._checkLocked(); 117 | const message = this.store.messages.find(msg => msg.id === id); 118 | if (message == null) throw new Error(`No msg with id:${id}`); 119 | try { 120 | message.data = signTx(message.data, this.store.keys[keyIndex]); 121 | message.status = 'signed' 122 | } catch (e) { 123 | message.err = { 124 | stack: e.stack, 125 | message: e.message 126 | }; 127 | message.status = 'failed' 128 | throw e 129 | } 130 | } 131 | 132 | @action 133 | reject(id) { 134 | const message = this.store.messages.find(msg => msg.id === id); 135 | if (message == null) throw new Error(`No msg with id:${id}`); 136 | message.status = 'rejected' 137 | } 138 | 139 | // public 140 | getState() { 141 | return { 142 | keys: this.store.keys, 143 | messages: this.store.newMessages, 144 | initialized: this.store.initialized, 145 | locked: this.store.locked 146 | } 147 | } 148 | 149 | popupApi() { 150 | return { 151 | addKey: async (key) => this.addKey(key), 152 | removeKey: async (index) => this.removeKey(index), 153 | 154 | lock: async () => this.lock(), 155 | unlock: async (password) => this.unlock(password), 156 | 157 | initVault: async (password) => this.initVault(password), 158 | deleteVault: async () => this.deleteVault(), 159 | 160 | approve: async (id, keyIndex) => this.approve(id, keyIndex), 161 | reject: async (id) => this.reject(id) 162 | } 163 | } 164 | 165 | pageApi(origin) { 166 | return { 167 | signTransaction: async (txParams) => this.newMessage(txParams, origin) 168 | } 169 | } 170 | 171 | // 172 | connectPopup(connectionStream) { 173 | const api = this.popupApi(); 174 | const dnode = setupDnode(connectionStream, api); 175 | 176 | dnode.once('remote', (remote) => { 177 | // Создаем reaction на изменения стейта, который сделает RPC вызов к UI и обновит стейт 178 | const updateStateReaction = reaction( 179 | () => this.getState(), 180 | (state) => remote.updateState(state), 181 | // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу. 182 | // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce 183 | {fireImmediately: true, delay: 500} 184 | ); 185 | dnode.once('end', () => updateStateReaction.dispose()) 186 | 187 | }) 188 | } 189 | 190 | connectPage(connectionStream, origin) { 191 | const api = this.pageApi(origin); 192 | const dnode = setupDnode(connectionStream, api); 193 | 194 | dnode.on('remote', (remote) => { 195 | console.log(origin); 196 | console.log(remote) 197 | }) 198 | } 199 | 200 | // private 201 | _checkPassword(password) { 202 | SignerApp._decryptVault(this.store.vault, password); 203 | } 204 | 205 | _checkLocked(){ 206 | if (this.store.locked) { 207 | throw new Error('App is locked') 208 | } 209 | } 210 | 211 | static _encryptVault(obj, pass) { 212 | const jsonString = JSON.stringify(obj) 213 | return encrypt(jsonString, pass) 214 | } 215 | 216 | static _decryptVault(str, pass) { 217 | if (str === undefined) { 218 | throw new Error('Vault not initialized') 219 | } 220 | try { 221 | const jsonString = decrypt(str, pass) 222 | return JSON.parse(jsonString) 223 | } catch (e) { 224 | throw new Error('Wrong password') 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | dist 10 | sandbox.test.js 11 | .DS_STORE 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless 78 | ### VisualStudio template 79 | ## Ignore Visual Studio temporary files, build results, and 80 | ## files generated by popular Visual Studio add-ons. 81 | ## 82 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 83 | 84 | # User-specific files 85 | *.suo 86 | *.user 87 | *.userosscache 88 | *.sln.docstates 89 | 90 | # User-specific files (MonoDevelop/Xamarin Studio) 91 | *.userprefs 92 | 93 | # Build results 94 | [Dd]ebug/ 95 | [Dd]ebugPublic/ 96 | [Rr]elease/ 97 | [Rr]eleases/ 98 | x64/ 99 | x86/ 100 | bld/ 101 | [Bb]in/ 102 | [Oo]bj/ 103 | [Ll]og/ 104 | 105 | # Visual Studio 2015/2017 cache/options directory 106 | .vs/ 107 | # Uncomment if you have tasks that create the project's static files in wwwroot 108 | #wwwroot/ 109 | 110 | # Visual Studio 2017 auto generated files 111 | Generated\ Files/ 112 | 113 | # MSTest test Results 114 | [Tt]est[Rr]esult*/ 115 | [Bb]uild[Ll]og.* 116 | 117 | # NUNIT 118 | *.VisualState.xml 119 | TestResult.xml 120 | 121 | # Build Results of an ATL Project 122 | [Dd]ebugPS/ 123 | [Rr]eleasePS/ 124 | dlldata.c 125 | 126 | # Benchmark Results 127 | BenchmarkDotNet.Artifacts/ 128 | 129 | # .NET Core 130 | project.lock.json 131 | project.fragment.lock.json 132 | artifacts/ 133 | 134 | # StyleCop 135 | StyleCopReport.xml 136 | 137 | # Files built by Visual Studio 138 | *_i.c 139 | *_p.c 140 | *_i.h 141 | *.ilk 142 | *.meta 143 | *.obj 144 | *.iobj 145 | *.pch 146 | *.pdb 147 | *.ipdb 148 | *.pgc 149 | *.pgd 150 | *.rsp 151 | *.sbr 152 | *.tlb 153 | *.tli 154 | *.tlh 155 | *.tmp 156 | *.tmp_proj 157 | *.vspscc 158 | *.vssscc 159 | .builds 160 | *.pidb 161 | *.svclog 162 | *.scc 163 | 164 | # Chutzpah Test files 165 | _Chutzpah* 166 | 167 | # Visual C++ cache files 168 | ipch/ 169 | *.aps 170 | *.ncb 171 | *.opendb 172 | *.opensdf 173 | *.sdf 174 | *.cachefile 175 | *.VC.db 176 | *.VC.VC.opendb 177 | 178 | # Visual Studio profiler 179 | *.psess 180 | *.vsp 181 | *.vspx 182 | *.sap 183 | 184 | # Visual Studio Trace Files 185 | *.e2e 186 | 187 | # TFS 2012 Local Workspace 188 | $tf/ 189 | 190 | # Guidance Automation Toolkit 191 | *.gpState 192 | 193 | # ReSharper is a .NET coding add-in 194 | _ReSharper*/ 195 | *.[Rr]e[Ss]harper 196 | *.DotSettings.user 197 | 198 | # JustCode is a .NET coding add-in 199 | .JustCode 200 | 201 | # TeamCity is a build add-in 202 | _TeamCity* 203 | 204 | # DotCover is a Code Coverage Tool 205 | *.dotCover 206 | 207 | # AxoCover is a Code Coverage Tool 208 | .axoCover/* 209 | !.axoCover/settings.json 210 | 211 | # Visual Studio code coverage results 212 | *.coverage 213 | *.coveragexml 214 | 215 | # NCrunch 216 | _NCrunch_* 217 | .*crunch*.local.xml 218 | nCrunchTemp_* 219 | 220 | # MightyMoose 221 | *.mm.* 222 | AutoTest.Net/ 223 | 224 | # Web workbench (sass) 225 | .sass-cache/ 226 | 227 | # Installshield output folder 228 | [Ee]xpress/ 229 | 230 | # DocProject is a documentation generator add-in 231 | DocProject/buildhelp/ 232 | DocProject/Help/*.HxT 233 | DocProject/Help/*.HxC 234 | DocProject/Help/*.hhc 235 | DocProject/Help/*.hhk 236 | DocProject/Help/*.hhp 237 | DocProject/Help/Html2 238 | DocProject/Help/html 239 | 240 | # Click-Once directory 241 | publish/ 242 | 243 | # Publish Web Output 244 | *.[Pp]ublish.xml 245 | *.azurePubxml 246 | # Note: Comment the next line if you want to checkin your web deploy settings, 247 | # but database connection strings (with potential passwords) will be unencrypted 248 | *.pubxml 249 | *.publishproj 250 | 251 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 252 | # checkin your Azure Web App publish settings, but sensitive information contained 253 | # in these scripts will be unencrypted 254 | PublishScripts/ 255 | 256 | # NuGet Packages 257 | *.nupkg 258 | # The packages folder can be ignored because of Package Restore 259 | **/[Pp]ackages/* 260 | # except build/, which is used as an MSBuild target. 261 | !**/[Pp]ackages/build/ 262 | # Uncomment if necessary however generally it will be regenerated when needed 263 | #!**/[Pp]ackages/repositories.config 264 | # NuGet v3's project.json files produces more ignorable files 265 | *.nuget.props 266 | *.nuget.targets 267 | 268 | # Microsoft Azure Build Output 269 | csx/ 270 | *.build.csdef 271 | 272 | # Microsoft Azure Emulator 273 | ecf/ 274 | rcf/ 275 | 276 | # Windows Store app package directories and files 277 | AppPackages/ 278 | BundleArtifacts/ 279 | Package.StoreAssociation.xml 280 | _pkginfo.txt 281 | *.appx 282 | 283 | # Visual Studio cache files 284 | # files ending in .cache can be ignored 285 | *.[Cc]ache 286 | # but keep track of directories ending in .cache 287 | !*.[Cc]ache/ 288 | 289 | # Others 290 | ClientBin/ 291 | ~$* 292 | *~ 293 | *.dbmdl 294 | *.dbproj.schemaview 295 | *.jfm 296 | *.pfx 297 | *.publishsettings 298 | orleans.codegen.cs 299 | 300 | # Including strong name files can present a security risk 301 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 302 | #*.snk 303 | 304 | # Since there are multiple workflows, uncomment next line to ignore bower_components 305 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 306 | #bower_components/ 307 | 308 | # RIA/Silverlight projects 309 | Generated_Code/ 310 | 311 | # Backup & report files from converting an old project file 312 | # to a newer Visual Studio version. Backup files are not needed, 313 | # because we have git ;-) 314 | _UpgradeReport_Files/ 315 | Backup*/ 316 | UpgradeLog*.XML 317 | UpgradeLog*.htm 318 | ServiceFabricBackup/ 319 | *.rptproj.bak 320 | 321 | # SQL Server files 322 | *.mdf 323 | *.ldf 324 | *.ndf 325 | 326 | # Business Intelligence projects 327 | *.rdl.data 328 | *.bim.layout 329 | *.bim_*.settings 330 | *.rptproj.rsuser 331 | 332 | # Microsoft Fakes 333 | FakesAssemblies/ 334 | 335 | # GhostDoc plugin setting file 336 | *.GhostDoc.xml 337 | 338 | # Node.js Tools for Visual Studio 339 | .ntvs_analysis.dat 340 | 341 | # Visual Studio 6 build log 342 | *.plg 343 | 344 | # Visual Studio 6 workspace options file 345 | *.opt 346 | 347 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 348 | *.vbw 349 | 350 | # Visual Studio LightSwitch build output 351 | **/*.HTMLClient/GeneratedArtifacts 352 | **/*.DesktopClient/GeneratedArtifacts 353 | **/*.DesktopClient/ModelManifest.xml 354 | **/*.Server/GeneratedArtifacts 355 | **/*.Server/ModelManifest.xml 356 | _Pvt_Extensions 357 | 358 | # Paket dependency manager 359 | .paket/paket.exe 360 | paket-files/ 361 | 362 | # FAKE - F# Make 363 | .fake/ 364 | 365 | # JetBrains Rider 366 | .idea/ 367 | *.sln.iml 368 | 369 | # CodeRush 370 | .cr/ 371 | 372 | # Python Tools for Visual Studio (PTVS) 373 | __pycache__/ 374 | *.pyc 375 | 376 | # Cake - Uncomment if you are using it 377 | # tools/** 378 | # !tools/packages.config 379 | 380 | # Tabs Studio 381 | *.tss 382 | 383 | # Telerik's JustMock configuration file 384 | *.jmconfig 385 | 386 | # BizTalk build output 387 | *.btp.cs 388 | *.btm.cs 389 | *.odx.cs 390 | *.xsd.cs 391 | 392 | # OpenCover UI analysis results 393 | OpenCover/ 394 | 395 | # Azure Stream Analytics local run output 396 | ASALocalRun/ 397 | 398 | # MSBuild Binary and Structured Log 399 | *.binlog 400 | 401 | # NVidia Nsight GPU debugger configuration file 402 | *.nvuser 403 | 404 | # MFractors (Xamarin productivity tool) working folder 405 | .mfractor/ 406 | ### JetBrains template 407 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 408 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 409 | 410 | # User-specific stuff 411 | .idea/**/workspace.xml 412 | .idea/**/tasks.xml 413 | .idea/**/usage.statistics.xml 414 | .idea/**/dictionaries 415 | .idea/**/shelf 416 | 417 | # Sensitive or high-churn files 418 | .idea/**/dataSources/ 419 | .idea/**/dataSources.ids 420 | .idea/**/dataSources.local.xml 421 | .idea/**/sqlDataSources.xml 422 | .idea/**/dynamic.xml 423 | .idea/**/uiDesigner.xml 424 | .idea/**/dbnavigator.xml 425 | 426 | # Gradle 427 | .idea/**/gradle.xml 428 | .idea/**/libraries 429 | 430 | # Gradle and Maven with auto-import 431 | # When using Gradle or Maven with auto-import, you should exclude module files, 432 | # since they will be recreated, and may cause churn. Uncomment if using 433 | # auto-import. 434 | # .idea/modules.xml 435 | # .idea/*.iml 436 | # .idea/modules 437 | 438 | # CMake 439 | cmake-build-*/ 440 | 441 | # Mongo Explorer plugin 442 | .idea/**/mongoSettings.xml 443 | 444 | # File-based project format 445 | *.iws 446 | 447 | # IntelliJ 448 | out/ 449 | 450 | # mpeltonen/sbt-idea plugin 451 | .idea_modules/ 452 | 453 | # JIRA plugin 454 | atlassian-ide-plugin.xml 455 | 456 | # Cursive Clojure plugin 457 | .idea/replstate.xml 458 | 459 | # Crashlytics plugin (for Android Studio and IntelliJ) 460 | com_crashlytics_export_strings.xml 461 | crashlytics.properties 462 | crashlytics-build.properties 463 | fabric.properties 464 | 465 | # Editor-based Rest Client 466 | .idea/httpRequests 467 | --------------------------------------------------------------------------------