├── src ├── assets │ ├── .gitkeep │ ├── img │ │ ├── shield.png │ │ ├── wallet.png │ │ ├── bv-logo.png │ │ ├── banano-mark.png │ │ ├── bv-logo-alt.png │ │ ├── handshake.png │ │ ├── loader-logo.png │ │ ├── vault-icon.png │ │ ├── banano-mark2.png │ │ ├── bv-logo-cent.png │ │ ├── planet-earth.png │ │ └── bananovaullogo.png │ ├── lib │ │ └── pow │ │ │ ├── pow.wasm │ │ │ ├── thread.js │ │ │ └── startThreads.js │ └── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon-57x57-precomposed.png │ │ ├── apple-touch-icon-60x60-precomposed.png │ │ ├── apple-touch-icon-72x72-precomposed.png │ │ ├── apple-touch-icon-76x76-precomposed.png │ │ ├── apple-touch-icon-114x114-precomposed.png │ │ ├── apple-touch-icon-120x120-precomposed.png │ │ ├── apple-touch-icon-144x144-precomposed.png │ │ ├── apple-touch-icon-152x152-precomposed.png │ │ ├── apple-touch-icon-180x180-precomposed.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ └── safari-pinned-tab.svg ├── app │ ├── welcome │ │ ├── welcome.component.css │ │ ├── welcome.component.ts │ │ ├── welcome.component.spec.ts │ │ └── welcome.component.html │ ├── components │ │ ├── receive │ │ │ ├── receive.component.css │ │ │ ├── receive.component.spec.ts │ │ │ ├── receive.component.html │ │ │ └── receive.component.ts │ │ ├── address-book │ │ │ ├── address-book.component.css │ │ │ ├── address-book.component.spec.ts │ │ │ ├── address-book.component.ts │ │ │ └── address-book.component.html │ │ ├── configure-app │ │ │ ├── configure-app.component.css │ │ │ └── configure-app.component.spec.ts │ │ ├── import-wallet │ │ │ ├── import-wallet.component.css │ │ │ ├── import-wallet.component.spec.ts │ │ │ ├── import-wallet.component.html │ │ │ └── import-wallet.component.ts │ │ ├── manage-wallet │ │ │ ├── manage-wallet.component.css │ │ │ ├── manage-wallet.component.spec.ts │ │ │ ├── manage-wallet.component.ts │ │ │ └── manage-wallet.component.html │ │ ├── account-details │ │ │ ├── account-details.component.css │ │ │ └── account-details.component.spec.ts │ │ ├── change-rep-widget │ │ │ ├── change-rep-widget.component.css │ │ │ ├── change-rep-widget.component.spec.ts │ │ │ ├── change-rep-widget.component.ts │ │ │ └── change-rep-widget.component.html │ │ ├── helpers │ │ │ └── banano-account-id │ │ │ │ ├── banano-account-id.component.css │ │ │ │ ├── banano-account-id.component.html │ │ │ │ ├── banano-account-id.component.spec.ts │ │ │ │ └── banano-account-id.component.ts │ │ ├── wallet-widget │ │ │ ├── wallet-widget.component.css │ │ │ ├── wallet-widget.component.spec.ts │ │ │ ├── wallet-widget.component.ts │ │ │ └── wallet-widget.component.html │ │ ├── import-address-book │ │ │ ├── import-address-book.component.css │ │ │ ├── import-address-book.component.spec.ts │ │ │ ├── import-address-book.component.ts │ │ │ └── import-address-book.component.html │ │ ├── accounts │ │ │ ├── accounts.component.css │ │ │ ├── accounts.component.spec.ts │ │ │ ├── accounts.component.html │ │ │ └── accounts.component.ts │ │ ├── representatives │ │ │ ├── representatives.component.css │ │ │ ├── representatives.component.spec.ts │ │ │ └── representatives.component.spec.ts~338597e99ae8ca659e49a2ed96fa7c6f1e4baf38 │ │ ├── manage-representatives │ │ │ ├── manage-representatives.component.css │ │ │ ├── manage-representatives.component.spec.ts │ │ │ └── manage-representatives.component.ts │ │ ├── notifications │ │ │ ├── notifications.component.css │ │ │ ├── notifications.component.html │ │ │ ├── notifications.component.spec.ts │ │ │ └── notifications.component.ts │ │ ├── transaction-details │ │ │ ├── transaction-details.component.css │ │ │ ├── transaction-details.component.spec.ts │ │ │ └── transaction-details.component.ts │ │ ├── send │ │ │ ├── send.component.css │ │ │ └── send.component.spec.ts │ │ └── configure-wallet │ │ │ ├── configure-wallet.component.spec.ts │ │ │ └── configure-wallet.component.css │ ├── pipes │ │ ├── rai.pipe.spec.ts │ │ ├── fiat.pipe.spec.ts │ │ ├── account.pipe.spec.ts │ │ ├── squeeze.pipe.spec.ts │ │ ├── currency-symbol.pipe.spec.ts │ │ ├── currency-symbol.pipe.ts │ │ ├── squeeze.pipe.ts │ │ ├── account.pipe.ts │ │ ├── fiat.pipe.ts │ │ └── rai.pipe.ts │ ├── services │ │ ├── modal.service.ts │ │ ├── api.service.spec.ts │ │ ├── pow.service.spec.ts │ │ ├── node.service.spec.ts │ │ ├── util.service.spec.ts │ │ ├── modal.service.spec.ts │ │ ├── price.service.spec.ts │ │ ├── ledger.service.spec.ts │ │ ├── wallet.service.spec.ts │ │ ├── work-pool.service.spec.ts │ │ ├── websocket.service.spec.ts │ │ ├── nano-block.service.spec.ts │ │ ├── address-book.service.spec.ts │ │ ├── app-settings.service.spec.ts │ │ ├── notification.service.spec.ts │ │ ├── representative.service.spec.ts │ │ ├── index.ts │ │ ├── desktop.service.ts │ │ ├── node.service.ts │ │ ├── price.service.ts │ │ ├── notification.service.ts │ │ ├── work-pool.service.ts │ │ ├── app-settings.service.ts │ │ ├── address-book.service.ts │ │ ├── api.service.ts │ │ └── websocket.service.ts │ ├── app.component.spec.ts │ ├── app.component.css │ ├── app-routing.module.ts │ ├── app.module.ts │ └── app.component.ts ├── pow.wasm ├── styles.css ├── environments │ ├── environment.prod.ts │ ├── environment.desktop.ts │ └── environment.ts ├── typings.d.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── main.ts ├── test.ts ├── index.html └── polyfills.ts ├── desktop-app ├── assets │ ├── icon.ico │ ├── icon.png │ └── icon.icns ├── tsconfig.json └── src │ └── desktop-app.ts ├── e2e ├── app.po.ts ├── tsconfig.e2e.json └── app.e2e-spec.ts ├── .editorconfig ├── appveyor.yml ├── tsconfig.json ├── renderer.js ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── .angular-cli.json ├── .travis.yml ├── tslint.json ├── package.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/welcome/welcome.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/receive/receive.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/address-book/address-book.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/configure-app/configure-app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/import-wallet/import-wallet.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/manage-wallet/manage-wallet.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/account-details/account-details.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/change-rep-widget/change-rep-widget.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/helpers/banano-account-id/banano-account-id.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pow.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/pow.wasm -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ -------------------------------------------------------------------------------- /src/app/components/wallet-widget/wallet-widget.component.css: -------------------------------------------------------------------------------- 1 | .wallet-bar a { 2 | color: #fff; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/img/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/shield.png -------------------------------------------------------------------------------- /src/assets/img/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/wallet.png -------------------------------------------------------------------------------- /desktop-app/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/desktop-app/assets/icon.ico -------------------------------------------------------------------------------- /desktop-app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/desktop-app/assets/icon.png -------------------------------------------------------------------------------- /src/assets/img/bv-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/bv-logo.png -------------------------------------------------------------------------------- /src/assets/lib/pow/pow.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/lib/pow/pow.wasm -------------------------------------------------------------------------------- /desktop-app/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/desktop-app/assets/icon.icns -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/banano-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/banano-mark.png -------------------------------------------------------------------------------- /src/assets/img/bv-logo-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/bv-logo-alt.png -------------------------------------------------------------------------------- /src/assets/img/handshake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/handshake.png -------------------------------------------------------------------------------- /src/assets/img/loader-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/loader-logo.png -------------------------------------------------------------------------------- /src/assets/img/vault-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/vault-icon.png -------------------------------------------------------------------------------- /src/app/components/import-address-book/import-address-book.component.css: -------------------------------------------------------------------------------- 1 | .import-warning { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/img/banano-mark2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/banano-mark2.png -------------------------------------------------------------------------------- /src/assets/img/bv-logo-cent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/bv-logo-cent.png -------------------------------------------------------------------------------- /src/assets/img/planet-earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/planet-earth.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | desktop: false, 4 | }; 5 | -------------------------------------------------------------------------------- /src/assets/img/bananovaullogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/img/bananovaullogo.png -------------------------------------------------------------------------------- /src/environments/environment.desktop.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | desktop: true, 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/components/accounts/accounts.component.css: -------------------------------------------------------------------------------- 1 | .account-index { 2 | color: #d2d2d2; 3 | margin-right: 5px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-57x57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-57x57-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-60x60-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-60x60-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-72x72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-72x72-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-76x76-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-76x76-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-114x114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-114x114-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-120x120-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-120x120-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-152x152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-152x152-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon-180x180-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/bananovault/HEAD/src/assets/favicon/apple-touch-icon-180x180-precomposed.png -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | 7 | interface Window { 8 | require: NodeRequire; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pipes/rai.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { RaiPipe } from './rai.pipe'; 2 | 3 | describe('RaiPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new RaiPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/pipes/fiat.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FiatPipe } from './fiat.pipe'; 2 | 3 | describe('FiatPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FiatPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/components/representatives/representatives.component.css: -------------------------------------------------------------------------------- 1 | .circle:before { 2 | content: ' \25CF'; 3 | font-size: 16px; 4 | } 5 | .circle-online { 6 | color: #00d60f; 7 | } 8 | .circle-offline { 9 | color: #F00; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/pipes/account.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { AccountPipe } from './account.pipe'; 2 | 3 | describe('AccountPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new AccountPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/pipes/squeeze.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { SqueezePipe } from './squeeze.pipe'; 2 | 3 | describe('SqueezePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new SqueezePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/components/manage-representatives/manage-representatives.component.css: -------------------------------------------------------------------------------- 1 | .circle:before { 2 | content: ' \25CF'; 3 | font-size: 16px; 4 | } 5 | .circle-online { 6 | color: #00d60f; 7 | } 8 | .circle-offline { 9 | color: #F00; 10 | } 11 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/pipes/currency-symbol.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { CurrencySymbolPipe } from './currency-symbol.pipe'; 2 | 3 | describe('CurrencySymbolPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new CurrencySymbolPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/components/helpers/banano-account-id/banano-account-id.component.html: -------------------------------------------------------------------------------- 1 | {{ firstCharacters }}...{{ lastCharacters }} 2 | 3 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('raivault App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/components/notifications/notifications.component.css: -------------------------------------------------------------------------------- 1 | .wallet-notification { 2 | border: 1px solid #d4d4d4; 3 | font-size: 14px; 4 | padding: 12px; 5 | } 6 | 7 | .close-notification { 8 | top: 16px; 9 | } 10 | 11 | .notification-container { 12 | position: absolute; 13 | bottom: 0; 14 | width: 70%; 15 | left: 15%; 16 | z-index: 1050; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/transaction-details/transaction-details.component.css: -------------------------------------------------------------------------------- 1 | .confirm-title { 2 | font-weight: bolder; 3 | /*display: block;*/ 4 | font-size: 24px; 5 | } 6 | 7 | .confirm-currency { 8 | font-weight: bolder; 9 | display: block; 10 | font-size: 20px; 11 | } 12 | 13 | .confirm-subtitle { 14 | display: block; 15 | font-size: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/services/modal.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {BehaviorSubject} from "rxjs/BehaviorSubject"; 3 | 4 | @Injectable() 5 | export class ModalService { 6 | 7 | showAccount$ = new BehaviorSubject(null); 8 | constructor() { } 9 | 10 | showAccount(account) { 11 | this.showAccount$.next(account); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | platform: 4 | - x64 5 | 6 | cache: 7 | - node_modules 8 | - '%USERPROFILE%\.electron' 9 | 10 | init: 11 | - git config --global core.autocrlf input 12 | 13 | install: 14 | - ps: Install-Product node 8 x64 15 | - yarn 16 | - npm install -g typescript 17 | 18 | build_script: 19 | - yarn release 20 | 21 | test: off 22 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "allowJs": false, 8 | "types": [], 9 | "lib": ["dom", "es7", "scripthost"] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts", 14 | "app/lib/tweetnacl/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/app/components/notifications/notifications.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/services/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ApiService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ApiService], (service: ApiService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/pow.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { PowService } from './pow.service'; 4 | 5 | describe('PowService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [PowService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([PowService], (service: PowService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/services/node.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { NodeService } from './node.service'; 4 | 5 | describe('NodeService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [NodeService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([NodeService], (service: NodeService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/util.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { UtilService } from './util.service'; 4 | 5 | describe('UtilService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [UtilService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([UtilService], (service: UtilService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/modal.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ModalService } from './modal.service'; 4 | 5 | describe('ModalService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ModalService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ModalService], (service: ModalService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/price.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { PriceService } from './price.service'; 4 | 5 | describe('PriceService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [PriceService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([PriceService], (service: PriceService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | desktop: false, 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/services/ledger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { LedgerService } from './ledger.service'; 4 | 5 | describe('LedgerService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [LedgerService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([LedgerService], (service: LedgerService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/wallet.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { WalletService } from './wallet.service'; 4 | 5 | describe('WalletService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [WalletService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([WalletService], (service: WalletService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/work-pool.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { WorkPoolService } from './work-pool.service'; 4 | 5 | describe('WorkPoolService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [WorkPoolService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([WorkPoolService], (service: WorkPoolService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/websocket.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { WebsocketService } from './websocket.service'; 4 | 5 | describe('WebsocketService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [WebsocketService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([WebsocketService], (service: WebsocketService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/nano-block.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { BananoBlockService } from './nano-block.service'; 4 | 5 | describe('BananoBlockService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [BananoBlockService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([BananoBlockService], (service: BananoBlockService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/address-book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AddressBookService } from './address-book.service'; 4 | 5 | describe('AddressBookService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AddressBookService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([AddressBookService], (service: AddressBookService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/app-settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AppSettingsService } from './app-settings.service'; 4 | 5 | describe('AppSettingsService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AppSettingsService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([AppSettingsService], (service: AppSettingsService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/notification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { NotificationService } from './notification.service'; 4 | 5 | describe('NotificationService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [NotificationService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([NotificationService], (service: NotificationService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/components/send/send.component.css: -------------------------------------------------------------------------------- 1 | .confirm-title { 2 | font-weight: bolder; 3 | display: block; 4 | font-size: 24px; 5 | } 6 | 7 | .confirm-currency { 8 | font-weight: bolder; 9 | display: block; 10 | font-size: 20px; 11 | } 12 | 13 | .confirm-subtitle { 14 | display: block; 15 | font-size: 10px; 16 | } 17 | 18 | 19 | @media (max-width: 12450px) { 20 | .br-spacer { 21 | display: block; 22 | } 23 | } 24 | @media (max-width: 959px) { 25 | .br-spacer { 26 | display: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/services/representative.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { RepresentativeService } from './representative.service'; 4 | 5 | describe('RepresentativeService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [RepresentativeService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([RepresentativeService], (service: RepresentativeService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /desktop-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "baseUrl": ".", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | // This file is required by the index.html file and will 2 | // be executed in the renderer process for that window. 3 | // All of the Node.js APIs are available in this process. 4 | 5 | // Allow fetch to call a local file (for pow.wasm) 6 | require('electron').webFrame.registerURLSchemeAsPrivileged('file', { bypassCSP: true }); 7 | 8 | 9 | const Transport = require('@ledgerhq/hw-transport-u2f').default; 10 | 11 | window.LedgerTransport = Transport; 12 | 13 | console.log('Set the ledger transport on the window! ', window.LedgerTransport) 14 | -------------------------------------------------------------------------------- /src/app/pipes/currency-symbol.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {CurrencyPipe} from "@angular/common"; 3 | 4 | @Pipe({ 5 | name: 'currencySymbol' 6 | }) 7 | export class CurrencySymbolPipe extends CurrencyPipe implements PipeTransform { 8 | 9 | // This pipe simply shows the currency symbol ($, BTC, etc) and removes any numeric values 10 | transform(value: any, args?: any): any { 11 | let currency = super.transform(0, value, true, '1.0-2'); 12 | return currency.replace(/[0-9]/g, ''); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BananoVault", 3 | "short_name": "BananoVault", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/lib/pow/thread.js: -------------------------------------------------------------------------------- 1 | self.importScripts('pow.js'); 2 | 3 | var ready = false; 4 | 5 | Module['onRuntimeInitialized'] = function() { 6 | postMessage('ready'); 7 | }; 8 | 9 | onmessage = function(ev) { 10 | var PoW = Module.cwrap("launchPoW", 'string', ['string']); 11 | var hash = ev.data; 12 | //let generate = Module.ccall("launchPoW", 'string', ['string'], hash); 13 | var generate = PoW(hash); 14 | 15 | if (generate != "0000000000000000") { 16 | postMessage(generate); // Worker return 17 | } else { 18 | postMessage(false); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/pipes/squeeze.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'squeeze' 5 | }) 6 | export class SqueezePipe implements PipeTransform { 7 | 8 | transform(value: any, args?: any): any { 9 | const arg = args ? args.split(',') || [] : []; 10 | const openingChars = arg[0] || 9; 11 | const closingChars = arg[1] || 5; 12 | const firstChars = value.split('').slice(0, openingChars).join(''); 13 | const lastChars = value.split('').slice(-closingChars).join(''); 14 | return `${firstChars}...${lastChars}`; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pipes/account.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {UtilService} from "../services/util.service"; 3 | import {AppSettingsService} from "../services/app-settings.service"; 4 | 5 | @Pipe({ 6 | name: 'account' 7 | }) 8 | export class AccountPipe implements PipeTransform { 9 | 10 | constructor(private util: UtilService, private settings: AppSettingsService) { 11 | 12 | } 13 | 14 | transform(value: any, args?: any): any { 15 | // const val = this.util.account.setPrefix(value, this.settings.settings.displayPrefix); 16 | 17 | return value; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address-book.service'; 2 | export * from './api.service'; 3 | export * from './app-settings.service'; 4 | export * from './desktop.service'; 5 | export * from './ledger.service'; 6 | export * from './modal.service'; 7 | export * from './nano-block.service'; 8 | export * from './node.service'; 9 | export * from './notification.service'; 10 | export * from './pow.service'; 11 | export * from './price.service'; 12 | export * from './representative.service'; 13 | export * from './util.service'; 14 | export * from './wallet.service'; 15 | export * from './websocket.service'; 16 | export * from './work-pool.service'; 17 | -------------------------------------------------------------------------------- /src/app/welcome/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {WalletService} from "../services/wallet.service"; 3 | 4 | @Component({ 5 | selector: 'app-welcome', 6 | templateUrl: './welcome.component.html', 7 | styleUrls: ['./welcome.component.css'] 8 | }) 9 | export class WelcomeComponent implements OnInit { 10 | 11 | donationAccount = `ban_318syypnqcgdouy3p3ekckwmnmmyk5z3dpyq48phzndrmmspyqdqjymoo8hj`; 12 | 13 | wallet = this.walletService.wallet; 14 | isConfigured = this.walletService.isConfigured; 15 | 16 | constructor(private walletService: WalletService) { } 17 | 18 | ngOnInit() { 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/send/send.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SendComponent } from './send.component'; 4 | 5 | describe('SendComponent', () => { 6 | let component: SendComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SendComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SendComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/welcome/welcome.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WelcomeComponent } from './welcome.component'; 4 | 5 | describe('WelcomeComponent', () => { 6 | let component: WelcomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WelcomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WelcomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/receive/receive.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ReceiveComponent } from './receive.component'; 4 | 5 | describe('ReceiveComponent', () => { 6 | let component: ReceiveComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ReceiveComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ReceiveComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-desktop 6 | /tmp 7 | /out-tsc 8 | /desktop-app/dist 9 | /desktop-app/build 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | yarn-error.log 38 | /typings 39 | 40 | # e2e 41 | /e2e/*.js 42 | /e2e/*.map 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/app/components/accounts/accounts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountsComponent } from './accounts.component'; 4 | 5 | describe('AccountsComponent', () => { 6 | let component: AccountsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AccountsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AccountsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/services/desktop.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {NotificationService} from "./notification.service"; 3 | 4 | @Injectable() 5 | export class DesktopService { 6 | 7 | _ipc: any; 8 | 9 | constructor(private notifications: NotificationService) { 10 | } 11 | 12 | connect() { 13 | if (window.require) { 14 | try { 15 | this._ipc = window.require('electron').ipcRenderer; 16 | } catch (e) { 17 | throw e; 18 | } 19 | } else { 20 | } 21 | } 22 | 23 | on(channel: string, listener) { 24 | if (!this._ipc) return; 25 | this._ipc.on(channel, listener); 26 | } 27 | 28 | send(channel: string, ...args) { 29 | if (!this._ipc) return; 30 | this._ipc.send(channel, ...args); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/app/components/address-book/address-book.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddressBookComponent } from './address-book.component'; 4 | 5 | describe('AddressBookComponent', () => { 6 | let component: AddressBookComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddressBookComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddressBookComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/configure-app/configure-app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfigureAppComponent } from './configure-app.component'; 4 | 5 | describe('ConfigureAppComponent', () => { 6 | let component: ConfigureAppComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ConfigureAppComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ConfigureAppComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/import-wallet/import-wallet.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ImportWalletComponent } from './import-wallet.component'; 4 | 5 | describe('ImportWalletComponent', () => { 6 | let component: ImportWalletComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ImportWalletComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ImportWalletComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/manage-wallet/manage-wallet.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ManageWalletComponent } from './manage-wallet.component'; 4 | 5 | describe('ManageWalletComponent', () => { 6 | let component: ManageWalletComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ManageWalletComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ManageWalletComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/wallet-widget/wallet-widget.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WalletWidgetComponent } from './wallet-widget.component'; 4 | 5 | describe('WalletWidgetComponent', () => { 6 | let component: WalletWidgetComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WalletWidgetComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WalletWidgetComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/notifications/notifications.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotificationsComponent } from './notifications.component'; 4 | 5 | describe('NotificationsComponent', () => { 6 | let component: NotificationsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotificationsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotificationsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/components/account-details/account-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountDetailsComponent } from './account-details.component'; 4 | 5 | describe('AccountDetailsComponent', () => { 6 | let component: AccountDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AccountDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AccountDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/configure-wallet/configure-wallet.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfigureWalletComponent } from './configure-wallet.component'; 4 | 5 | describe('ConfigureWalletComponent', () => { 6 | let component: ConfigureWalletComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ConfigureWalletComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ConfigureWalletComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/representatives/representatives.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RepresentativesComponent } from './representatives.component'; 4 | 5 | describe('RepresentativesComponent', () => { 6 | let component: RepresentativesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RepresentativesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RepresentativesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/change-rep-widget/change-rep-widget.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChangeRepWidgetComponent } from './change-rep-widget.component'; 4 | 5 | describe('ChangeRepWidgetComponent', () => { 6 | let component: ChangeRepWidgetComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChangeRepWidgetComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChangeRepWidgetComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/helpers/banano-account-id/banano-account-id.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BananoAccountIdComponent } from './banano-account-id.component'; 4 | 5 | describe('BananoAccountIdComponent', () => { 6 | let component: BananoAccountIdComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BananoAccountIdComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BananoAccountIdComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/import-address-book/import-address-book.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ImportAddressBookComponent } from './import-address-book.component'; 4 | 5 | describe('ImportAddressBookComponent', () => { 6 | let component: ImportAddressBookComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ImportAddressBookComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ImportAddressBookComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/transaction-details/transaction-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TransactionDetailsComponent } from './transaction-details.component'; 4 | 5 | describe('TransactionDetailsComponent', () => { 6 | let component: TransactionDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TransactionDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TransactionDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/representatives/representatives.component.spec.ts~338597e99ae8ca659e49a2ed96fa7c6f1e4baf38: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RepresentativesComponent } from './representatives.component'; 4 | 5 | describe('RepresentativesComponent', () => { 6 | let component: RepresentativesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RepresentativesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RepresentativesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pipes/fiat.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {CurrencyPipe} from "@angular/common"; 3 | import {BigNumber} from 'bignumber.js'; 4 | 5 | @Pipe({ 6 | name: 'fiat' 7 | }) 8 | export class FiatPipe extends CurrencyPipe implements PipeTransform { 9 | // transform(value: any, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | boolean, digits?: string, locale?: string): string | null; 10 | 11 | transform(value: any, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | boolean, digits?: string, locale?: string): any { 12 | if (currencyCode === '') { 13 | return ``; 14 | } 15 | if (currencyCode === 'BTC') { 16 | return `BTC ${new BigNumber(new Number(value).toFixed(4) || 0).toFixed(6)}`; 17 | } 18 | return super.transform(value, currencyCode, 'symbol-narrow', digits, locale); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/manage-representatives/manage-representatives.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ManageRepresentativesComponent } from './manage-representatives.component'; 4 | 5 | describe('ManageRepresentativesComponent', () => { 6 | let component: ManageRepresentativesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ManageRepresentativesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ManageRepresentativesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/helpers/banano-account-id/banano-account-id.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-banano-account-id', 5 | templateUrl: './banano-account-id.component.html', 6 | styleUrls: ['./banano-account-id.component.css'] 7 | }) 8 | export class BananoAccountIdComponent implements OnInit { 9 | 10 | @Input('accountID') accountID: string; 11 | 12 | firstCharacters = ''; 13 | lastCharacters = ''; 14 | 15 | constructor() { } 16 | 17 | ngOnInit() { 18 | const accountID = this.accountID; 19 | const openingChars = 9; 20 | const closingChars = 5; 21 | this.firstCharacters = accountID.split('').slice(0, openingChars).join(''); 22 | this.lastCharacters = accountID.split('').slice(-closingChars).join(''); 23 | // return `${firstChars}.....${lastChars}`; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/node.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {NotificationService} from "./notification.service"; 3 | 4 | @Injectable() 5 | export class NodeService { 6 | 7 | node = { 8 | status: null, // null - loading, false - offline, true - online 9 | }; 10 | 11 | constructor(private notifications: NotificationService) { } 12 | 13 | setOffline() { 14 | if (this.node.status === false) return; // Already offline 15 | this.node.status = false; 16 | 17 | this.notifications.sendError(`Unable to connect to the Banano node, your balances may be inaccurate!`, { identifier: 'node-offline', length: 0 }); 18 | } 19 | 20 | setOnline() { 21 | if (this.node.status) return; // Already online 22 | 23 | this.node.status = true; 24 | this.notifications.removeNotification('node-offline'); 25 | } 26 | 27 | setLoading() { 28 | this.node.status = null; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/app/components/configure-wallet/configure-wallet.component.css: -------------------------------------------------------------------------------- 1 | pre.mnemonic { 2 | white-space: pre-wrap; 3 | } 4 | pre.mnemonic:before { 5 | counter-reset: listing; 6 | } 7 | pre.mnemonic span.mnemonic-word { 8 | counter-increment: listing; 9 | } 10 | pre.mnemonic span.mnemonic-word::before { 11 | content: counter(listing) ". "; 12 | display: inline-block; 13 | width: 8em; /* doesn't work */ 14 | padding-left: auto; /* doesn't work */ 15 | margin-left: auto; /* doesn't work */ 16 | text-align: right; /* doesn't work */ 17 | } 18 | .mne-num { 19 | color: #CCC; 20 | display: inline-block; 21 | width: 13px; 22 | font-size: 10px; 23 | text-align: left; 24 | -webkit-touch-callout: none; /* iOS Safari */ 25 | -webkit-user-select: none; /* Safari */ 26 | -khtml-user-select: none; /* Konqueror HTML */ 27 | -moz-user-select: none; /* Firefox */ 28 | -ms-user-select: none; /* Internet Explorer/Edge */ 29 | user-select: none; /* Non-prefixed version, currently 30 | supported by Chrome and Opera */ 31 | } 32 | 33 | .mne-box { 34 | max-width: 410px; 35 | } 36 | 37 | .mne-cont { 38 | display: block; 39 | border-bottom: 1px dashed #449af373; 40 | } 41 | 42 | .mne-word { 43 | display: inline-block; 44 | min-width: 100px; 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/lib/pow/startThreads.js: -------------------------------------------------------------------------------- 1 | var pow_initiate = function(threads, worker_path) { 2 | if (typeof worker_path == 'undefined') { worker_path = ''; } 3 | if (isNaN(threads)) { threads = self.navigator.hardwareConcurrency - 1; } 4 | var workers = []; 5 | for (let i = 0; i < threads; i++) { 6 | workers[i] = new Worker(worker_path + 'thread.js'); 7 | } 8 | return workers; 9 | } 10 | 11 | var pow_start = function(workers, hash) { 12 | if ((hash instanceof Uint8Array) && (hash.length == 32)) { 13 | var threads = workers.length; 14 | for (let i = 0; i < threads; i++) { 15 | workers[i].postMessage(hash); 16 | } 17 | } 18 | } 19 | 20 | var pow_terminate = function(workers) { 21 | var threads = workers.length; 22 | for (let i = 0; i < threads; i++) { 23 | workers[i].terminate(); 24 | } 25 | } 26 | 27 | var pow_callback = function(workers, hash, ready, callback) { 28 | if ( (hash.length == 64) && (typeof callback == 'function')) { 29 | var threads = workers.length; 30 | for (let i = 0; i < threads; i++) { 31 | workers[i].onmessage = function(e) { 32 | result = e.data; 33 | if(result == 'ready') { 34 | workers[i].postMessage(hash); 35 | ready(); 36 | } else if (result !== false && result != "0000000000000000") { 37 | pow_terminate (workers); 38 | callback (result); 39 | } 40 | else workers[i].postMessage(hash); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/app/services/price.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {BehaviorSubject} from "rxjs/BehaviorSubject"; 4 | 5 | @Injectable() 6 | export class PriceService { 7 | apiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=banano&include_market_cap=false&include_24hr_vol=false&include_24hr_change=false&include_last_updated_at=false&vs_currencies=btc,usd`; 8 | 9 | price = { 10 | lastPrice: 0.00, 11 | lastPriceBTC: 0.00000000, 12 | }; 13 | lastPrice$ = new BehaviorSubject(1); 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | async getPrice(currency = 'USD') { 18 | 19 | if (!currency) return; // No currency defined, do not refetch 20 | 21 | const convertString = currency !== 'USD' && currency !== 'BTC' ? `,${currency}` : ``; 22 | 23 | const response: any = await this.http.get(`${this.apiUrl}${convertString}`).toPromise(); 24 | 25 | // if (!Response || !Response.length) { 26 | // return this.price.lastPrice; 27 | // } 28 | 29 | const quote = response; 30 | 31 | const currencyPrice = quote.banano[currency.toLowerCase()]; 32 | const usdPrice = quote.banano.usd; 33 | const btcPrice = quote.banano.btc; 34 | 35 | 36 | this.price.lastPrice = currencyPrice; 37 | this.price.lastPriceBTC = btcPrice; 38 | 39 | this.lastPrice$.next(currencyPrice); 40 | 41 | return this.price.lastPrice; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "bananovault" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "pow.wasm" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "scripts": [], 22 | "environmentSource": "environments/environment.ts", 23 | "environments": { 24 | "dev": "environments/environment.ts", 25 | "prod": "environments/environment.prod.ts", 26 | "desktop": "environments/environment.desktop.ts" 27 | } 28 | } 29 | ], 30 | "e2e": { 31 | "protractor": { 32 | "config": "./protractor.conf.js" 33 | } 34 | }, 35 | "lint": [ 36 | { 37 | "project": "src/tsconfig.app.json", 38 | "exclude": "**/node_modules/**" 39 | }, 40 | { 41 | "project": "src/tsconfig.spec.json", 42 | "exclude": "**/node_modules/**" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json", 46 | "exclude": "**/node_modules/**" 47 | } 48 | ], 49 | "test": { 50 | "karma": { 51 | "config": "./karma.conf.js" 52 | } 53 | }, 54 | "defaults": { 55 | "styleExt": "css", 56 | "component": {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/components/change-rep-widget/change-rep-widget.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit} from '@angular/core'; 2 | import {RepresentativeService} from "../../services/representative.service"; 3 | import {Router} from "@angular/router"; 4 | 5 | @Component({ 6 | selector: 'app-change-rep-widget', 7 | templateUrl: './change-rep-widget.component.html', 8 | styleUrls: ['./change-rep-widget.component.css'] 9 | }) 10 | export class ChangeRepWidgetComponent implements OnInit, AfterViewInit { 11 | 12 | representatives = this.repService.changeableReps; 13 | modalElement = null; 14 | 15 | constructor(private repService: RepresentativeService, private router: Router) { } 16 | 17 | async ngOnInit() { 18 | await this.repService.detectChangeableReps(); 19 | 20 | this.repService.changeableReps$.subscribe(reps => { 21 | this.representatives = reps; 22 | }); 23 | } 24 | 25 | ngAfterViewInit() { 26 | const UIkit = window['UIkit']; 27 | this.modalElement = UIkit.modal('#change-rep-modal'); 28 | } 29 | 30 | showModal() { 31 | this.modalElement.show(); 32 | } 33 | 34 | closeModal() { 35 | this.modalElement.hide(); 36 | } 37 | 38 | changeReps() { 39 | const allAccounts = this.representatives.map(rep => rep.accounts.map(a => a.id).join(',')).join(','); 40 | 41 | this.modalElement.hide(); 42 | 43 | this.router.navigate(['/representatives'], { queryParams: { hideOverview: true, accounts: allAccounts, showRecommended: true } }); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode9.4 5 | language: node_js 6 | node_js: "8" 7 | env: 8 | - ELECTRON_CACHE=$HOME/.cache/electron 9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 10 | 11 | - os: linux 12 | services: docker 13 | language: generic 14 | 15 | cache: 16 | directories: 17 | - node_modules 18 | - $HOME/.cache/electron 19 | - $HOME/.cache/electron-builder 20 | 21 | script: 22 | - | 23 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 24 | docker run --rm \ 25 | --env-file <(env | grep -vE '\r|\n' | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') \ 26 | -v ${PWD}:/project \ 27 | -v ~/.cache/electron:/root/.cache/electron \ 28 | -v ~/.cache/electron-builder:/root/.cache/electron-builder \ 29 | electronuserland/builder:wine \ 30 | /bin/bash -c "apt-get update && apt-get install --no-install-recommends -y build-essential libudev-dev libusb-1.0-0 libusb-1.0-0-dev libopenjp2-tools gcc-multilib g++-multilib && npm install -g node-gyp && npm install --link-duplicates --pure-lockfile --build-from-source && cd node_modules/node-hid && git submodule update --init --recursive && npm install && cd ../usb && npm install && cd ../.. && yarn release --linux" 31 | else 32 | yarn release 33 | fi 34 | before_cache: 35 | - rm -rf $HOME/.cache/electron-builder/wine 36 | 37 | branches: 38 | except: 39 | - "/^v\\d+\\.\\d+\\.\\d+$/" 40 | -------------------------------------------------------------------------------- /src/app/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as Rx from 'rxjs'; 3 | 4 | type NotificationType = 'info'|'success'|'warning'|'error'; 5 | 6 | @Injectable() 7 | export class NotificationService { 8 | 9 | notifications$ = new Rx.BehaviorSubject(null); 10 | removeNotification$ = new Rx.BehaviorSubject(null); 11 | 12 | constructor() { } 13 | 14 | // This provides an entry point for all components to send notifications. 15 | // It exposes an observable that the actual component uses to grab new notifications 16 | 17 | sendNotification(type: NotificationType, message: string, options = {}) { 18 | this.notifications$.next({ type, message, options }); 19 | } 20 | 21 | removeNotification(identifier: string) { 22 | this.removeNotification$.next(identifier); 23 | } 24 | 25 | sendInfo(message:string, options = {}) { 26 | this.sendNotification('info', message, options); 27 | } 28 | sendSuccess(message:string, options = {}) { 29 | this.sendNotification('success', message, options); 30 | } 31 | sendWarning(message:string, options = {}) { 32 | this.sendNotification('warning', message, options); 33 | } 34 | sendError(message:string, options = {}) { 35 | this.sendNotification('error', message, options); 36 | } 37 | 38 | // Custom notification functions - these are re-used in multiple paces through the app 39 | sendLedgerChromeWarning() { 40 | this.sendWarning( 41 | `Notice: You may experience issues using a Ledger device with Certian browsers. ` + 42 | `If you do please try Brave/Opera browser`, 43 | { length: 0, identifier: 'chrome-ledger' } 44 | ); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/components/import-wallet/import-wallet.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Import Wallet

4 | 5 |
6 |
7 |

Bad Import Data

8 |
9 |
10 |

There was an issue reading the import data for your wallet. Double check the address and try again.

11 |
12 |
13 | 14 |
15 |
16 |

Confirm Wallet Password

17 |
18 |
19 |

Almost finished - just enter your wallet password to decrypt your wallet!

20 |
21 |
22 | 23 |
24 |
25 |
26 | 29 |
30 | 31 |
32 |
33 |

Wallet Imported

34 |
35 |
36 |

Your wallet has been successfully imported!

37 |
38 | 41 |
42 | 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .body { 2 | background: #000; 3 | } 4 | 5 | .app { 6 | font-family: 'Lato', sans-serif; 7 | background: #212124; 8 | /*height: 100%;*/ 9 | } 10 | 11 | .title-block { 12 | background: #333333; 13 | } 14 | 15 | .title-block .title { 16 | font-size: 24px; 17 | display: block; 18 | padding-top: 10px; 19 | padding-bottom: 10px; 20 | color: #fbdd11; 21 | } 22 | .title-block .title:hover { 23 | text-decoration: none; 24 | } 25 | .title-block .balance-text { 26 | display: block; 27 | font-size: 12px; 28 | } 29 | 30 | .title-block .balance { 31 | display: block; 32 | font-size: 18px; 33 | margin-top: -8px; 34 | } 35 | 36 | .left-nav { 37 | font-size: 18px; 38 | } 39 | 40 | .left-nav .active { 41 | color: #fbdd11; 42 | } 43 | 44 | .left-nav a { 45 | color: #cbcbcb; 46 | } 47 | 48 | .left-nav a:hover { 49 | color: #fbdd11; 50 | } 51 | 52 | .wallet-bar a { 53 | color: #fff; 54 | } 55 | 56 | .footer { 57 | color: #d4d5d5; 58 | font-size: 12px; 59 | } 60 | 61 | .footer a { 62 | color: #fff; 63 | } 64 | 65 | .footer-link { 66 | margin-left: 5px; 67 | margin-right: 5px; 68 | opacity: 0.5; 69 | } 70 | .footer-link:hover { 71 | opacity: 1; 72 | } 73 | 74 | .confirm-title { 75 | font-weight: bolder; 76 | display: block; 77 | font-size: 24px; 78 | } 79 | 80 | .confirm-currency { 81 | font-weight: bolder; 82 | display: block; 83 | font-size: 20px; 84 | } 85 | 86 | .confirm-subtitle { 87 | display: block; 88 | font-size: 10px; 89 | } 90 | 91 | 92 | @media (max-width: 12450px) { 93 | .content-container { 94 | padding-left: 15px; 95 | padding-right: 10px; 96 | } 97 | } 98 | @media (max-width: 639px) { 99 | .content-container { 100 | padding-left: 30px; 101 | padding-right: inherit; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/app/pipes/rai.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {AppSettingsService} from "../services/app-settings.service"; 3 | 4 | @Pipe({ 5 | name: 'rai' 6 | }) 7 | export class RaiPipe implements PipeTransform { 8 | precision = 2; 9 | 10 | banano = 100000000000000000000000000000; 11 | banoshi = 1000000000000000000000000000; 12 | raw = 1; 13 | 14 | transform(value: any, args?: any): any { 15 | const opts = args.split(','); 16 | let denomination = opts[0] || 'banano'; 17 | const hideText = opts[1] || false; 18 | 19 | switch (denomination.toLowerCase()) { 20 | default: 21 | 22 | case 'banano': 23 | const hasRawValue = (value / this.banoshi) % 1; 24 | if (hasRawValue) { 25 | const newVal = value / this.banano < 0.01 ? 0 : value / this.banano; // New more precise toFixed function, but bugs on huge raw numbers 26 | return `${this.toFixed(newVal, this.precision)}${!hideText ? ' BANANO': ''}`; 27 | } else { 28 | return `${(value / this.banano).toFixed(2)}${!hideText ? ' BANANO': ''}`; 29 | } 30 | case 'banoshi': return `${(value / this.banoshi).toFixed(0)}${!hideText ? ' banoshi': ''}`; 31 | 32 | case 'raw': return `${value}${!hideText ? ' raw': ''}`; 33 | case 'dynamic': 34 | const rai = (value / this.raw); 35 | if (rai >= 1000000) { 36 | return `${(value / this.banano).toFixed(this.precision)}${!hideText ? ' mRai': ''}`; 37 | } else if (rai >= 1000) { 38 | return `${(value / this.banoshi).toFixed(this.precision)}${!hideText ? ' kRai': ''}`; 39 | } else if (rai >= 0.00001) { 40 | return `${(value / this.raw).toFixed(this.precision)}${!hideText ? ' Rai': ''}`; 41 | } else if (rai === 0) { 42 | return `${value}${!hideText ? ' mRai': ''}`; 43 | } else { 44 | return `${value}${!hideText ? ' raw': ''}`; 45 | } 46 | } 47 | } 48 | 49 | toFixed(num, fixed) { 50 | if (isNaN(num)) return 0; 51 | var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?'); 52 | return num.toString().match(re)[0]; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BananoVault 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/app/components/import-wallet/import-wallet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ActivatedRoute, ActivatedRouteSnapshot} from "@angular/router"; 3 | import {NotificationService} from "../../services/notification.service"; 4 | import * as CryptoJS from "crypto-js"; 5 | import {WalletService} from "../../services/wallet.service"; 6 | 7 | @Component({ 8 | selector: 'app-import-wallet', 9 | templateUrl: './import-wallet.component.html', 10 | styleUrls: ['./import-wallet.component.css'] 11 | }) 12 | export class ImportWalletComponent implements OnInit { 13 | activePanel = 'error'; 14 | 15 | walletPassword = ''; 16 | validImportData = false; 17 | importData: any = null; 18 | 19 | constructor(private route: ActivatedRoute, private notifications: NotificationService, private wallet: WalletService) { } 20 | 21 | ngOnInit() { 22 | const importData = this.route.snapshot.fragment; 23 | if (!importData || !importData.length) return this.importDataError(`No import data found. Check your link and try again.`); 24 | 25 | const decodedData = atob(importData); 26 | 27 | try { 28 | const importBlob = JSON.parse(decodedData); 29 | if (!importBlob || !importBlob.seed) return this.importDataError(`Bad import data. Check your link and try again.`); 30 | this.validImportData = true; 31 | this.importData = importBlob; 32 | this.activePanel = 'import'; 33 | } catch (err) { 34 | return this.importDataError(`Unable to decode import data. Check your link and try again.`); 35 | } 36 | } 37 | 38 | importDataError(message) { 39 | this.activePanel = 'error'; 40 | return this.notifications.sendError(message); 41 | } 42 | 43 | async decryptWallet() { 44 | // Attempt to decrypt the seed value using the password 45 | try { 46 | const decryptedBytes = CryptoJS.AES.decrypt(this.importData.seed, this.walletPassword); 47 | const decryptedSeed = decryptedBytes.toString(CryptoJS.enc.Utf8); 48 | if (!decryptedSeed || decryptedSeed.length !== 64) { 49 | this.walletPassword = ''; 50 | return this.notifications.sendError(`Invalid password, please try again`); 51 | } 52 | 53 | await this.wallet.loadImportedWallet(decryptedSeed, this.walletPassword, this.importData.accountsIndex || 0); 54 | this.activePanel = 'imported'; 55 | 56 | } catch (err) { 57 | this.walletPassword = ''; 58 | return this.notifications.sendError(`Invalid password, please try again`); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/app/components/notifications/notifications.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {NotificationService} from "../../services/notification.service"; 3 | 4 | @Component({ 5 | selector: 'app-notifications', 6 | templateUrl: './notifications.component.html', 7 | styleUrls: ['./notifications.component.css'] 8 | }) 9 | export class NotificationsComponent implements OnInit { 10 | 11 | notificationLength = 5000; 12 | 13 | notifications: any[] = []; 14 | constructor(private notificationService: NotificationService) { } 15 | 16 | ngOnInit() { 17 | this.notificationService.notifications$.subscribe(notification => { 18 | if (!notification) return; // Default value 19 | 20 | // Check the options 21 | const length = notification.options.hasOwnProperty('length') ? notification.options.length : this.notificationLength; 22 | const identifier = notification.options.identifier || null; 23 | 24 | // Stop duplicates 25 | if (identifier) { 26 | const existingNotification = this.notifications.find(n => n.identifier === identifier); 27 | if (existingNotification) return; 28 | } 29 | 30 | const newNotification = { 31 | type: notification.type, 32 | message: notification.message, 33 | cssClass: this.getCssClass(notification.type), 34 | identifier: identifier, 35 | length: length, 36 | }; 37 | 38 | this.notifications.push(newNotification); 39 | if (length) { 40 | setTimeout(() => this.removeNotification(newNotification), length); 41 | } 42 | }); 43 | 44 | this.notificationService.removeNotification$.subscribe(identifier => { 45 | if (!identifier) return; 46 | 47 | const existingNotification = this.notifications.find(n => n.identifier === identifier); 48 | if (existingNotification) { 49 | this.removeNotification(existingNotification); 50 | } 51 | }); 52 | } 53 | 54 | private removeNotification(notification) { 55 | const existingNotification = this.notifications.findIndex(n => n === notification); 56 | if (existingNotification !== -1) { 57 | this.notifications.splice(existingNotification, 1); 58 | } 59 | } 60 | 61 | private getCssClass(type) { 62 | switch (type) { 63 | default: 64 | case 'info': return 'uk-alert-primary'; 65 | case 'success': return 'uk-alert-success'; 66 | case 'warning': return 'uk-alert-warning'; 67 | case 'error': return 'uk-alert-danger'; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | import 'babel-polyfill'; 68 | -------------------------------------------------------------------------------- /src/app/services/work-pool.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {PowService} from "./pow.service"; 3 | import {NotificationService} from "./notification.service"; 4 | 5 | @Injectable() 6 | export class WorkPoolService { 7 | storeKey = `bananovault-workcache`; 8 | 9 | cacheLength = 25; 10 | workCache = []; 11 | 12 | constructor(private pow: PowService, private notifications: NotificationService) { } 13 | 14 | public workExists(hash) { 15 | return !!this.workCache.find(p => p.hash == hash); 16 | } 17 | 18 | // A simple helper, which doesn't wait for a response (Used for pre-loading work) 19 | public addWorkToCache(hash) { 20 | this.getWork(hash); 21 | } 22 | 23 | // Remove a hash from from the cache 24 | public removeFromCache(hash) { 25 | const cachedIndex = this.workCache.findIndex(p => p.hash == hash); 26 | if (cachedIndex === -1) return; 27 | 28 | this.workCache.splice(cachedIndex, 1); 29 | this.saveWorkCache(); 30 | } 31 | 32 | public clearCache() { 33 | this.workCache = []; 34 | this.saveWorkCache(); 35 | 36 | return true; 37 | } 38 | 39 | public deleteCache() { 40 | this.workCache = []; 41 | localStorage.removeItem(this.storeKey); 42 | } 43 | 44 | // Get work for a hash. Uses the cache, or the current setting for generating it. 45 | public async getWork(hash) { 46 | const cached = this.workCache.find(p => p.hash == hash); 47 | if (cached && cached.work) return cached.work; 48 | 49 | const work = await this.pow.getPow(hash); 50 | if (!work) { 51 | this.notifications.sendWarning(`Failed to retrieve work for ${hash}. Try a different PoW method.`); 52 | return null; 53 | } 54 | 55 | this.workCache.push({ hash, work }); 56 | if (this.workCache.length >= this.cacheLength) this.workCache.shift(); // Prune if we are at max length 57 | this.saveWorkCache(); 58 | 59 | return work; 60 | } 61 | 62 | /** 63 | * Save the work cache to localStorage 64 | */ 65 | private saveWorkCache() { 66 | // Remove duplicates 67 | this.workCache = this.workCache.reduce((previous, current) => { 68 | if (!previous.find(p => p.hash == current.hash)) previous.push(current); 69 | return previous; 70 | }, []); 71 | 72 | localStorage.setItem(this.storeKey, JSON.stringify(this.workCache)); 73 | } 74 | 75 | /** 76 | * Load the work cache from localStorage 77 | */ 78 | public loadWorkCache() { 79 | let workCache = []; 80 | const workCacheStore = localStorage.getItem(this.storeKey); 81 | if (workCacheStore) { 82 | workCache = JSON.parse(workCacheStore); 83 | } 84 | this.workCache = workCache; 85 | 86 | return this.workCache; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/services/app-settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import set = Reflect.set; 3 | 4 | export type WalletStore = 'localStorage'|'none'; 5 | export type PoWSource = 'server'|'clientCPU'|'clientWebGL'|'best'; 6 | 7 | interface AppSettings { 8 | displayDenomination: string; 9 | // displayPrefix: string | null; 10 | walletStore: string; 11 | displayCurrency: string; 12 | defaultRepresentative: string | null; 13 | lockOnClose: number; 14 | lockInactivityMinutes: number; 15 | powSource: PoWSource; 16 | serverName: string; 17 | serverAPI: string | null; 18 | serverNode: string | null; 19 | serverWS: string | null; 20 | minimumReceive: string | null; 21 | } 22 | 23 | @Injectable() 24 | export class AppSettingsService { 25 | storeKey = `bananovault-appsettings`; 26 | 27 | settings: AppSettings = { 28 | displayDenomination: 'banano', 29 | // displayPrefix: 'xrb', 30 | walletStore: 'localStorage', 31 | displayCurrency: 'USD', 32 | defaultRepresentative: null, 33 | lockOnClose: 1, 34 | lockInactivityMinutes: 30, 35 | powSource: 'best', 36 | serverName: 'bananovault', 37 | serverAPI: null, 38 | serverNode: null, 39 | serverWS: null, 40 | minimumReceive: null, 41 | }; 42 | 43 | constructor() { } 44 | 45 | loadAppSettings() { 46 | let settings: AppSettings = this.settings; 47 | const settingsStore = localStorage.getItem(this.storeKey); 48 | if (settingsStore) { 49 | settings = JSON.parse(settingsStore); 50 | } 51 | this.settings = Object.assign(this.settings, settings); 52 | 53 | return this.settings; 54 | } 55 | 56 | saveAppSettings() { 57 | localStorage.setItem(this.storeKey, JSON.stringify(this.settings)); 58 | } 59 | 60 | getAppSetting(key) { 61 | return this.settings[key] || null; 62 | } 63 | 64 | setAppSetting(key, value) { 65 | this.settings[key] = value; 66 | this.saveAppSettings(); 67 | } 68 | 69 | setAppSettings(settingsObject) { 70 | for (let key in settingsObject) { 71 | if (!settingsObject.hasOwnProperty(key)) continue; 72 | this.settings[key] = settingsObject[key]; 73 | } 74 | 75 | this.saveAppSettings(); 76 | } 77 | 78 | clearAppSettings() { 79 | localStorage.removeItem(this.storeKey); 80 | this.settings = { 81 | displayDenomination: 'banano', 82 | // displayPrefix: 'xrb', 83 | walletStore: 'localStorage', 84 | displayCurrency: 'USD', 85 | defaultRepresentative: null, 86 | lockOnClose: 1, 87 | lockInactivityMinutes: 30, 88 | powSource: 'best', 89 | serverName: 'bananovault', 90 | serverNode: null, 91 | serverAPI: null, 92 | serverWS: null, 93 | minimumReceive: null, 94 | }; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import {WelcomeComponent} from "./welcome/welcome.component"; 5 | import {ConfigureWalletComponent} from "./components/configure-wallet/configure-wallet.component"; 6 | import {AccountsComponent} from "./components/accounts/accounts.component"; 7 | import {SendComponent} from "./components/send/send.component"; 8 | import {AddressBookComponent} from "./components/address-book/address-book.component"; 9 | import {ReceiveComponent} from "./components/receive/receive.component"; 10 | import {ManageWalletComponent} from "./components/manage-wallet/manage-wallet.component"; 11 | import {ConfigureAppComponent} from "./components/configure-app/configure-app.component"; 12 | import {AccountDetailsComponent} from "./components/account-details/account-details.component"; 13 | import {TransactionDetailsComponent} from "./components/transaction-details/transaction-details.component"; 14 | import {ImportWalletComponent} from "./components/import-wallet/import-wallet.component"; 15 | import { ImportAddressBookComponent } from './components/import-address-book/import-address-book.component'; 16 | import {RepresentativesComponent} from "./components/representatives/representatives.component"; 17 | 18 | import { environment } from '../environments/environment'; 19 | import {ManageRepresentativesComponent} from "./components/manage-representatives/manage-representatives.component"; 20 | 21 | const routes: Routes = [ 22 | { path: '', component: WelcomeComponent }, 23 | { path: 'accounts', component: AccountsComponent }, 24 | { path: 'account/:account', component: AccountDetailsComponent }, 25 | { path: 'address-book', component: AddressBookComponent }, 26 | { path: 'configure-wallet', component: ConfigureWalletComponent }, 27 | { path: 'configure-app', component: ConfigureAppComponent }, 28 | { path: 'import-address-book', component: ImportAddressBookComponent }, 29 | { path: 'import-wallet', component: ImportWalletComponent }, 30 | { path: 'manage-wallet', component: ManageWalletComponent }, 31 | { path: 'send', component: SendComponent }, 32 | { path: 'receive', component: ReceiveComponent }, 33 | { path: 'representatives', component: RepresentativesComponent }, 34 | { path: 'manage-representatives', component: ManageRepresentativesComponent }, 35 | { path: 'transaction/:transaction', component: TransactionDetailsComponent }, 36 | ]; 37 | 38 | @NgModule({ 39 | imports: [ 40 | RouterModule.forRoot(routes, { useHash: environment.desktop }), // On the desktop apps, use hashes so it works properly using only index.html 41 | ], 42 | declarations: [], 43 | exports: [RouterModule] 44 | }) 45 | export class AppRoutingModule { } 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/app/services/address-book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {BehaviorSubject} from 'rxjs'; 3 | 4 | 5 | interface AddressBookEntry { 6 | account: string; 7 | name: string; 8 | } 9 | 10 | 11 | @Injectable() 12 | export class AddressBookService { 13 | storeKey = `nanovault-addressbook`; 14 | 15 | addressBook: AddressBookEntry[] = []; 16 | 17 | addressBook$ = new BehaviorSubject([]); 18 | 19 | constructor() { } 20 | 21 | loadAddressBook() { 22 | let addressBook = []; 23 | const addressBookStore = localStorage.getItem(this.storeKey); 24 | if (addressBookStore) { 25 | addressBook = JSON.parse(addressBookStore); 26 | } 27 | this.addressBook = addressBook; 28 | this.addressBook$.next(this.addressBook); 29 | 30 | return this.addressBook; 31 | } 32 | 33 | async saveAddress(account, name) { 34 | const existingName = this.addressBook.find(a => a.name.toLowerCase() === name.toLowerCase()); 35 | if (existingName) throw new Error(`Name already exists in the address book`); 36 | 37 | const existingAccount = this.addressBook.find(a => a.account.toLowerCase() === account.toLowerCase()); 38 | if (existingAccount) { 39 | existingAccount.name = name; 40 | } else { 41 | this.addressBook.push({ account, name }); 42 | } 43 | this.saveAddressBook(); 44 | this.addressBook$.next(this.addressBook); 45 | 46 | } 47 | 48 | deleteAddress(account) { 49 | const existingAccountIndex = this.addressBook.findIndex(a => a.account.toLowerCase() === account.toLowerCase()); 50 | if (existingAccountIndex === -1) return; 51 | 52 | this.addressBook.splice(existingAccountIndex, 1); 53 | 54 | this.saveAddressBook(); 55 | 56 | this.addressBook$.next(this.addressBook); 57 | } 58 | 59 | saveAddressBook(): void { 60 | localStorage.setItem(this.storeKey, JSON.stringify(this.addressBook)); 61 | } 62 | 63 | clearAddressBook(): void { 64 | this.addressBook = []; 65 | this.addressBook$.next(this.addressBook); 66 | localStorage.removeItem(this.storeKey); 67 | } 68 | 69 | setAddressBookOrder(addressList) { 70 | this.addressBook = addressList 71 | .map(address => ({ 72 | account: address, 73 | name: this.getAccountName(address) 74 | })) 75 | .filter(entry => entry.name !== null); 76 | 77 | this.saveAddressBook(); 78 | this.addressBook$.next(this.addressBook); 79 | } 80 | 81 | getAccountName(account: string): string|null { 82 | if (!account || !account.length) return null; 83 | const match = this.addressBook.find(a => a.account.toLowerCase() === account.toLowerCase()); 84 | return match && match.name || null; 85 | } 86 | 87 | nameExists(name: string): boolean { 88 | return this.addressBook.findIndex(a => a.name.toLowerCase() === name.toLowerCase()) !== -1; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/app/components/import-address-book/import-address-book.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {NotificationService} from "../../services/notification.service"; 3 | import {ActivatedRoute} from "@angular/router"; 4 | import {AddressBookService} from "../../services/address-book.service"; 5 | 6 | @Component({ 7 | selector: 'app-import-address-book', 8 | templateUrl: './import-address-book.component.html', 9 | styleUrls: ['./import-address-book.component.css'] 10 | }) 11 | export class ImportAddressBookComponent implements OnInit { 12 | activePanel = 'error'; 13 | 14 | validImportData = false; 15 | importData: any = null; 16 | 17 | conflictingEntries = 0; 18 | newEntries = 0; 19 | existingEntries = 0; 20 | 21 | constructor(private route: ActivatedRoute, private notifications: NotificationService, private addressBook: AddressBookService) { } 22 | 23 | ngOnInit() { 24 | const importData = this.route.snapshot.fragment; 25 | if (!importData || !importData.length) return this.importDataError(`No import data found. Check your link and try again.`); 26 | 27 | const decodedData = atob(importData); 28 | 29 | try { 30 | const importBlob = JSON.parse(decodedData); 31 | if (!importBlob || !importBlob.length) return this.importDataError(`Bad import data. Check your link and try again.`); 32 | this.validImportData = true; 33 | this.importData = importBlob; 34 | this.activePanel = 'import'; 35 | 36 | // Now, find conflicting accounts 37 | for (let entry of importBlob) { 38 | if (!entry.account || !entry.name) continue; // Data missing? 39 | entry.originalName = this.addressBook.getAccountName(entry.account); 40 | if (!entry.originalName) { 41 | this.newEntries++; 42 | } else if (entry.originalName === entry.name) { 43 | this.existingEntries++; 44 | } else { 45 | this.conflictingEntries++; 46 | } 47 | } 48 | 49 | } catch (err) { 50 | return this.importDataError(`Unable to decode import data. Check your link and try again.`); 51 | } 52 | } 53 | 54 | async confirmImport() { 55 | // Go through our address book and see which ones need to be saved 56 | let importedCount = 0; 57 | for (let entry of this.importData) { 58 | if (!entry.originalName) { 59 | await this.addressBook.saveAddress(entry.account, entry.name); 60 | importedCount++; 61 | } else if (entry.originalName && entry.originalName !== entry.name) { 62 | await this.addressBook.saveAddress(entry.account, entry.name); 63 | importedCount++; 64 | } 65 | } 66 | 67 | this.notifications.sendSuccess(`Successfully imported ${importedCount} address book entries`); 68 | this.activePanel = 'imported'; 69 | } 70 | 71 | importDataError(message) { 72 | this.activePanel = 'error'; 73 | return this.notifications.sendError(message); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/app/components/wallet-widget/wallet-widget.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {WalletService} from "../../services/wallet.service"; 3 | import {NotificationService} from "../../services/notification.service"; 4 | import {LedgerService, LedgerStatus} from "../../services/ledger.service"; 5 | 6 | @Component({ 7 | selector: 'app-wallet-widget', 8 | templateUrl: './wallet-widget.component.html', 9 | styleUrls: ['./wallet-widget.component.css'] 10 | }) 11 | export class WalletWidgetComponent implements OnInit { 12 | wallet = this.walletService.wallet; 13 | 14 | ledgerStatus = 'not-connected'; 15 | 16 | unlockPassword = ''; 17 | 18 | modal: any = null; 19 | 20 | constructor(public walletService: WalletService, private notificationService: NotificationService, public ledgerService: LedgerService) { } 21 | 22 | ngOnInit() { 23 | const UIkit = (window as any).UIkit; 24 | const modal = UIkit.modal(document.getElementById('unlock-wallet-modal')); 25 | this.modal = modal; 26 | 27 | this.ledgerService.ledgerStatus$.subscribe((ledgerStatus: any) => { 28 | this.ledgerStatus = ledgerStatus.status; 29 | }) 30 | } 31 | 32 | async lockWallet() { 33 | if (this.wallet.type === 'ledger') { 34 | return; // No need to lock a ledger wallet, no password saved 35 | } 36 | if (!this.wallet.password) { 37 | return this.notificationService.sendWarning(`You must set a password on your wallet - it is currently blank!`); 38 | } 39 | const locked = await this.walletService.lockWallet(); 40 | if (locked) { 41 | this.notificationService.sendSuccess(`Wallet locked`); 42 | } else { 43 | this.notificationService.sendError(`Unable to lock wallet`); 44 | } 45 | } 46 | 47 | async reloadLedger() { 48 | this.notificationService.sendInfo(`Checking Ledger Status...`, { identifier: 'ledger-status', length: 0 }) 49 | try { 50 | const loaded = await this.ledgerService.loadLedger(); 51 | this.notificationService.removeNotification('ledger-status'); 52 | if (loaded) { 53 | this.notificationService.sendSuccess(`Successfully connected to Ledger device`); 54 | } else if (loaded === false) { 55 | this.notificationService.sendError(`Unable to connect to Ledger device`); 56 | } 57 | } catch (err) { 58 | console.log(`Got error when loading ledger! `, err); 59 | this.notificationService.removeNotification('ledger-status'); 60 | // this.notificationService.sendError(`Unable to load Ledger Device: ${err.message}`); 61 | } 62 | } 63 | 64 | async unlockWallet() { 65 | const unlocked = await this.walletService.unlockWallet(this.unlockPassword); 66 | this.unlockPassword = ''; 67 | 68 | if (unlocked) { 69 | this.notificationService.sendSuccess(`Wallet unlocked`); 70 | this.modal.hide(); 71 | } else { 72 | this.notificationService.sendError(`Invalid password, please try again!`); 73 | } 74 | 75 | this.unlockPassword = ''; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/app/components/wallet-widget/wallet-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 8 | 9 | Ledger Disconnected 10 | 11 |
12 | 13 | 14 | Ledger App Closed 15 | 16 |
17 |
18 | 19 | Ledger Ready 20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | Wallet Locked 28 | 29 |
30 |
31 | 32 | Wallet Unlocked 33 | 34 |
35 |
36 | 37 | 42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |

Unlock Wallet

52 |
53 |
54 | 55 |
56 | 60 |
61 |
62 | -------------------------------------------------------------------------------- /src/app/components/change-rep-widget/change-rep-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | Update Representatives 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 |

Representative Change Recommended

20 |
21 |
22 |
23 | Your representatives vote on transactions on your behalf to secure the Banano network.
24 | By changing the representatives below, you will help decentralize the network 25 |
26 | 27 |
    28 |
  • 29 | {{ rep.label }} {{ rep.account | squeeze }} 30 | 31 |
    32 |
      33 |
    • Representative is offline
    • 34 |
    • Representative has high voting weight (>1%)
    • 35 |
    • Representative is marked as avoid
    • 36 |
    37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 |
    42 | {{ account.addressBookName }} 43 | {{ account.id | squeeze }} 44 | {{ account.balance | rai:'mnano' }}
    49 | 50 |
    51 |
  • 52 |
53 | 54 |

To bypass this warning, add your representative as trusted to your known representative list

55 | 56 |
57 | 69 |
70 |
71 | 72 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "typeof-compare": true, 111 | "unified-signatures": true, 112 | "variable-name": false, 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-operator", 118 | "check-separator", 119 | "check-type" 120 | ], 121 | "directive-selector": [ 122 | true, 123 | "attribute", 124 | "app", 125 | "camelCase" 126 | ], 127 | "component-selector": [ 128 | true, 129 | "element", 130 | "app", 131 | "kebab-case" 132 | ], 133 | "no-output-on-prefix": true, 134 | "use-input-property-decorator": true, 135 | "use-output-property-decorator": true, 136 | "use-host-property-decorator": true, 137 | "no-input-rename": true, 138 | "no-output-rename": true, 139 | "use-life-cycle-interface": true, 140 | "use-pipe-transform-interface": true, 141 | "component-class-suffix": true, 142 | "directive-class-suffix": true 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/app/components/import-address-book/import-address-book.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Import Address Book

4 | 5 |
6 |
7 |

Bad Import Data

8 |
9 |
10 |

There was an issue reading the import data for your address book. Double check the address and try again.

11 |
12 |
13 | 14 |
15 |
16 |

Confirm Import

17 |
18 |
19 |

20 | 21 | {{ conflictingEntries }} Labels already exist and will have their name changed. 22 | 23 | 24 | {{ newEntries }} New labels will be added. 25 | 26 | 27 | {{ existingEntries }} Labels already exist and will not be modified. 28 | 29 |

30 | 31 |
32 |
33 |
    34 |
  • 35 |
    36 |
    New Label
    37 |
    Current Label
    38 |
    Account
    39 |
    40 |
  • 41 |
42 |
    43 |
  • 44 |
    45 |
    46 | {{ entry.name }} 47 |
    48 |
    49 | {{ entry.originalName }} 50 |
    51 |
    52 | {{ entry.account }} 53 |
    54 |
    55 | 56 |
  • 57 |
58 |
59 |
60 | 61 |
62 | 72 |
73 | 74 |
75 |
76 |

Address Book Imported

77 |
78 |
79 |

Your address book has been successfully imported!

80 |
81 | 84 |
85 | 86 | 87 |
88 |
89 | -------------------------------------------------------------------------------- /src/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {NodeService} from "./node.service"; 4 | import {AppSettingsService} from "./app-settings.service"; 5 | 6 | export interface NinjaVerifiedRep { 7 | votingweight: number; 8 | delegators: number; 9 | uptime: number; 10 | score: number; 11 | account: string; 12 | alias: string; 13 | } 14 | 15 | @Injectable() 16 | export class ApiService { 17 | // apiUrl = `http://localhost:9950/api`; 18 | apiUrl = `https://vault.banano.cc/api`; 19 | rpcUrl = `${this.apiUrl}/node-api`; 20 | 21 | constructor(private http: HttpClient, private node: NodeService, private appSettings: AppSettingsService) { } 22 | 23 | private async request(action, data): Promise { 24 | data.action = action; 25 | let apiUrl = this.appSettings.settings.serverAPI || this.rpcUrl; 26 | if (this.appSettings.settings.serverNode) { 27 | apiUrl += `?node=${this.appSettings.settings.serverNode}`; 28 | } 29 | if (this.node.node.status === false) { 30 | this.node.setLoading(); 31 | } 32 | return await this.http.post(apiUrl, data).toPromise() 33 | .then(res => { 34 | this.node.setOnline(); 35 | return res; 36 | }) 37 | .catch(err => { 38 | if (err.status === 500 || err.status === 0) { 39 | this.node.setOffline(); // Hard error, node is offline 40 | } 41 | throw err; 42 | }); 43 | } 44 | 45 | async recommendedReps(): Promise { 46 | return await this.http.get(`${this.apiUrl}/recommended-representatives`).toPromise() as NinjaVerifiedRep[]; 47 | } 48 | 49 | async accountsBalances(accounts: string[]): Promise<{balances: any }> { 50 | return await this.request('accounts_balances', { accounts }); 51 | } 52 | async accountsFrontiers(accounts: string[]): Promise<{frontiers: any }> { 53 | return await this.request('accounts_frontiers', { accounts }); 54 | } 55 | async accountsPending(accounts: string[], count: number = 50): Promise<{blocks: any }> { 56 | return await this.request('accounts_pending', { accounts, count, source: true }); 57 | } 58 | async accountsPendingLimit(accounts: string[], threshold: string, count: number = 50): Promise<{blocks: any }> { 59 | return await this.request('accounts_pending', { accounts, count, threshold, source: true }); 60 | } 61 | async delegatorsCount(account: string): Promise<{ count: string }> { 62 | return await this.request('delegators_count', { account }); 63 | } 64 | async representativesOnline(): Promise<{ representatives: any }> { 65 | return await this.request('representatives_online', { }); 66 | } 67 | 68 | async blocksInfo(blocks): Promise<{blocks: any, error?: string}> { 69 | return await this.request('blocks_info', { hashes: blocks, pending: true, source: true }); 70 | } 71 | async blockCount(): Promise<{count: number, unchecked: number }> { 72 | return await this.request('block_count', { }); 73 | } 74 | async workGenerate(hash): Promise<{ work: string }> { 75 | return await this.request('work_generate', { hash, use_peers: true }); 76 | } 77 | async process(block): Promise<{ hash: string, error?: string }> { 78 | return await this.request('process', { block: JSON.stringify(block) }); 79 | } 80 | async accountHistory(account, count = 25, raw = false): Promise<{history: any }> { 81 | return await this.request('account_history', { account, count, raw }); 82 | } 83 | async accountInfo(account): Promise { 84 | return await this.request('account_info', { account, pending: true, representative: true, weight: true }); 85 | } 86 | async validateAccountNumber(account): Promise<{ valid: '1'|'0' }> { 87 | return await this.request('validate_account_number', { account }); 88 | } 89 | async pending(account, count): Promise { 90 | return await this.request('pending', { account, count, source: true }); 91 | } 92 | async pendingLimit(account, count, threshold): Promise { 93 | return await this.request('pending', { account, count, threshold, source: true }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/components/receive/receive.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Receive Banano

6 |
7 |
8 | 9 |
10 |
11 |

12 | When someone sends you Banano, you are also in charge of performing a receive block to actually receive the funds.
13 | If you have the wallet open when you receive a transaction, this will be performed automatically.
14 | Otherwise, select the account you are expecting to receive from below to search for pending transactions. 15 |

16 |
17 |
18 | 19 |
20 |
21 | 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 56 | 70 | 71 | 75 | 76 | 77 | 78 | 79 | 80 |
AccountSenderAmountOptions
43 |
44 | 49 |
50 |
    51 |
  • 52 |
53 |
54 |
55 |
57 |
58 | 63 |
64 |
    65 |
  • 66 |
67 |
68 |
69 |
{{ block.amount | rai: settings.settings.displayDenomination }} 72 | 73 | 74 |
No pending transactions
81 |
82 | 83 |
84 |
85 | 86 |
87 |
88 | 89 | -------------------------------------------------------------------------------- /src/app/components/address-book/address-book.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit} from '@angular/core'; 2 | import {AddressBookService} from "../../services/address-book.service"; 3 | import {WalletService} from "../../services/wallet.service"; 4 | import {NotificationService} from "../../services/notification.service"; 5 | import {ModalService} from "../../services/modal.service"; 6 | import {ApiService} from "../../services/api.service"; 7 | import {Router} from "@angular/router"; 8 | 9 | @Component({ 10 | selector: 'app-address-book', 11 | templateUrl: './address-book.component.html', 12 | styleUrls: ['./address-book.component.css'] 13 | }) 14 | export class AddressBookComponent implements OnInit, AfterViewInit { 15 | 16 | activePanel = 0; 17 | 18 | addressBook$ = this.addressBookService.addressBook$; 19 | newAddressAccount = ''; 20 | newAddressName = ''; 21 | 22 | constructor( 23 | private addressBookService: AddressBookService, 24 | private walletService: WalletService, 25 | private notificationService: NotificationService, 26 | public modal: ModalService, 27 | private router: Router, 28 | private nodeApi: ApiService) { } 29 | 30 | async ngOnInit() { 31 | this.addressBookService.loadAddressBook(); 32 | } 33 | 34 | ngAfterViewInit() { 35 | // Listen for reordering events 36 | document.getElementById('address-book-sortable').addEventListener('moved', (e) => { 37 | const elements = e.srcElement.children; 38 | 39 | const result = [].slice.call(elements); 40 | const datas = result.map(e => e.dataset.account); 41 | 42 | this.addressBookService.setAddressBookOrder(datas); 43 | this.notificationService.sendSuccess(`Updated address book order`); 44 | }); 45 | } 46 | 47 | editEntry(addressBook) { 48 | this.newAddressAccount = addressBook.account; 49 | this.newAddressName = addressBook.name; 50 | this.activePanel = 1; 51 | setTimeout(() => { 52 | document.getElementById('new-address-name').focus(); 53 | }, 150); 54 | } 55 | 56 | async saveNewAddress() { 57 | if (!this.newAddressAccount || !this.newAddressName) return this.notificationService.sendError(`Account and name are required`); 58 | 59 | this.newAddressAccount = this.newAddressAccount.replace(/ /g, ''); // Remove spaces 60 | 61 | // Make sure name doesn't exist 62 | if (this.addressBookService.nameExists(this.newAddressName)) { 63 | return this.notificationService.sendError(`This name is already in use! Please use a unique name`); 64 | } 65 | 66 | // Make sure the address is valid 67 | const valid = await this.nodeApi.validateAccountNumber(this.newAddressAccount); 68 | if (!valid || valid.valid !== '1') return this.notificationService.sendWarning(`Account ID is not a valid account`); 69 | 70 | try { 71 | await this.addressBookService.saveAddress(this.newAddressAccount, this.newAddressName); 72 | this.notificationService.sendSuccess(`Successfully created new name for account!`); 73 | // IF this is one of our accounts, set its name, and hope things update? 74 | const walletAccount = this.walletService.wallet.accounts.find(a => a.id.toLowerCase() === this.newAddressAccount.toLowerCase()); 75 | if (walletAccount) { 76 | walletAccount.addressBookName = this.newAddressName; 77 | } 78 | this.cancelNewAddress(); 79 | } catch (err) { 80 | this.notificationService.sendError(`Unable to save entry: ${err.message}`) 81 | } 82 | } 83 | 84 | cancelNewAddress() { 85 | this.newAddressName = ''; 86 | this.newAddressAccount = ''; 87 | this.activePanel = 0; 88 | } 89 | 90 | copied() { 91 | this.notificationService.sendSuccess(`Account address copied to clipboard!`); 92 | } 93 | 94 | async deleteAddress(account) { 95 | try { 96 | this.addressBookService.deleteAddress(account); 97 | this.notificationService.sendSuccess(`Successfully deleted address book entry`) 98 | } catch (err) { 99 | this.notificationService.sendError(`Unable to delete entry: ${err.message}`) 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/app/welcome/welcome.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | The "no strings attached" open source wallet for Banano
7 |
8 |
9 |
10 |
11 |

To get started, choose an option below to configure your wallet

12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 | 36 |

Security Focused

37 |

All sensitive operations happen in your browser only - your seed and private keys are never sent across your network, or stored on any server in any format. (So there is no need for server-based authentication measures such as 2FA)

38 |
39 | 40 |
41 | 42 |

No Strings Attached

43 |

Because no information is ever stored on a server, there is no account to login to or email address required. Create as many wallets as you want and switch between them easily.

44 |
45 | 46 |
47 | 48 |

Any Device, Anywhere

49 |

Send and receive Banano from any device using the web wallet, or alternatively download our official wallet KALIUM directly to your mobile device kalium.banano.cc.

50 |
51 | 52 |
53 | 54 |

Helpful Features

55 |

Store labels for your friends and your own accounts in the address book, track your balances in your local currency, and more!

56 |
57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 |
66 |
67 | BananoVault is open source and all code is available at https://github.com/bananocoin/bananovault.
68 | Please submit any feedback or bug reports on our GitHub page. Thank you for using BananoVault! 69 |
70 |
71 | 72 |
73 |
74 | 75 | Don't have any Banano, but want to see how quick and easy it is to use?
76 | See our website https://banano.cc/ for details on the next faucet event! 77 |
78 |
79 | 80 |
81 | Donation Address: {{ donationAccount }} 82 |
83 |
84 |
85 |
86 |
87 | -------------------------------------------------------------------------------- /src/app/services/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import {AppSettingsService} from "./app-settings.service"; 4 | 5 | @Injectable() 6 | export class WebsocketService { 7 | 8 | queuedCommands = []; 9 | 10 | keepaliveTimeout = 60 * 1000; 11 | reconnectTimeout = 5 * 1000; 12 | 13 | keepaliveSet = false; 14 | 15 | socket = { 16 | connected: false, 17 | ws: null, 18 | }; 19 | 20 | subscribedAccounts = []; 21 | 22 | newTransactions$ = new BehaviorSubject(null); 23 | 24 | constructor(private appSettings: AppSettingsService) { } 25 | 26 | forceReconnect() { 27 | if (this.socket.connected && this.socket.ws) { 28 | // Override the onclose event so it doesnt try to reconnect the old instance 29 | this.socket.ws.onclose = event => { 30 | }; 31 | this.socket.ws.close(); 32 | delete this.socket.ws; 33 | this.socket.connected = false; 34 | } 35 | 36 | setTimeout(() => this.connect(), 250); 37 | } 38 | 39 | connect() { 40 | if (this.socket.connected && this.socket.ws) return; 41 | delete this.socket.ws; // Maybe this will erase old connections 42 | 43 | const wsUrl = this.appSettings.settings.serverWS || 'wss://ws.banano.cc'; 44 | const ws = new WebSocket(wsUrl); 45 | this.socket.ws = ws; 46 | 47 | ws.onopen = event => { 48 | this.socket.connected = true; 49 | this.queuedCommands.forEach(event => ws.send(JSON.stringify(event))); 50 | 51 | // Resubscribe to accounts? 52 | if (this.subscribedAccounts.length) { 53 | this.subscribeAccounts(this.subscribedAccounts); 54 | } 55 | 56 | if (!this.keepaliveSet) { 57 | this.keepalive(); // Start keepalives! 58 | } 59 | }; 60 | ws.onerror = event => { 61 | // this.socket.connected = false; 62 | console.log(`Socket error`, event); 63 | }; 64 | ws.onclose = event => { 65 | this.socket.connected = false; 66 | console.log(`Socket close`, event); 67 | 68 | // Start attempting to recconect 69 | setTimeout(() => this.attemptReconnect(), this.reconnectTimeout); 70 | }; 71 | ws.onmessage = event => { 72 | try { 73 | const newEvent = JSON.parse(event.data); 74 | 75 | if (newEvent.event === 'newTransaction') { 76 | this.newTransactions$.next(newEvent.data); 77 | } 78 | } catch (err) { 79 | console.log(`Error parsing message`, err); 80 | } 81 | } 82 | } 83 | 84 | attemptReconnect() { 85 | this.connect(); 86 | if (this.reconnectTimeout < 30 * 1000) { 87 | this.reconnectTimeout += 5 * 1000; // Slowly increase the timeout up to 30 seconds 88 | } 89 | } 90 | 91 | keepalive() { 92 | this.keepaliveSet = true; 93 | if (this.socket.connected) { 94 | this.socket.ws.send(JSON.stringify({ event: 'keepalive' })); 95 | } 96 | 97 | setTimeout(() => { 98 | this.keepalive(); 99 | }, this.keepaliveTimeout); 100 | } 101 | 102 | 103 | 104 | subscribeAccounts(accountIDs: string[]) { 105 | const event = { event: 'subscribe', data: accountIDs }; 106 | accountIDs.forEach(account => { 107 | if (this.subscribedAccounts.indexOf(account) === -1) { 108 | this.subscribedAccounts.push(account); // Keep a unique list of subscriptions for reconnecting 109 | } 110 | }); 111 | if (!this.socket.connected) { 112 | this.queuedCommands.push(event); 113 | if (this.queuedCommands.length >= 3) { 114 | this.queuedCommands.shift(); // Prune queued commands 115 | } 116 | return; 117 | } 118 | this.socket.ws.send(JSON.stringify(event)); 119 | } 120 | 121 | unsubscribeAccounts(accountIDs: string[]) { 122 | const event = { event: 'unsubscribe', data: accountIDs }; 123 | accountIDs.forEach(account => { 124 | const existingIndex = this.subscribedAccounts.indexOf(account); 125 | if (existingIndex !== -1) { 126 | this.subscribedAccounts.splice(existingIndex, 1); // Remove from our internal subscription list 127 | } 128 | }); 129 | // If we aren't connected, we don't need to do anything. On reconnect, it won't subscribe. 130 | if (this.socket.connected) { 131 | this.socket.ws.send(JSON.stringify(event)); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 34 | 48 | 53 | 58 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/app/components/accounts/accounts.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | Accounts 7 | {{ viewAdvanced ? 'BASIC' : 'ADVANCED' }} 8 |

9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 56 | 68 | 78 | 79 | 80 | 81 | 82 | 83 |
27 | 29 | Account 30 | Balance
38 |
39 |
40 | 43 | 44 | {{ account.addressBookName }} {{ account.id }} 45 | 46 |
47 |
48 |
    49 | 50 |
  • 51 |
  • 52 |
53 |
54 |
55 |
57 | {{ account.balance | rai: settings.settings.displayDenomination }} 58 |
59 | +{{ account.balanceRaw.toString(10) | squeeze:'5,5' }} raw 60 |
61 |
62 | {{ account.pending | rai: settings.settings.displayDenomination }} 63 |
64 |
65 | +{{ account.pendingRaw.toString(10) | squeeze:'5,5' }} raw 66 |
67 |
69 | {{ account.balanceFiat | fiat: settings.settings.displayCurrency }} 70 |
71 | {{ account.pendingFiat | fiat: settings.settings.displayCurrency }} 72 |
73 |
74 | 75 |
76 | 77 |
You don't have any accounts yet, click here to create one
84 |
85 | 86 |

Click on an account to view more details!

87 | 88 |
89 |
90 | -------------------------------------------------------------------------------- /src/app/components/manage-representatives/manage-representatives.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit} from '@angular/core'; 2 | import {AddressBookService} from "../../services/address-book.service"; 3 | import {WalletService} from "../../services/wallet.service"; 4 | import {NotificationService} from "../../services/notification.service"; 5 | import {ModalService} from "../../services/modal.service"; 6 | import {ApiService} from "../../services/api.service"; 7 | import {Router} from "@angular/router"; 8 | import {RepresentativeService} from "../../services/representative.service"; 9 | 10 | @Component({ 11 | selector: 'app-manage-representatives', 12 | templateUrl: './manage-representatives.component.html', 13 | styleUrls: ['./manage-representatives.component.css'] 14 | }) 15 | export class ManageRepresentativesComponent implements OnInit, AfterViewInit { 16 | 17 | activePanel = 0; 18 | 19 | // Set the online status of each representative 20 | representatives$ = this.repService.representatives$.map(reps => { 21 | return reps.map(rep => { 22 | rep.online = this.onlineReps.indexOf(rep.id) !== -1; 23 | return rep; 24 | }) 25 | }); 26 | 27 | newRepAccount = ''; 28 | newRepName = ''; 29 | newRepTrusted = false; 30 | newRepWarn = false; 31 | 32 | onlineReps = []; 33 | 34 | constructor( 35 | private api: ApiService, 36 | private addressBookService: AddressBookService, 37 | private walletService: WalletService, 38 | private notificationService: NotificationService, 39 | public modal: ModalService, 40 | private repService: RepresentativeService, 41 | private router: Router, 42 | private nodeApi: ApiService) { } 43 | 44 | async ngOnInit() { 45 | this.repService.loadRepresentativeList(); 46 | this.onlineReps = await this.getOnlineRepresentatives(); 47 | this.repService.representatives$.next(this.repService.representatives); // Forcefully repush rep list once we have online status 48 | } 49 | 50 | ngAfterViewInit() { 51 | } 52 | 53 | editEntry(representative) { 54 | this.newRepAccount = representative.id; 55 | this.newRepName = representative.name; 56 | this.newRepTrusted = !!representative.trusted; 57 | this.newRepWarn = !!representative.warn; 58 | this.activePanel = 1; 59 | setTimeout(() => { 60 | document.getElementById('new-address-name').focus(); 61 | }, 150); 62 | } 63 | 64 | async saveNewRepresentative() { 65 | if (!this.newRepAccount || !this.newRepName) return this.notificationService.sendError(`Account and name are required`); 66 | 67 | this.newRepAccount = this.newRepAccount.replace(/ /g, ''); // Remove spaces 68 | 69 | // Make sure the address is valid 70 | const valid = await this.nodeApi.validateAccountNumber(this.newRepAccount); 71 | if (!valid || valid.valid !== '1') return this.notificationService.sendWarning(`Account ID is not a valid account`); 72 | 73 | try { 74 | await this.repService.saveRepresentative(this.newRepAccount, this.newRepName, this.newRepTrusted, this.newRepWarn); 75 | this.notificationService.sendSuccess(`Successfully saved new representative!`); 76 | 77 | this.cancelNewRep(); 78 | } catch (err) { 79 | this.notificationService.sendError(`Unable to save entry: ${err.message}`) 80 | } 81 | } 82 | 83 | cancelNewRep() { 84 | this.newRepName = ''; 85 | this.newRepAccount = ''; 86 | this.newRepTrusted = false; 87 | this.newRepWarn = false; 88 | this.activePanel = 0; 89 | } 90 | 91 | copied() { 92 | this.notificationService.sendSuccess(`Account address copied to clipboard!`); 93 | } 94 | 95 | async getOnlineRepresentatives() { 96 | const representatives = []; 97 | try { 98 | const reps = await this.api.representativesOnline(); 99 | for (let representative in reps.representatives) { 100 | if (!reps.representatives.hasOwnProperty(representative)) continue; 101 | representatives.push(reps.representatives[representative]); 102 | } 103 | } catch (err) { 104 | this.notificationService.sendWarning(`Unable to determine online status of representatives`); 105 | } 106 | 107 | return representatives; 108 | } 109 | 110 | async deleteRepresentative(accountID) { 111 | try { 112 | this.repService.deleteRepresentative(accountID); 113 | this.notificationService.sendSuccess(`Successfully deleted representative`) 114 | } catch (err) { 115 | this.notificationService.sendError(`Unable to delete representative: ${err.message}`) 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BananoVault", 3 | "version": "1.1.3", 4 | "license": "MIT", 5 | "description": "Wallet for interacting with Banano", 6 | "author": "Andrew Steele", 7 | "main": "desktop-app/dist/desktop-app.js", 8 | "scripts": { 9 | "ng": "ng", 10 | "start": "ng serve", 11 | "build": "ng build", 12 | "test": "ng test", 13 | "lint": "ng lint", 14 | "e2e": "ng e2e", 15 | "desktop": "electron main.js", 16 | "pack": "electron-builder --dir", 17 | "dist": "electron-builder", 18 | "dist-full": "electron-builder -wml --x64", 19 | "publish": "build -wml --x64 -p always", 20 | "release": "npm run desktop:build && electron-builder", 21 | "wallet:dev": "ng serve --open", 22 | "wallet:dev-ssl": "ng serve --open --ssl 1 --ssl-key \"assets/dev-cert/key.pem\" --ssl-cert \"assets/dev-cert/cert.pem\"", 23 | "wallet:build": "ng build --prod", 24 | "wallet:build-desktop": "ng build --env=desktop --base-href ", 25 | "desktop:compile": "cd desktop-app && tsc && cd ..", 26 | "desktop:build": "npm run wallet:build-desktop && npm run desktop:compile", 27 | "desktop:dev": "npm run desktop:compile && electron desktop-app/dist/desktop-app.js", 28 | "desktop:local": "electron-builder", 29 | "desktop:full": "electron-builder -wml --x64", 30 | "desktop:publish": "build -wml --x64 -p always", 31 | "server:copy-wallet": "cd .. && rm -rf bananovault-server/static && cp -R bananovault/dist bananovault-server/static" 32 | }, 33 | "private": true, 34 | "dependencies": { 35 | "@angular-devkit/core": "0.0.28", 36 | "@angular/animations": "^5.0.0", 37 | "@angular/common": "^5.0.0", 38 | "@angular/compiler": "^5.0.0", 39 | "@angular/core": "^5.0.0", 40 | "@angular/forms": "^5.0.0", 41 | "@angular/http": "^5.0.0", 42 | "@angular/platform-browser": "^5.0.0", 43 | "@angular/platform-browser-dynamic": "^5.0.0", 44 | "@angular/router": "^5.0.0", 45 | "@ledgerhq/hw-transport-node-hid": "^4.22.0", 46 | "@ledgerhq/hw-transport-u2f": "^4.7.3", 47 | "@types/crypto-js": "^3.1.38", 48 | "babel-polyfill": "^6.26.0", 49 | "babel-runtime": "^6.26.0", 50 | "bignumber.js": "^5.0.0", 51 | "bip39": "^2.5.0", 52 | "blakejs": "^1.1.0", 53 | "core-js": "^2.4.1", 54 | "crypto": "^1.0.1", 55 | "crypto-js": "^3.1.9-1", 56 | "electron-updater": "^2.21.0", 57 | "hw-app-nano": "^1.3.0", 58 | "ngx-clipboard": "^9.1.2", 59 | "node-hid": "^0.7.3", 60 | "qrcode": "^1.2.0", 61 | "rxjs": "^5.5.2", 62 | "tweetnacl": "^1.0.0", 63 | "zone.js": "^0.8.14" 64 | }, 65 | "devDependencies": { 66 | "@angular/cli": "1.6.0", 67 | "@angular/compiler-cli": "^5.0.0", 68 | "@angular/language-service": "^5.0.0", 69 | "@types/bip39": "^2.4.0", 70 | "@types/jasmine": "~2.5.53", 71 | "@types/jasminewd2": "~2.0.2", 72 | "@types/node": "^10.11.0", 73 | "@types/qrcode": "^0.8.1", 74 | "codelyzer": "^4.0.1", 75 | "electron": "^1.8.2", 76 | "electron-builder": "^19.52.1", 77 | "jasmine-core": "~2.6.2", 78 | "jasmine-spec-reporter": "~4.1.0", 79 | "karma": "~1.7.0", 80 | "karma-chrome-launcher": "~2.1.1", 81 | "karma-cli": "~1.0.1", 82 | "karma-coverage-istanbul-reporter": "^1.2.1", 83 | "karma-jasmine": "~1.1.0", 84 | "karma-jasmine-html-reporter": "^0.2.2", 85 | "protractor": "~5.1.2", 86 | "ts-node": "~3.2.0", 87 | "tslint": "~5.7.0", 88 | "typescript": "~2.4.2", 89 | "uikit": "^3.0.0-beta.40" 90 | }, 91 | "build": { 92 | "appId": "com.electron.bananovault", 93 | "buildVersion": "1.1.3", 94 | "productName": "BananoVault", 95 | "copyright": "Copyright © 2019 Andrew Steele", 96 | "directories": { 97 | "output": "desktop-app/build", 98 | "buildResources": "desktop-app/assets", 99 | "app": "./" 100 | }, 101 | "mac": { 102 | "category": "public.app-category.finance", 103 | "extendInfo": { 104 | "CFBundleURLTypes": [ 105 | { 106 | "CFBundleURLName": "xrb", 107 | "CFBundleURLSchemes": [ 108 | "xrb" 109 | ] 110 | } 111 | ] 112 | } 113 | }, 114 | "linux": { 115 | "target": "AppImage" 116 | }, 117 | "publish": [ 118 | { 119 | "provider": "github", 120 | "owner": "cronoh", 121 | "repo": "bananovault" 122 | } 123 | ], 124 | "protocols": { 125 | "name": "ban", 126 | "schemes": [ 127 | "ban" 128 | ], 129 | "role": "Viewer" 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | 5 | import { AppComponent } from './app.component'; 6 | import {HttpClientModule} from "@angular/common/http"; 7 | import {FormsModule, ReactiveFormsModule} from "@angular/forms"; 8 | import {WelcomeComponent} from "./welcome/welcome.component"; 9 | import {AppRoutingModule} from "./app-routing.module"; 10 | import {UtilService} from "./services/util.service"; 11 | import {WalletService} from "./services/wallet.service"; 12 | import {ConfigureWalletComponent} from "./components/configure-wallet/configure-wallet.component"; 13 | import {NotificationService} from "./services/notification.service"; 14 | import {NotificationsComponent} from "./components/notifications/notifications.component"; 15 | import {RaiPipe} from "./pipes/rai.pipe"; 16 | import {AccountsComponent} from "./components/accounts/accounts.component"; 17 | import {ApiService} from "./services/api.service"; 18 | import {AddressBookService} from "./services/address-book.service"; 19 | import {SendComponent} from "./components/send/send.component"; 20 | import {SqueezePipe} from "./pipes/squeeze.pipe"; 21 | import {ModalService} from "./services/modal.service"; 22 | import {AddressBookComponent} from "./components/address-book/address-book.component"; 23 | import {ClipboardModule} from "ngx-clipboard"; 24 | import {ReceiveComponent} from "./components/receive/receive.component"; 25 | import {WalletWidgetComponent} from "./components/wallet-widget/wallet-widget.component"; 26 | import {ManageWalletComponent} from "./components/manage-wallet/manage-wallet.component"; 27 | import {WorkPoolService} from "./services/work-pool.service"; 28 | import {ConfigureAppComponent} from "./components/configure-app/configure-app.component"; 29 | import {AppSettingsService} from "./services/app-settings.service"; 30 | import {WebsocketService} from "./services/websocket.service"; 31 | import {BananoBlockService} from "./services/nano-block.service"; 32 | import { AccountDetailsComponent } from './components/account-details/account-details.component'; 33 | import { TransactionDetailsComponent } from './components/transaction-details/transaction-details.component'; 34 | import {PriceService} from "./services/price.service"; 35 | import { FiatPipe } from './pipes/fiat.pipe'; 36 | import { ImportWalletComponent } from './components/import-wallet/import-wallet.component'; 37 | import { BananoAccountIdComponent } from './components/helpers/banano-account-id/banano-account-id.component'; 38 | import {PowService} from "./services/pow.service"; 39 | import { ImportAddressBookComponent } from './components/import-address-book/import-address-book.component'; 40 | import { CurrencySymbolPipe } from './pipes/currency-symbol.pipe'; 41 | import { RepresentativesComponent } from './components/representatives/representatives.component'; 42 | import {RepresentativeService} from "./services/representative.service"; 43 | import {ManageRepresentativesComponent} from "./components/manage-representatives/manage-representatives.component"; 44 | import {NodeService} from "./services/node.service"; 45 | import {LedgerService} from "./services/ledger.service"; 46 | import {DesktopService} from "./services/desktop.service"; 47 | import { AccountPipe } from './pipes/account.pipe'; 48 | import { ChangeRepWidgetComponent } from './components/change-rep-widget/change-rep-widget.component'; 49 | 50 | 51 | @NgModule({ 52 | declarations: [ 53 | AppComponent, 54 | WelcomeComponent, 55 | ConfigureWalletComponent, 56 | NotificationsComponent, 57 | RaiPipe, 58 | SqueezePipe, 59 | AccountsComponent, 60 | SendComponent, 61 | AddressBookComponent, 62 | ReceiveComponent, 63 | WalletWidgetComponent, 64 | ManageWalletComponent, 65 | ConfigureAppComponent, 66 | AccountDetailsComponent, 67 | TransactionDetailsComponent, 68 | FiatPipe, 69 | ImportWalletComponent, 70 | BananoAccountIdComponent, 71 | ImportAddressBookComponent, 72 | CurrencySymbolPipe, 73 | RepresentativesComponent, 74 | ManageRepresentativesComponent, 75 | AccountPipe, 76 | ChangeRepWidgetComponent, 77 | ], 78 | imports: [ 79 | BrowserModule, 80 | HttpClientModule, 81 | AppRoutingModule, 82 | ReactiveFormsModule, 83 | FormsModule, 84 | ClipboardModule 85 | ], 86 | providers: [ 87 | UtilService, 88 | WalletService, 89 | NotificationService, 90 | ApiService, 91 | AddressBookService, 92 | ModalService, 93 | WorkPoolService, 94 | AppSettingsService, 95 | WebsocketService, 96 | BananoBlockService, 97 | PriceService, 98 | PowService, 99 | RepresentativeService, 100 | NodeService, 101 | LedgerService, 102 | DesktopService, 103 | ], 104 | bootstrap: [AppComponent] 105 | }) 106 | export class AppModule { } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BananoVault 2 | 3 | BananoVault is a fully client-side signing wallet for sending and receiving [Banano](https://github.com/bananocoin/banano) 4 | on your [desktop](https://github.com/bananocoin/bananovault/releases) or [in your browser](https://vault.banano.cc) 5 | 6 | ![BananoVault Screenshot](https://i.imgur.com/DWlPQdM.png) 7 | ___ 8 | 9 | # Table of Contents 10 | * [Install](#install-bananovault) 11 | * [Bugs/Feedback](#bugsfeedback) 12 | * [Application Structure](#application-structure) 13 | * [Development Prerequisites](#development-prerequisites) 14 | * [Development Guide](#development-guide) 15 | * [Acknowledgements](#acknowledgements) 16 | 17 | 18 | # Install BananoVault 19 | BananoVault is available on your desktop (Windows/Mac/Linux) - just head over to the [releases section](https://github.com/bananocoin/bananovault/releases) and download the latest version for your OS. 20 | 21 | You can also use BananoVault from any device on the web at [vault.banano.cc](https://vault.banano.cc) 22 | 23 | 24 | # Bugs/Feedback 25 | If you run into any issues, please use the [GitHub Issue Tracker](https://github.com/bananocoin/bananovault/issues) or head over to our [Discord Server](https://chat.banano.cc/)! 26 | We are continually improving and adding new features based on the feedback you provide, so please let your opinions be known! 27 | 28 | To get an idea of some of the things that are planned for the near future, check out the [Road Map](https://github.com/bananocoin/bananovault/wiki/Road-Map). 29 | 30 | ___ 31 | 32 | #### Everything below is only for contributing to the development of BananoVault 33 | #### To download BananoVault go to the [releases section](https://github.com/bananocoin/bananovault/releases), or use the web wallet at [vault.banano.cc](https://vault.banano.cc) 34 | 35 | ___ 36 | 37 | # Application Structure 38 | 39 | The application is broken into a few separate pieces: 40 | 41 | - [BananoVault](https://github.com/bananocoin/bananovault) - The main wallet application (UI + Seed Generation/Block Signing/Etc). 42 | - [BananoVault-Server](https://github.com/bananocoin/bananovault-server) - Serves the Wallet UI and brokers public communication between the wallet and the Banano Node. 43 | - [BananoVault-WS](https://github.com/bananocoin/bananovault-ws) - Websocket server that receives new blocks from the Banano node and sends them in real time to the wallet ui. 44 | 45 | 46 | # Development Prerequisites 47 | - Node Package Manager: [Install NPM](https://www.npmjs.com/get-npm) 48 | - Angular CLI: `npm install -g @angular/cli` 49 | 50 | 51 | # Development Guide 52 | #### Clone repository and install dependencies 53 | ```bash 54 | git clone https://github.com/bananocoin/bananovault 55 | cd bananovault 56 | npm install 57 | ``` 58 | 59 | #### Run the wallet in dev mode (use http://localhost:4200) 60 | ```bash 61 | npm run wallet:dev 62 | ``` 63 | 64 | #### Run the wallet in dev mode with ledger support (use https://localhost:4200, and ignore the ssl cert error) 65 | ```bash 66 | npm run wallet:dev-ssl 67 | ``` 68 | 69 | ## Build Wallet (For Production) 70 | Build a production version of the wallet for web: 71 | ```bash 72 | npm run wallet:build 73 | ``` 74 | 75 | Build a production version of the wallet for desktop: *(Required for all desktop builds)* 76 | ```bash 77 | npm run wallet:build-desktop 78 | ``` 79 | 80 | ## Desktop Builds 81 | 82 | *All desktop builds require that you have built a desktop version of the wallet before running!* 83 | 84 | Run the desktop wallet in dev mode: 85 | ```bash 86 | npm run desktop:dev 87 | ``` 88 | 89 | Build the desktop wallet for your local OS (Will be in `dist-desktop`): 90 | ```bash 91 | npm run desktop:local 92 | ``` 93 | 94 | Build the desktop wallet for Windows+Mac+Linux (May require dependencies for your OS [View them here](https://www.electron.build/multi-platform-build)): 95 | ```bash 96 | npm run desktop:full 97 | ``` 98 | 99 | ## Running unit tests 100 | 101 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 102 | 103 | ## Running end-to-end tests 104 | 105 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 106 | 107 | # Acknowledgements 108 | Special thanks to the following! 109 | - [cronoh/nanovault](https://github.com/cronoh) - Creator of nanovault 110 | - [numtel/nano-webgl-pow](https://github.com/numtel/nano-webgl-pow) - WebGL PoW Implementation 111 | - [jaimehgb/RaiBlocksWebAssemblyPoW](https://github.com/jaimehgb/RaiBlocksWebAssemblyPoW) - CPU PoW Implementation 112 | - [dcposch/blakejs](https://github.com/dcposch/blakejs) - Blake2b Implementation 113 | - [dchest/tweetnacl-js](https://github.com/dchest/tweetnacl-js) - Cryptography Implementation 114 | 115 | If you have found BananoVault useful and are feeling generous, you can donate to the original author's nano address: `xrb_318syypnqcgdouy3p3ekckwmnmmyk5z3dpyq48phzndrmmspyqdqjymoo8hj` 116 | -------------------------------------------------------------------------------- /src/app/components/accounts/accounts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {Subject} from "rxjs/Subject"; 3 | import {BehaviorSubject} from "rxjs/BehaviorSubject"; 4 | import {Observable} from "rxjs/Observable"; 5 | import {timer} from "rxjs/observable/timer"; 6 | import {debounce} from "rxjs/operators"; 7 | import { 8 | AppSettingsService, 9 | LedgerService, 10 | LedgerStatus, 11 | ModalService, 12 | NotificationService, 13 | RepresentativeService, 14 | WalletService 15 | } from "../../services"; 16 | 17 | @Component({ 18 | selector: 'app-accounts', 19 | templateUrl: './accounts.component.html', 20 | styleUrls: ['./accounts.component.css'] 21 | }) 22 | export class AccountsComponent implements OnInit { 23 | accounts = this.walletService.wallet.accounts; 24 | isLedgerWallet = this.walletService.isLedgerWallet(); 25 | viewAdvanced = false; 26 | newAccountIndex = null; 27 | 28 | // When we change the accounts, redetect changable reps (Debounce by 5 seconds) 29 | accountsChanged$ = new Subject(); 30 | reloadRepWarning$ = this.accountsChanged$.pipe(debounce(() => timer(5000))); 31 | 32 | constructor( 33 | private walletService: WalletService, 34 | private notificationService: NotificationService, 35 | public modal: ModalService, 36 | public settings: AppSettingsService, 37 | private representatives: RepresentativeService, 38 | private ledger: LedgerService) { } 39 | 40 | async ngOnInit() { 41 | this.reloadRepWarning$.subscribe(a => { 42 | this.representatives.detectChangeableReps(); 43 | }) 44 | } 45 | 46 | async createAccount() { 47 | if (this.walletService.isLocked()) { 48 | return this.notificationService.sendError(`Wallet is locked.`); 49 | } 50 | if (!this.walletService.isConfigured()) return this.notificationService.sendError(`Wallet is not configured`); 51 | if (this.walletService.wallet.accounts.length >= 20) return this.notificationService.sendWarning(`You can only track up to 20 accounts at a time.`); 52 | // Advanced view, manual account index? 53 | let accountIndex = null; 54 | if (this.viewAdvanced && this.newAccountIndex != null) { 55 | let index = parseInt(this.newAccountIndex); 56 | if (index < 0) return this.notificationService.sendWarning(`Invalid account index - must be positive number`); 57 | const existingAccount = this.walletService.wallet.accounts.find(a => a.index == index); 58 | if (existingAccount) { 59 | return this.notificationService.sendWarning(`The account at this index is already loaded`); 60 | } 61 | accountIndex = index; 62 | } 63 | try { 64 | const newAccount = await this.walletService.addWalletAccount(accountIndex); 65 | this.notificationService.sendSuccess(`Successfully created new account ${newAccount.id}`); 66 | this.newAccountIndex = null; 67 | this.accountsChanged$.next(newAccount.id); 68 | } catch (err) { 69 | this.notificationService.sendError(`Unable to add new account: ${err.message}`); 70 | } 71 | } 72 | 73 | sortAccounts() { 74 | if (this.walletService.isLocked()) { 75 | return this.notificationService.sendError(`Wallet is locked.`); 76 | } 77 | if (!this.walletService.isConfigured()) return this.notificationService.sendError(`Wallet is not configured`); 78 | if (this.walletService.wallet.accounts.length <= 1) return this.notificationService.sendWarning(`You need at least 2 accounts to sort them`); 79 | this.walletService.wallet.accounts = this.walletService.wallet.accounts.sort((a, b) => a.index - b.index); 80 | // this.accounts = this.walletService.wallet.accounts; 81 | this.walletService.saveWalletExport(); // Save new sorted accounts list 82 | this.notificationService.sendSuccess(`Successfully sorted accounts by index!`); 83 | } 84 | 85 | copied() { 86 | this.notificationService.sendSuccess(`Successfully copied to clipboard!`); 87 | } 88 | 89 | async deleteAccount(account) { 90 | if (this.walletService.walletIsLocked()) { 91 | return this.notificationService.sendWarning(`Wallet must be unlocked.`); 92 | } 93 | try { 94 | await this.walletService.removeWalletAccount(account.id); 95 | this.notificationService.sendSuccess(`Successfully removed account ${account.id}`); 96 | this.accountsChanged$.next(account.id); 97 | } catch (err) { 98 | this.notificationService.sendError(`Unable to delete account: ${err.message}`); 99 | } 100 | } 101 | 102 | async showLedgerAddress(account) { 103 | if (this.ledger.ledger.status !== LedgerStatus.READY) { 104 | return this.notificationService.sendWarning(`Ledger device must be ready`); 105 | } 106 | this.notificationService.sendInfo(`Confirming account address on Ledger device...`, { identifier: 'ledger-account', length: 0 }); 107 | try { 108 | await this.ledger.getLedgerAccount(account.index, true); 109 | this.notificationService.sendSuccess(`Account address confirmed on Ledger`); 110 | } catch (err) { 111 | this.notificationService.sendError(`Account address denied - if it is wrong do not use the wallet!`); 112 | } 113 | this.notificationService.removeNotification('ledger-account'); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/app/components/transaction-details/transaction-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ActivatedRoute, ActivatedRouteSnapshot, ChildActivationEnd, Router} from "@angular/router"; 3 | import {ApiService} from "../../services/api.service"; 4 | import {AppSettingsService} from "../../services/app-settings.service"; 5 | import BigNumber from "bignumber.js"; 6 | import {AddressBookService} from "../../services/address-book.service"; 7 | 8 | @Component({ 9 | selector: 'app-transaction-details', 10 | templateUrl: './transaction-details.component.html', 11 | styleUrls: ['./transaction-details.component.css'] 12 | }) 13 | export class TransactionDetailsComponent implements OnInit { 14 | banoshi = 1000000000000000000000000000; 15 | 16 | routerSub = null; 17 | transaction: any = {}; 18 | hashID = ''; 19 | blockType = 'send'; 20 | isStateBlock = true; 21 | 22 | toAccountID = ''; 23 | fromAccountID = ''; 24 | toAddressBook = ''; 25 | fromAddressBook = ''; 26 | 27 | transactionJSON = ''; 28 | showBlockData = false; 29 | 30 | amountRaw = new BigNumber(0); 31 | 32 | constructor(private route: ActivatedRoute, 33 | private router: Router, 34 | private addressBook: AddressBookService, 35 | private api: ApiService, 36 | public settings: AppSettingsService 37 | ) { } 38 | 39 | async ngOnInit() { 40 | this.routerSub = this.router.events.subscribe(event => { 41 | if (event instanceof ChildActivationEnd) { 42 | this.loadTransaction(); // Reload the state when navigating to itself from the transactions page 43 | } 44 | }); 45 | 46 | await this.loadTransaction(); 47 | } 48 | 49 | async loadTransaction() { 50 | this.toAccountID = ''; 51 | this.fromAccountID = ''; 52 | this.toAddressBook = ''; 53 | this.fromAddressBook = ''; 54 | this.transactionJSON = ''; 55 | this.showBlockData = false; 56 | let legacyFromAccount = ''; 57 | this.amountRaw = new BigNumber(0); 58 | const hash = this.route.snapshot.params.transaction; 59 | this.hashID = hash; 60 | const blockData = await this.api.blocksInfo([hash]); 61 | if (!blockData || blockData.error || !blockData.blocks[hash]) { 62 | this.transaction = null; 63 | return; 64 | } 65 | const hashData = blockData.blocks[hash]; 66 | const hashContents = JSON.parse(hashData.contents); 67 | hashData.contents = hashContents; 68 | 69 | this.transactionJSON = JSON.stringify(hashData.contents, null ,4); 70 | 71 | this.blockType = hashData.contents.type; 72 | if (this.blockType === 'state') { 73 | const isOpen = hashData.contents.previous === "0000000000000000000000000000000000000000000000000000000000000000"; 74 | if (isOpen) { 75 | this.blockType = 'open' 76 | } else { 77 | const prevRes = await this.api.blocksInfo([hashData.contents.previous]); 78 | const prevData = prevRes.blocks[hashData.contents.previous]; 79 | prevData.contents = JSON.parse(prevData.contents); 80 | if (!prevData.contents.balance) { 81 | // Previous block is not a state block. 82 | this.blockType = prevData.contents.type; 83 | legacyFromAccount = prevData.source_account; 84 | } else { 85 | const prevBalance = new BigNumber(prevData.contents.balance); 86 | const curBalance = new BigNumber(hashData.contents.balance); 87 | const balDifference = curBalance.minus(prevBalance); 88 | if (balDifference.isNegative()) { 89 | this.blockType = 'send'; 90 | } else if (balDifference.isZero()) { 91 | this.blockType = 'change'; 92 | } else { 93 | this.blockType = 'receive'; 94 | } 95 | } 96 | } 97 | } else { 98 | this.isStateBlock = false; 99 | } 100 | if (hashData.amount) { 101 | this.amountRaw = new BigNumber(hashData.amount).mod(this.banoshi); 102 | } 103 | 104 | this.transaction = hashData; 105 | 106 | let fromAccount = ''; 107 | let toAccount = ''; 108 | switch (this.blockType) { 109 | case 'send': 110 | fromAccount = this.transaction.block_account; 111 | toAccount = this.transaction.contents.destination || this.transaction.contents.link_as_account; 112 | break; 113 | case 'open': 114 | case 'receive': 115 | fromAccount = this.transaction.source_account; 116 | toAccount = this.transaction.block_account; 117 | break; 118 | case 'change': 119 | fromAccount = this.transaction.block_account; 120 | toAccount = this.transaction.contents.representative; 121 | break; 122 | } 123 | 124 | if (legacyFromAccount) { 125 | fromAccount = legacyFromAccount; 126 | } 127 | 128 | this.toAccountID = toAccount; 129 | this.fromAccountID = fromAccount; 130 | 131 | this.fromAddressBook = this.addressBook.getAccountName(fromAccount); 132 | this.toAddressBook = this.addressBook.getAccountName(toAccount); 133 | 134 | } 135 | 136 | getBalanceFromHex(balance) { 137 | return new BigNumber(balance, 16); 138 | } 139 | 140 | getBalanceFromDec(balance) { 141 | return new BigNumber(balance, 10); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/app/components/receive/receive.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {WalletService} from "../../services/wallet.service"; 3 | import {NotificationService} from "../../services/notification.service"; 4 | import {ModalService} from "../../services/modal.service"; 5 | import {ApiService} from "../../services/api.service"; 6 | import * as blake from 'blakejs'; 7 | import BigNumber from "bignumber.js"; 8 | import {UtilService} from "../../services/util.service"; 9 | import {WorkPoolService} from "../../services/work-pool.service"; 10 | import {AppSettingsService} from "../../services/app-settings.service"; 11 | import {BananoBlockService} from "../../services/nano-block.service"; 12 | const nacl = window['nacl']; 13 | 14 | @Component({ 15 | selector: 'app-receive', 16 | templateUrl: './receive.component.html', 17 | styleUrls: ['./receive.component.css'] 18 | }) 19 | export class ReceiveComponent implements OnInit { 20 | accounts = this.walletService.wallet.accounts; 21 | 22 | pendingAccountModel = 0; 23 | pendingBlocks = []; 24 | 25 | constructor( 26 | private walletService: WalletService, 27 | private notificationService: NotificationService, 28 | public modal: ModalService, 29 | private api: ApiService, 30 | private workPool: WorkPoolService, 31 | public settings: AppSettingsService, 32 | private bananoBlock: BananoBlockService, 33 | private util: UtilService) { } 34 | 35 | async ngOnInit() { 36 | await this.loadPendingForAll(); 37 | } 38 | 39 | async loadPendingForAll() { 40 | this.pendingBlocks = []; 41 | 42 | let pending; 43 | if (this.settings.settings.minimumReceive) { 44 | const minAmount = this.util.banano.banToRaw(this.settings.settings.minimumReceive); 45 | pending = await this.api.accountsPendingLimit(this.accounts.map(a => a.id), minAmount.toString(10)); 46 | } else { 47 | pending = await this.api.accountsPending(this.accounts.map(a => a.id)); 48 | } 49 | if (!pending || !pending.blocks) return; 50 | 51 | for (let account in pending.blocks) { 52 | if (!pending.blocks.hasOwnProperty(account)) continue; 53 | for (let block in pending.blocks[account]) { 54 | if (!pending.blocks[account].hasOwnProperty(block)) continue; 55 | const pendingTx = { 56 | block: block, 57 | amount: pending.blocks[account][block].amount, 58 | source: pending.blocks[account][block].source, 59 | account: account, 60 | }; 61 | // Account should be one of ours, so we should maybe know the frontier block for it? 62 | 63 | this.pendingBlocks.push(pendingTx); 64 | } 65 | } 66 | 67 | // Now, only if we have results, do a unique on the account names, and run account info on all of them? 68 | if (this.pendingBlocks.length) { 69 | const frontiers = await this.api.accountsFrontiers(this.pendingBlocks.map(p => p.account)); 70 | if (frontiers && frontiers.frontiers) { 71 | for (let account in frontiers.frontiers) { 72 | if (!frontiers.frontiers.hasOwnProperty(account)) continue; 73 | this.workPool.addWorkToCache(frontiers.frontiers[account]); 74 | } 75 | } 76 | } 77 | 78 | } 79 | 80 | async loadPendingForAccount(account) { 81 | this.pendingBlocks = []; 82 | 83 | let pending; 84 | if (this.settings.settings.minimumReceive) { 85 | const minAmount = this.util.banano.banToRaw(this.settings.settings.minimumReceive); 86 | pending = await this.api.pendingLimit(account, 50, minAmount.toString(10)); 87 | } else { 88 | pending = await this.api.pending(account, 50); 89 | } 90 | if (!pending || !pending.blocks) return; 91 | 92 | for (let block in pending.blocks) { 93 | const pendingTx = { 94 | block: block, 95 | amount: pending.blocks[block].amount, 96 | source: pending.blocks[block].source, 97 | account: account, 98 | }; 99 | this.pendingBlocks.push(pendingTx); 100 | } 101 | } 102 | 103 | async getPending(account) { 104 | if (!account || account == 0) { 105 | await this.loadPendingForAll(); 106 | } else { 107 | await this.loadPendingForAccount(account); 108 | } 109 | } 110 | 111 | async receivePending(pendingBlock) { 112 | const sourceBlock = pendingBlock.block; 113 | 114 | const walletAccount = this.walletService.wallet.accounts.find(a => a.id == pendingBlock.account); 115 | if (!walletAccount) throw new Error(`unable to find receiving account in wallet`); 116 | 117 | if (this.walletService.walletIsLocked()) return this.notificationService.sendWarning(`Wallet must be unlocked`); 118 | pendingBlock.loading = true; 119 | 120 | const newBlock = await this.bananoBlock.generateReceive(walletAccount, sourceBlock, this.walletService.isLedgerWallet()); 121 | 122 | if (newBlock) { 123 | this.notificationService.sendSuccess(`Successfully received BANANO!`); 124 | } else { 125 | if (!this.walletService.isLedgerWallet()) { 126 | this.notificationService.sendError(`There was an error receiving the transaction`) 127 | } 128 | } 129 | 130 | pendingBlock.loading = false; 131 | 132 | await this.walletService.reloadBalances(); 133 | await this.loadPendingForAll(); 134 | } 135 | 136 | copied() { 137 | this.notificationService.sendSuccess(`Successfully copied to clipboard!`); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/app/components/address-book/address-book.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Address Book

6 |
7 |
8 | 9 |
10 |
11 |

12 | You can use the address book to store a label for your own accounts and others you frequently transact with, which are visible throughout the application. 13 |

14 | 15 | 16 |
17 |
18 |
19 | 20 |
    21 |
  • 22 |
    23 |
    Name
    24 |
    Account ID
    25 |
    Options
    26 |
    27 |
  • 28 |
29 | 30 | 66 | 67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |

Create New Contact

76 |
77 |
78 |
79 |
80 | 81 |
82 | 83 |
84 |
85 | 86 |
87 | 88 |
89 | 90 |
91 |
92 |
93 |
94 | 104 |
105 |
106 |
107 | 108 | 109 |
110 |
111 | -------------------------------------------------------------------------------- /src/app/components/manage-wallet/manage-wallet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {WalletService} from "../../services/wallet.service"; 3 | import {NotificationService} from "../../services/notification.service"; 4 | import * as QRCode from 'qrcode'; 5 | import {AddressBookService} from "../../services/address-book.service"; 6 | import {Router} from "@angular/router"; 7 | import * as bip from 'bip39'; 8 | 9 | @Component({ 10 | selector: 'app-manage-wallet', 11 | templateUrl: './manage-wallet.component.html', 12 | styleUrls: ['./manage-wallet.component.css'] 13 | }) 14 | export class ManageWalletComponent implements OnInit { 15 | 16 | wallet = this.walletService.wallet; 17 | 18 | newPassword = ''; 19 | confirmPassword = ''; 20 | 21 | showQRExport = false; 22 | QRExportUrl = ''; 23 | QRExportImg = ''; 24 | addressBookShowQRExport = false; 25 | addressBookQRExportUrl = ''; 26 | addressBookQRExportImg = ''; 27 | 28 | constructor( 29 | public walletService: WalletService, 30 | private addressBookService: AddressBookService, 31 | public notifications: NotificationService, 32 | private router: Router) { } 33 | 34 | async ngOnInit() { 35 | this.wallet = this.walletService.wallet; 36 | } 37 | 38 | async changePassword() { 39 | if (this.newPassword !== this.confirmPassword) return this.notifications.sendError(`Passwords do not match`); 40 | if (this.newPassword.length < 1) return this.notifications.sendError(`Password cannot be empty`); 41 | if (this.walletService.walletIsLocked()) return this.notifications.sendWarning(`Wallet must be unlocked`); 42 | 43 | this.walletService.wallet.password = this.newPassword; 44 | this.walletService.saveWalletExport(); 45 | 46 | this.newPassword = ''; 47 | this.confirmPassword = ''; 48 | this.notifications.sendSuccess(`Wallet password successfully updated`); 49 | } 50 | 51 | async exportWallet() { 52 | if (this.walletService.walletIsLocked()) return this.notifications.sendWarning(`Wallet must be unlocked`); 53 | 54 | const exportUrl = this.walletService.generateExportUrl(); 55 | this.QRExportUrl = exportUrl; 56 | this.QRExportImg = await QRCode.toDataURL(exportUrl); 57 | this.showQRExport = true; 58 | } 59 | 60 | copied() { 61 | this.notifications.sendSuccess(`Wallet seed copied to clipboard!`); 62 | } 63 | 64 | seedMnemonic() { 65 | return bip.entropyToMnemonic(this.wallet.seed); 66 | } 67 | 68 | async exportAddressBook() { 69 | const exportData = this.addressBookService.addressBook; 70 | if (exportData.length >= 25) { 71 | return this.notifications.sendError(`Address books with 25 or more entries need to use the file export method.`); 72 | } 73 | const base64Data = btoa(JSON.stringify(exportData)); 74 | const exportUrl = `https://vault.banano.cc/import-address-book#${base64Data}`; 75 | 76 | this.addressBookQRExportUrl = exportUrl; 77 | this.addressBookQRExportImg = await QRCode.toDataURL(exportUrl); 78 | this.addressBookShowQRExport = true; 79 | } 80 | 81 | exportAddressBookToFile() { 82 | if (this.walletService.walletIsLocked()) return this.notifications.sendWarning(`Wallet must be unlocked`); 83 | const fileName = `BananoVault-AddressBook.json`; 84 | 85 | const exportData = this.addressBookService.addressBook; 86 | this.triggerFileDownload(fileName, exportData); 87 | 88 | this.notifications.sendSuccess(`Address book export downloaded!`); 89 | } 90 | 91 | triggerFileDownload(fileName, exportData) { 92 | const blob = new Blob([JSON.stringify(exportData)], { type: 'application/json' }); 93 | 94 | // Check for iOS, which is weird with saving files 95 | const iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); 96 | 97 | if (window.navigator.msSaveOrOpenBlob) { 98 | window.navigator.msSaveBlob(blob, fileName); 99 | } else { 100 | const elem = window.document.createElement('a'); 101 | const objUrl = window.URL.createObjectURL(blob); 102 | if (iOS) { 103 | elem.href = `data:attachment/file,${JSON.stringify(exportData)}`; 104 | } else { 105 | elem.href = objUrl; 106 | } 107 | elem.download = fileName; 108 | document.body.appendChild(elem); 109 | elem.click(); 110 | setTimeout(function(){ 111 | document.body.removeChild(elem); 112 | window.URL.revokeObjectURL(objUrl); 113 | }, 200); 114 | } 115 | } 116 | 117 | exportToFile() { 118 | if (this.walletService.walletIsLocked()) return this.notifications.sendWarning(`Wallet must be unlocked`); 119 | 120 | const fileName = `BananoVault-Wallet.json`; 121 | const exportData = this.walletService.generateExportData(); 122 | this.triggerFileDownload(fileName, exportData); 123 | 124 | this.notifications.sendSuccess(`Wallet export downloaded!`); 125 | } 126 | 127 | importFromFile(files) { 128 | if (!files.length) return; 129 | 130 | const file = files[0]; 131 | const reader = new FileReader(); 132 | reader.onload = (event) => { 133 | const fileData = event.target['result']; 134 | try { 135 | const importData = JSON.parse(fileData); 136 | if (!importData.length || !importData[0].account) { 137 | return this.notifications.sendError(`Bad import data, make sure you selected a BananoVault Address Book export`) 138 | } 139 | 140 | const walletEncrypted = btoa(JSON.stringify(importData)); 141 | this.router.navigate(['import-address-book'], { fragment: walletEncrypted }); 142 | } catch (err) { 143 | this.notifications.sendError(`Unable to parse import data, make sure you selected the right file!`); 144 | } 145 | }; 146 | 147 | reader.readAsText(file); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/app/components/manage-wallet/manage-wallet.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Manage Wallet

4 | 5 |
6 |
7 |

Change Wallet Password

8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | 22 |
23 | 24 |
25 |
26 |

Backup Wallet

27 |
28 |
29 |

30 | To access backup options, unlock your wallet. 31 |

32 |

33 | To backup your wallet mnemonic phrase, click here to copy it to your clipboard.
34 |
35 | To backup your wallet seed, click here to copy it to your clipboard.
36 |

37 |
38 |
39 | 40 |
41 |
42 |

Export BananoVault Wallet

43 |
44 |
45 | Use this export tool to simplify wallet transfer to other devices. Your data will be encrypted by your password, and then imported into BananoVault on your new device! 46 | The export does not contain your address book.
47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 | Scan the QR code on any device to load your BananoVault wallet!
57 |
58 | If you do not have a QR code scanner, you can also import your wallet by using the URL below.
59 |
60 | Copy to clipboard 61 |
62 |
63 |
64 | 68 |
69 | 70 |
71 |
72 |

BananoVault Address Book

73 |
74 |
75 | Use this tool to simplify transferring your address book between devices. Use the options below to import or export your 76 | address book from a file or QR Code/URL. Your address book is not encrypted by your wallet password. 77 |
78 |
79 |
80 |
81 | 82 |
83 | 84 |
85 |
86 | Scan the QR code on any device to import your BananoVault Address Book!
87 |
88 | If you do not have a QR code scanner, you can also import your address book by using the URL below.
89 |
90 | Copy to clipboard 91 |
92 |
93 |
94 | 102 |
103 | 104 |
105 |
106 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostListener, OnInit} from '@angular/core'; 2 | import {WalletService} from "./services/wallet.service"; 3 | import {AddressBookService} from "./services/address-book.service"; 4 | import {AppSettingsService} from "./services/app-settings.service"; 5 | import {WebsocketService} from "./services/websocket.service"; 6 | import {PriceService} from "./services/price.service"; 7 | import {NotificationService} from "./services/notification.service"; 8 | import {PowService} from "./services/pow.service"; 9 | import {WorkPoolService} from "./services/work-pool.service"; 10 | import {Router} from "@angular/router"; 11 | import {RepresentativeService} from "./services/representative.service"; 12 | import {NodeService} from "./services/node.service"; 13 | import { LedgerService } from './services'; 14 | 15 | 16 | @Component({ 17 | selector: 'app-root', 18 | templateUrl: './app.component.html', 19 | styleUrls: ['./app.component.css'] 20 | }) 21 | export class AppComponent implements OnInit { 22 | @HostListener('window:resize', ['$event']) onResize (e) { 23 | this.windowHeight = e.target.innerHeight; 24 | }; 25 | wallet = this.walletService.wallet; 26 | node = this.nodeService.node; 27 | bananoPrice = this.price.price; 28 | fiatTimeout = 5 * 60 * 1000; // Update fiat prices every 5 minutes 29 | inactiveSeconds = 0; 30 | windowHeight = 1000; 31 | showSearchBar = false; 32 | searchData = ''; 33 | isConfigured = this.walletService.isConfigured; 34 | 35 | constructor( 36 | public walletService: WalletService, 37 | private addressBook: AddressBookService, 38 | public settings: AppSettingsService, 39 | private websocket: WebsocketService, 40 | private notifications: NotificationService, 41 | private pow: PowService, 42 | public nodeService: NodeService, 43 | private representative: RepresentativeService, 44 | private router: Router, 45 | private workPool: WorkPoolService, 46 | private ledger: LedgerService, 47 | public price: PriceService) { } 48 | 49 | async ngOnInit() { 50 | this.windowHeight = window.innerHeight; 51 | this.settings.loadAppSettings(); 52 | this.addressBook.loadAddressBook(); 53 | this.workPool.loadWorkCache(); 54 | await this.walletService.loadStoredWallet(); 55 | this.websocket.connect(); 56 | 57 | this.representative.loadRepresentativeList(); 58 | 59 | // If the wallet is locked and there is a pending balance, show a warning to unlock the wallet 60 | if (this.wallet.locked && this.walletService.hasPendingTransactions()) { 61 | this.notifications.sendWarning(`New incoming transaction - unlock the wallet to receive it!`, { length: 0, identifier: 'pending-locked' }); 62 | } 63 | 64 | // If they are using a Ledger device with a bad browser, warn them 65 | if (this.walletService.isLedgerWallet() && this.ledger.isBrokenBrowser()) { 66 | this.notifications.sendLedgerChromeWarning(); 67 | } 68 | 69 | // When the page closes, determine if we should lock the wallet 70 | window.addEventListener("beforeunload", (e) => { 71 | if (this.wallet.locked) return; // Already locked, nothing to worry about 72 | this.walletService.lockWallet(); 73 | }); 74 | window.addEventListener("unload", (e) => { 75 | if (this.wallet.locked) return; // Already locked, nothing to worry about 76 | this.walletService.lockWallet(); 77 | }); 78 | 79 | // Listen for an ban: protocol link, triggered by the desktop application 80 | window.addEventListener('protocol-load', (e: CustomEvent) => { 81 | const protocolText = e.detail; 82 | const stripped = protocolText.split('').splice(4).join(''); // Remove ban: 83 | if (stripped.startsWith('ban_')) { 84 | this.router.navigate(['account', stripped]); 85 | } 86 | // Soon: Load seed, automatic send page? 87 | }); 88 | 89 | // Check how long the wallet has been inactive, and lock it if it's been too long 90 | setInterval(() => { 91 | this.inactiveSeconds += 1; 92 | if (!this.settings.settings.lockInactivityMinutes) return; // Do not lock on inactivity 93 | if (this.wallet.locked || !this.wallet.password) return; 94 | 95 | // Determine if we have been inactive for longer than our lock setting 96 | if (this.inactiveSeconds >= this.settings.settings.lockInactivityMinutes * 60) { 97 | this.walletService.lockWallet(); 98 | this.notifications.sendSuccess(`Wallet locked after ${this.settings.settings.lockInactivityMinutes} minutes of inactivity`); 99 | } 100 | }, 1000); 101 | 102 | try { 103 | await this.updateFiatPrices(); 104 | } catch (err) { 105 | this.notifications.sendWarning(`There was an issue retrieving latest Banano price. Ensure your AdBlocker is disabled on this page then reload to see accurate FIAT values.`, { length: 0, identifier: `price-adblock` }); 106 | } 107 | 108 | } 109 | 110 | toggleSearch(mobile = false) { 111 | this.showSearchBar = !this.showSearchBar; 112 | if (this.showSearchBar) { 113 | setTimeout(() => document.getElementById(mobile ? 'search-input-mobile' : 'search-input').focus(), 150); 114 | } 115 | } 116 | 117 | performSearch() { 118 | const searchData = this.searchData.trim(); 119 | if (!searchData.length) return; 120 | 121 | if (searchData.startsWith('ban_')) { 122 | this.router.navigate(['account', searchData]); 123 | } else if (searchData.length === 64) { 124 | this.router.navigate(['transaction', searchData]); 125 | } else { 126 | this.notifications.sendWarning(`Invalid Banano account or transaction hash!`) 127 | } 128 | this.searchData = ''; 129 | } 130 | 131 | updateIdleTime() { 132 | this.inactiveSeconds = 0; // Action has happened, reset the inactivity timer 133 | } 134 | 135 | retryConnection() { 136 | this.walletService.reloadBalances(true); 137 | this.notifications.sendInfo(`Attempting to reconnect to Banano node`); 138 | } 139 | 140 | async updateFiatPrices() { 141 | const displayCurrency = this.settings.getAppSetting(`displayCurrency`) || ''; 142 | await this.price.getPrice(displayCurrency); 143 | this.walletService.reloadFiatBalances(); 144 | setTimeout(() => this.updateFiatPrices(), this.fiatTimeout); 145 | } 146 | 147 | 148 | 149 | 150 | } 151 | 152 | 153 | -------------------------------------------------------------------------------- /desktop-app/src/desktop-app.ts: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import { app, BrowserWindow, shell, Menu, protocol, webFrame, ipcMain } from 'electron'; 4 | import { autoUpdater } from 'electron-updater'; 5 | import * as url from 'url'; 6 | import * as path from 'path'; 7 | import { initialize } from './lib/ledger'; 8 | 9 | app.setAsDefaultProtocolClient('xrb'); // Register handler for xrb: links 10 | 11 | // Initialize Ledger device detection 12 | initialize(); 13 | 14 | let mainWindow; 15 | 16 | function createWindow () { 17 | // Create the browser window. 18 | mainWindow = new BrowserWindow({width: 1000, height: 600, webPreferences: { webSecurity: false, devTools: true } }); 19 | 20 | // mainWindow.loadURL('http://localhost:4200/'); // Only use this for development 21 | mainWindow.loadURL(url.format({ 22 | pathname: path.join(__dirname, '../../dist/index.html'), 23 | protocol: 'file:', 24 | slashes: true 25 | })); 26 | 27 | // Emitted when the window is closed. 28 | mainWindow.on('closed', function () { 29 | mainWindow = null 30 | }); 31 | 32 | // Detect link clicks to new windows and open them in the default browser 33 | mainWindow.webContents.on('new-window', function(e, url) { 34 | e.preventDefault(); 35 | shell.openExternal(url); 36 | }); 37 | 38 | const menuTemplate = getApplicationMenu(); 39 | 40 | // Create our menu entries so that we can use MAC shortcuts 41 | Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)); 42 | } 43 | 44 | app.on('ready', () => { 45 | // Once the app is ready, launch the wallet window 46 | createWindow(); 47 | 48 | // Detect when the application has been loaded using an xrb: link, send it to the wallet to load 49 | app.on('open-url', (event, path) => { 50 | if (!mainWindow) { 51 | createWindow(); 52 | } 53 | if (!mainWindow.webContents.isLoading()) { 54 | mainWindow.webContents.executeJavaScript(`window.dispatchEvent(new CustomEvent('protocol-load', { detail: '${path}' }));`); 55 | } 56 | mainWindow.webContents.once('did-finish-load', () => { 57 | mainWindow.webContents.executeJavaScript(`window.dispatchEvent(new CustomEvent('protocol-load', { detail: '${path}' }));`); 58 | }); 59 | event.preventDefault(); 60 | }); 61 | 62 | // Check for any updates on GitHub 63 | checkForUpdates(); 64 | }); 65 | 66 | // Quit when all windows are closed. 67 | app.on('window-all-closed', function () { 68 | // On OS X it is common for applications and their menu bar 69 | // to stay active until the user quits explicitly with Cmd + Q 70 | if (process.platform !== 'darwin') { 71 | app.quit() 72 | } 73 | }); 74 | 75 | app.on('activate', function () { 76 | // On OS X it's common to re-create a window in the app when the 77 | // dock icon is clicked and there are no other windows open. 78 | if (mainWindow === null) { 79 | createWindow() 80 | } 81 | }); 82 | 83 | function checkForUpdates() { 84 | autoUpdater.checkForUpdatesAndNotify() 85 | .then(() => {}) 86 | .catch(console.log); 87 | } 88 | 89 | // Build up the menu bar options based on platform 90 | function getApplicationMenu() { 91 | const template: any = [ 92 | { 93 | label: 'Edit', 94 | submenu: [ 95 | {role: 'undo'}, 96 | {role: 'redo'}, 97 | {type: 'separator'}, 98 | {role: 'cut'}, 99 | {role: 'copy'}, 100 | {role: 'paste'}, 101 | {role: 'pasteandmatchstyle'}, 102 | {role: 'delete'}, 103 | {role: 'selectall'} 104 | ] 105 | }, 106 | { 107 | label: 'View', 108 | submenu: [ 109 | {role: 'reload'}, 110 | {role: 'forcereload'}, 111 | {role: 'toggledevtools'}, 112 | {type: 'separator'}, 113 | {role: 'resetzoom'}, 114 | {role: 'zoomin'}, 115 | {role: 'zoomout'}, 116 | {type: 'separator'}, 117 | {role: 'togglefullscreen'} 118 | ] 119 | }, 120 | { 121 | role: 'window', 122 | submenu: [ 123 | {role: 'minimize'}, 124 | {role: 'close'} 125 | ] 126 | }, 127 | { 128 | role: 'help', 129 | submenu: [ 130 | { 131 | label: 'View GitHub', 132 | click () { loadExternal('https://github.com/bananocoin/bananovault') } 133 | }, 134 | { 135 | label: 'Submit Issue', 136 | click () { loadExternal('https://github.com/bananocoin/bananovault/issues/new') } 137 | }, 138 | {type: 'separator'}, 139 | { 140 | type: 'normal', 141 | label: `BananoVault Version: ${autoUpdater.currentVersion}`, 142 | }, 143 | { 144 | label: 'View Latest Updates', 145 | click () { loadExternal('https://github.com/bananocoin/bananovault/releases') } 146 | }, 147 | {type: 'separator'}, 148 | { 149 | label: `Check for Updates...`, 150 | click (menuItem, browserWindow) { 151 | checkForUpdates(); 152 | } 153 | }, 154 | ] 155 | } 156 | ]; 157 | 158 | if (process.platform === 'darwin') { 159 | template.unshift({ 160 | label: 'BananoVault', 161 | submenu: [ 162 | {role: 'about'}, 163 | {type: 'separator'}, 164 | { 165 | label: `Check for Updates...`, 166 | click (menuItem, browserWindow) { 167 | checkForUpdates(); 168 | } 169 | }, 170 | {type: 'separator'}, 171 | // {role: 'services', submenu: []}, 172 | // {type: 'separator'}, 173 | {role: 'hide'}, 174 | {role: 'hideothers'}, 175 | {role: 'unhide'}, 176 | {type: 'separator'}, 177 | {role: 'quit'} 178 | ] 179 | }); 180 | 181 | // Edit menu 182 | template[1].submenu.push( 183 | {type: 'separator'}, 184 | { 185 | label: 'Speech', 186 | submenu: [ 187 | {role: 'startspeaking'}, 188 | {role: 'stopspeaking'} 189 | ] 190 | } 191 | ); 192 | 193 | // Window menu 194 | template[3].submenu = [ 195 | {role: 'close'}, 196 | {role: 'minimize'}, 197 | {role: 'zoom'}, 198 | {type: 'separator'}, 199 | {role: 'front'} 200 | ]; 201 | } 202 | 203 | return template; 204 | } 205 | 206 | function loadExternal(url) { 207 | shell.openExternal(url); 208 | } 209 | --------------------------------------------------------------------------------