├── static ├── robots.txt └── favicon.ico ├── assets ├── hand.png ├── auth-bg.png ├── icon-tor.png ├── lightning.png ├── loading-dots.gif ├── extension-promo.png ├── chevron.svg ├── copy.svg ├── alert.svg ├── icon-circle-warning.svg ├── icon-close.svg ├── fee-cheap.svg ├── fee-default.svg ├── fee-fast.svg ├── fee-slow.svg ├── icon-password.svg ├── lightning.svg ├── gray-logo.svg ├── info.svg ├── icon-info-blue.svg ├── logo-casa-white.svg ├── red-excl.svg ├── channel.svg ├── yellow-excl.svg ├── chevron-blue.svg ├── paper-plane.svg ├── bitcoin.svg ├── checkmark.svg ├── green-check.svg ├── blue-check.svg ├── casa-c-logo.svg ├── settings.svg ├── qr-code.svg ├── casa.svg ├── logo.svg └── logo-casa-extension.svg ├── qemu-arm-static ├── helpers ├── event-bus.js ├── constants.js ├── env.js ├── units.js ├── api.js ├── redirects.js └── interval-bus.js ├── plugins ├── vue-moment.js ├── vue-offline.js ├── vue-qr-code.js ├── vue-clipboard.js ├── vue-slideout.js ├── font-awesome.js ├── interceptor.js ├── filters.js └── vee-validate.js ├── cypress.env.example.json ├── test ├── browser │ ├── screenshots │ │ └── All Specs │ │ │ ├── my-image.png │ │ │ └── my-image (1).png │ ├── integration │ │ ├── welcome.spec.js │ │ ├── login.spec.js │ │ └── btc-transaction.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── index.js │ │ └── commands.js ├── interval-bus.test.js └── unit │ └── interval-bus.test.js ├── README.md ├── .dockerignore ├── .env-sample ├── .editorconfig ├── .gitignore ├── store ├── index.js └── README.md ├── cypress.json ├── css ├── pages │ ├── bitcoin.scss │ ├── deposit.scss │ ├── dashboard.scss │ ├── lightning.scss │ ├── autopilot.scss │ ├── connections.scss │ └── withdraw.scss ├── components │ ├── welcome.scss │ ├── stats.scss │ ├── unit-switch.scss │ ├── bitcoin.scss │ ├── transactions.scss │ ├── modals.scss │ ├── slideout.scss │ └── channels.scss ├── main.scss └── colors.scss ├── middleware ├── teardown.js └── intro.js ├── Dockerfile ├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── .eslintrc.js ├── layouts ├── loading.vue ├── default.vue └── login.vue ├── components ├── Lightning │ ├── Alerts │ │ ├── Success.vue │ │ ├── Caution.vue │ │ ├── ConfirmSave.vue │ │ ├── ConfirmCancel.vue │ │ ├── Welcome.vue │ │ ├── Alert.vue │ │ └── Unlock.vue │ └── Modals │ │ ├── SendPayment │ │ ├── Send.vue │ │ └── ConfirmPayment.vue │ │ ├── PaymentRequests │ │ ├── Request.vue │ │ ├── ConfirmInvoice.vue │ │ └── RequestLnd.vue │ │ ├── ConnectionCode.vue │ │ ├── ConnectionDetails.vue │ │ └── Channels │ │ ├── ConfirmCloseChannel.vue │ │ └── ManageChannel.vue ├── Bitcoin │ ├── Alerts │ │ ├── ConfirmRedownload.vue │ │ └── ConfirmSync.vue │ └── Modals │ │ ├── Withdraw │ │ ├── ConfirmSend.vue │ │ └── Confirm.vue │ │ ├── ConnectionDetails.vue │ │ └── Deposit │ │ └── Deposit.vue └── Settings │ ├── UnitSwitch.vue │ ├── Modals │ └── TorAddress.vue │ └── Alerts │ ├── ConfirmFactoryReset.vue │ ├── UpdateNotice.vue │ ├── ConfirmUpdate.vue │ └── ConfirmShutdown.vue ├── Dockerfile.armhf ├── LICENSE ├── pages ├── please-restart.vue ├── shutdown.vue ├── loading-password.vue ├── loading.vue └── login.vue ├── package.json └── nuxt.config.js /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /assets/hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/hand.png -------------------------------------------------------------------------------- /qemu-arm-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/qemu-arm-static -------------------------------------------------------------------------------- /assets/auth-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/auth-bg.png -------------------------------------------------------------------------------- /assets/icon-tor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/icon-tor.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /assets/lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/lightning.png -------------------------------------------------------------------------------- /assets/loading-dots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/loading-dots.gif -------------------------------------------------------------------------------- /helpers/event-bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | const EventBus = new Vue(); 3 | export default EventBus; 4 | -------------------------------------------------------------------------------- /assets/extension-promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/assets/extension-promo.png -------------------------------------------------------------------------------- /plugins/vue-moment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueMoment from 'vue-moment'; 3 | 4 | Vue.use(VueMoment); 5 | -------------------------------------------------------------------------------- /cypress.env.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "password": "your_node_password", 3 | "btc_address": "address_for_testing" 4 | } 5 | -------------------------------------------------------------------------------- /plugins/vue-offline.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueOffline from 'vue-offline'; 3 | 4 | Vue.use(VueOffline); 5 | -------------------------------------------------------------------------------- /plugins/vue-qr-code.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueQriously from 'vue-qriously'; 3 | 4 | Vue.use(VueQriously); 5 | -------------------------------------------------------------------------------- /plugins/vue-clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueClipboard from 'vue-clipboard2'; 3 | 4 | Vue.use(VueClipboard); 5 | -------------------------------------------------------------------------------- /test/browser/screenshots/All Specs/my-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/test/browser/screenshots/All Specs/my-image.png -------------------------------------------------------------------------------- /test/browser/screenshots/All Specs/my-image (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casa/Casa-Node-Dashboard/HEAD/test/browser/screenshots/All Specs/my-image (1).png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the Casa Node Web Interface. It is responsible for displaying information to the user in a web browser and communicating with the Casa Node APIs. 2 | -------------------------------------------------------------------------------- /plugins/vue-slideout.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { VueSlideoutPanel } from 'vue2-slideout-panel'; 3 | 4 | Vue.component('slideout-panel', VueSlideoutPanel); 5 | -------------------------------------------------------------------------------- /test/browser/integration/welcome.spec.js: -------------------------------------------------------------------------------- 1 | describe('Node Welcome', function() { 2 | it('Visits the home page', function() { 3 | cy.visit('/'); 4 | cy.contains('.subtitle', 'Welcome home.'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /assets/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | node_modules 3 | npm-debug.log 4 | README.md 5 | .git 6 | .gitignore 7 | .idea 8 | Dockerfile 9 | .eslintrc 10 | .eslintrc.js 11 | .eslintignore 12 | .editorconfig 13 | .nuxt 14 | dist/ 15 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | # Rename to .env and replace with your settings 2 | SYSLOG_TAG=nick 3 | DEVICE_HOST=http://18.224.31.235 4 | BITCOIN_EXPLORER=https://testnet.smartbit.com.au/tx/ 5 | LIGHTNING_EXPLORER=https://1ml.com/testnet/node/ 6 | -------------------------------------------------------------------------------- /helpers/constants.js: -------------------------------------------------------------------------------- 1 | const contants = { 2 | MAX_CHANNELS: 40, 3 | MAX_CHANNEL_SIZE_BTC: 0.16777216, 4 | MAX_CHANNEL_SIZE_SATS: 16777216, 5 | TOAST_DURATION_SHORT: 3000, 6 | TOAST_DURATION_LONG: 10000, 7 | }; 8 | 9 | export default contants; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /assets/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # environment 5 | .env 6 | 7 | # logs 8 | npm-debug.log 9 | 10 | # Nuxt build 11 | .nuxt 12 | 13 | # Nuxt generate 14 | dist 15 | 16 | # IDE 17 | .idea 18 | 19 | reports/ 20 | selenium-server.log 21 | 22 | test/cypress/screenshots/ 23 | cypress.env.json 24 | -------------------------------------------------------------------------------- /assets/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icon-circle-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | export const actions = { 2 | async onHttpRequest(store, context) {}, 3 | }; 4 | 5 | export const getters = { 6 | isAuthenticated(state) { 7 | console.log('state.auth', state.auth); 8 | return state.auth.loggedIn; 9 | }, 10 | 11 | loggedInUser(state) { 12 | return state.auth.user; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "fixturesFolder": "test/browser/fixtures", 4 | "integrationFolder": "test/browser/integration", 5 | "pluginsFile": "test/browser/plugins/index.js", 6 | "screenshotsFolder": "test/browser/screenshots", 7 | "videosFolder": "test/browser/videos", 8 | "supportFile": "test/browser/support/index.js" 9 | } 10 | -------------------------------------------------------------------------------- /assets/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /plugins/font-awesome.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import FontAwesomeIcon from '@fortawesome/vue-fontawesome'; 3 | import fontawesome from '@fortawesome/fontawesome'; 4 | 5 | // add more icons to library ex/ add spinner (use comma to add multiple) 6 | // import {faSpinner} from '@fortawesome/free-solid-svg-icons' 7 | // fontawesome.library.add(faSpinner); 8 | 9 | Vue.component(FontAwesomeIcon.name, FontAwesomeIcon); 10 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory activate the option in the framework automatically. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /css/pages/bitcoin.scss: -------------------------------------------------------------------------------- 1 | .bitcoin-menu { 2 | .transaction-list-wrap { 3 | height: calc(100vh - 350px); 4 | overflow: auto; 5 | } 6 | 7 | /* Tablet */ 8 | @media screen and (min-width: 769px) { 9 | .transaction-list-wrap { 10 | height: calc(100vh - 385px); 11 | } 12 | } 13 | 14 | /* Desktop */ 15 | @media screen and (min-width: 1088px) { 16 | .transaction-list-wrap { 17 | height: calc(100vh - 440px); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /helpers/env.js: -------------------------------------------------------------------------------- 1 | // Helper function to define API URLs 2 | export default function updateEnv(app) { 3 | let url = app.$env.DEVICE_HOST; 4 | 5 | // We have to check process.browser to prevent server-side rendering errors 6 | if (process.browser && window.location.href.includes('.onion')) { 7 | url = app.$env.CASA_NODE_HIDDEN_SERVICE; 8 | } 9 | 10 | app.$env.API_MANAGER = `${url}:3000`; 11 | app.$env.API_LND = `${url}:3002`; 12 | app.$env.UPDATE_MANAGER = `${url}:3001`; 13 | } 14 | -------------------------------------------------------------------------------- /middleware/teardown.js: -------------------------------------------------------------------------------- 1 | import BitcoinData, { BitcoinSetup, BitcoinTeardown } from '@/data/bitcoin'; 2 | import LightningData, { LightningSetup, LightningTeardown } from '@/data/lightning'; 3 | import SystemData, { SystemSetup, SystemTeardown } from '@/data/system'; 4 | 5 | export default async function (context) { 6 | // Make sure we aren't still trying to make API calls for the dashboard after navigating to another page 7 | BitcoinTeardown(); 8 | LightningTeardown(); 9 | SystemTeardown(); 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # specify the node base image with your desired version node: 2 | FROM node:8-slim 3 | 4 | # Create app directory 5 | WORKDIR /usr/src/app 6 | 7 | # Install app dependencies 8 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 9 | # where available (npm@5+) 10 | COPY package*.json ./ 11 | 12 | RUN CYPRESS_INSTALL_BINARY=0 yarn install 13 | 14 | # Bundle app source 15 | COPY . . 16 | 17 | RUN yarn run build 18 | 19 | ENV HOST 0.0.0.0 20 | 21 | EXPOSE 3000 22 | CMD [ "yarn", "start" ] 23 | -------------------------------------------------------------------------------- /plugins/interceptor.js: -------------------------------------------------------------------------------- 1 | export default function ({$axios, app}) { 2 | $axios.interceptors.response.use( 3 | function (response) { 4 | return response; 5 | }, 6 | function (error) { 7 | const code = parseInt(error.response && error.response.status); 8 | 9 | if ([401].includes(code)) { 10 | app.$auth.logout(); 11 | app.router.push('/login'); 12 | 13 | console.log('401 Unauthorized. Redirecting to Login Page.'); 14 | } 15 | 16 | return Promise.reject(error); 17 | } 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Issue** 11 | A clear and concise description of what the problem is. 12 | 13 | **Proposed Solution** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Dependencies** 17 | 1. Links to PRs 18 | 2. Links to Issues 19 | 3. Description of other dependencies 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential', 14 | ], 15 | // required to lint *.vue files 16 | plugins: [ 17 | 'vue' 18 | ], 19 | // add your custom rules here 20 | rules: {} 21 | } 22 | -------------------------------------------------------------------------------- /css/components/welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome h2 { 2 | font-size: 26px; 3 | font-weight: 900; 4 | text-align: center; 5 | color: #0a0525; 6 | margin-bottom: 1em; 7 | } 8 | 9 | .welcome .modal-card-head { 10 | padding: 30px; 11 | } 12 | 13 | .welcome .alert-icon-container img { 14 | position: absolute; 15 | z-index: 999; 16 | top: -32px; 17 | left: calc(50% - 40px); 18 | width: 80px; 19 | height: 80px; 20 | } 21 | 22 | .welcome p { 23 | font-size: 22px; 24 | font-weight: 500; 25 | line-height: 1.45; 26 | text-align: center; 27 | color: #8d8e8e; 28 | } 29 | 30 | .welcome .button { 31 | width: 220px; 32 | } 33 | -------------------------------------------------------------------------------- /middleware/intro.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | async function isRegistered(ctx) { 4 | // Check if user is registered 5 | const data = (await axios.get(`${ctx.env.DEVICE_HOST}:3000/v1/accounts/registered`)).data; 6 | if (data.registered === false) { 7 | // redirect unregistered user to /intro 8 | ctx.app.router.push('/intro'); 9 | } 10 | } 11 | 12 | export default async function (ctx) { 13 | 14 | // TODO - run registration check as middleware and cookie the user so we don't have to make 15 | // an HTTP request each time we refresh index. We can simply check for the cookie. 16 | 17 | // await isRegistered(ctx); 18 | } 19 | -------------------------------------------------------------------------------- /layouts/loading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /assets/fee-cheap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/fee-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/fee-fast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/fee-slow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icon-password.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/browser/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /test/browser/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /assets/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/Success.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /css/main.scss: -------------------------------------------------------------------------------- 1 | @import '@/css/colors.scss'; 2 | @import '@/css/general.scss'; 3 | 4 | @import '@/css/pages/autopilot.scss'; 5 | @import '@/css/pages/bitcoin.scss'; 6 | @import '@/css/pages/connections.scss'; 7 | @import '@/css/pages/dashboard.scss'; 8 | @import '@/css/pages/deposit.scss'; 9 | @import '@/css/pages/lightning.scss'; 10 | @import '@/css/pages/withdraw.scss'; 11 | 12 | @import '@/css/components/bitcoin.scss'; 13 | @import '@/css/components/channels.scss'; 14 | @import '@/css/components/modals.scss'; 15 | @import '@/css/components/slideout.scss'; 16 | @import '@/css/components/stats.scss'; 17 | @import '@/css/components/steps.scss'; 18 | @import '@/css/components/transactions.scss'; 19 | @import '@/css/components/unit-switch.scss'; 20 | @import '@/css/components/welcome.scss'; 21 | -------------------------------------------------------------------------------- /css/colors.scss: -------------------------------------------------------------------------------- 1 | /* Status Colors */ 2 | .offline { 3 | color: #f7bd00!important; 4 | } 5 | 6 | .syncing { 7 | color: #3bccfc!important; 8 | } 9 | 10 | .locked { 11 | color: #f0649e!important; 12 | } 13 | 14 | .synced { 15 | color: #2dcccd!important; 16 | } 17 | 18 | .status-circle { 19 | width: 10px; 20 | height: 10px; 21 | border-radius: 10px; 22 | display: inline-block; 23 | margin-left: 10px; 24 | } 25 | 26 | .status-syncing { 27 | background-color: #3bccfc; 28 | } 29 | 30 | .status-locked { 31 | background-color: #f0649e; 32 | } 33 | 34 | .status-synced { 35 | background-color: #2dcccd; 36 | } 37 | 38 | .status-offline { 39 | background-color: #f7bd00; 40 | } 41 | 42 | /* Text colors */ 43 | a.blue { 44 | color: #3bccfc; 45 | font-weight: 500; 46 | } 47 | -------------------------------------------------------------------------------- /assets/gray-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icon-info-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/Caution.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /assets/logo-casa-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /test/browser/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/ConfirmSave.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /css/components/stats.scss: -------------------------------------------------------------------------------- 1 | /* Bitcoin and Lightning statistics */ 2 | .stats { 3 | padding: 0 0.5em; 4 | } 5 | 6 | .stats-col { 7 | margin-top: 1em; 8 | } 9 | 10 | .stats-col * { 11 | font-weight: bold; 12 | letter-spacing: -0.2px; 13 | } 14 | 15 | .stats-col h1 { 16 | font-size: 24px; 17 | letter-spacing: -0.3px; 18 | color: #0a0525; 19 | } 20 | 21 | .stats-col h2 { 22 | font-size: 18px; 23 | color: #8d8e8e; 24 | } 25 | 26 | .stats-col h3 { 27 | font-size: 18px; 28 | color: #d7d8d9; 29 | } 30 | 31 | 32 | /* Tablet */ 33 | @media screen and (min-width: 769px) { 34 | .stats { 35 | display: flex; 36 | justify-content: space-between; 37 | padding: 2em 3em; 38 | } 39 | 40 | .stats-col { 41 | margin-top: 0; 42 | } 43 | } 44 | 45 | /* Desktop */ 46 | @media screen and (min-width: 1088px) { 47 | .stats-col h1 { 48 | font-size: 30px; 49 | } 50 | 51 | .stats-col h2 { 52 | font-size: 20px; 53 | } 54 | 55 | .stats-col h3 { 56 | font-size: 20px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/ConfirmCancel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/Welcome.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /helpers/units.js: -------------------------------------------------------------------------------- 1 | import {BigNumber} from 'bignumber.js'; 2 | 3 | // Never display numbers as exponents 4 | BigNumber.config({ EXPONENTIAL_AT: 1e+9 }); 5 | 6 | export function btcToSats(input) { 7 | const btc = new BigNumber(input); 8 | const sats = btc.multipliedBy(100000000); 9 | 10 | if(isNaN(sats)) { 11 | return 0; 12 | } 13 | 14 | return sats.toString(); 15 | } 16 | 17 | export function satsToBtc(input, decimals = 8) { 18 | const sats = new BigNumber(input); 19 | const btc = sats.dividedBy(100000000); 20 | 21 | if(isNaN(btc)) { 22 | return 0; 23 | } 24 | 25 | return btc.decimalPlaces(decimals).toString(); 26 | } 27 | 28 | export function formatSats(input) { 29 | const sats = new BigNumber(input); 30 | 31 | if(isNaN(sats)) { 32 | return 0; 33 | } 34 | 35 | return sats.toFormat(0); 36 | } 37 | 38 | export function toPrecision(input, decimals = 8) { 39 | const number = new BigNumber(input); 40 | 41 | if(isNaN(number)) { 42 | return 0; 43 | } 44 | 45 | return number.decimalPlaces(decimals).toString(); 46 | } 47 | -------------------------------------------------------------------------------- /Dockerfile.armhf: -------------------------------------------------------------------------------- 1 | # specify the node base image with your desired version 2 | FROM balenalib/armv7hf-node:8-stretch-run 3 | 4 | # need qemu to emulate arm architecture 5 | # can be downloaded here, $ docker run -v /usr/bin/qemu-arm-static:/usr/bin/qemu-arm-static --rm -ti arm32v7/debian:stretch-slim 6 | COPY ./qemu-arm-static /usr/bin/qemu-arm-static 7 | 8 | # install tools 9 | RUN apt-get update --no-install-recommends \ 10 | && apt-get install -y --no-install-recommends vim \ 11 | && apt-get install -y --no-install-recommends python \ 12 | && apt-get install -y --no-install-recommends build-essential g++ \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Create app directory 16 | WORKDIR /usr/src/app 17 | 18 | # Install app dependencies 19 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 20 | # where available (npm@5+) 21 | COPY package*.json ./ 22 | 23 | RUN CYPRESS_INSTALL_BINARY=0 yarn install 24 | 25 | # Bundle app source 26 | COPY . . 27 | 28 | RUN yarn run build 29 | 30 | ENV HOST 0.0.0.0 31 | 32 | EXPOSE 3000 33 | CMD [ "yarn", "start" ] 34 | -------------------------------------------------------------------------------- /assets/red-excl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 7 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/channel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2019 Casa, Inc. https://keys.casa/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/yellow-excl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 14 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/Bitcoin/Alerts/ConfirmRedownload.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /components/Bitcoin/Alerts/ConfirmSync.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /test/browser/integration/login.spec.js: -------------------------------------------------------------------------------- 1 | describe('Logging in to the node', function() { 2 | it('has login form', function() { 3 | cy.visit('/'); 4 | cy.get('.login-form input[type=password]').should('exist'); 5 | }); 6 | 7 | it('requires password', function() { 8 | cy.visit('/'); 9 | 10 | cy.get('.login-form form').trigger('submit'); 11 | cy.wait(1000); 12 | 13 | cy.get('.auth-error').should('be.visible'); 14 | }); 15 | 16 | it('denies invalid password', function() { 17 | cy.visit('/'); 18 | 19 | cy.get('.login-form input[type=password]').type('abc123'); // Invalid password. Minimum length is 12 characters. 20 | cy.get('.login-form form').trigger('submit'); 21 | cy.wait(1000); 22 | 23 | cy.get('.auth-error').should('be.visible'); 24 | }); 25 | 26 | it('allows valid password', function() { 27 | cy.visit('/'); 28 | 29 | cy.get('.login-form input[type=password]').type(Cypress.env('password')); // This password needs to be the real password for the node you're testing on 30 | cy.get('.login-form form').trigger('submit'); 31 | cy.wait(1000); 32 | 33 | cy.get('.card-header span').first().contains('Bitcoin').should('exist'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /assets/chevron-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/paper-plane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /css/pages/deposit.scss: -------------------------------------------------------------------------------- 1 | .deposit .modal-card-body .address.payment-request { 2 | max-width: 350px; 3 | word-break: break-all; 4 | font-size: 14px; 5 | margin-top: 0; 6 | } 7 | 8 | .deposit .modal-card-body .address { 9 | margin: 0 auto; 10 | display: block; 11 | max-width: 70vw; 12 | word-wrap: break-word; 13 | font-size: 1.2em; 14 | text-align: center; 15 | } 16 | 17 | .deposit .modal-card-foot button { 18 | width: 100%; 19 | height: 60px; 20 | font-size: 18px; 21 | font-weight: bold; 22 | } 23 | 24 | .deposit img { 25 | margin: 0 auto; 26 | } 27 | 28 | .deposit .modal-card-foot .button { 29 | width: 100%; 30 | height: 60px; 31 | font-size: 18px; 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* Tablet */ 37 | @media screen and (min-width: 769px) { 38 | .deposit .modal-card-body .address { 39 | max-width: 320px; 40 | margin-left: 0; 41 | margin-bottom: 1em; 42 | text-align: left; 43 | } 44 | 45 | .deposit .modal-card-body .address.is-inline-block { 46 | margin-left: 1em; 47 | } 48 | 49 | .deposit .modal-card-body .address-info { 50 | text-align: left; 51 | clear: both; 52 | } 53 | 54 | .deposit .modal-card-body .address-info-left { 55 | float: left; 56 | margin-bottom: 1em; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/Alert.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /pages/please-restart.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | 58 | -------------------------------------------------------------------------------- /components/Settings/UnitSwitch.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | -------------------------------------------------------------------------------- /components/Lightning/Modals/SendPayment/Send.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | -------------------------------------------------------------------------------- /test/interval-bus.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import IntervalBus from '../helpers/interval-bus'; 3 | import { intervalFunctionExists } from '../helpers/interval-bus'; 4 | 5 | function logTest() { 6 | console.log("Test!"); 7 | } 8 | 9 | test('Add a 10 second interval function', t => { 10 | t.is(intervalFunctionExists(logTest), false); 11 | IntervalBus.set(logTest, 10); 12 | t.not(intervalFunctionExists(logTest), false); 13 | }); 14 | 15 | test('Remove an interval function', t => { 16 | t.not(intervalFunctionExists(logTest), false); 17 | IntervalBus.clear(logTest); 18 | t.is(intervalFunctionExists(logTest), false); 19 | }); 20 | 21 | test('Change a 10 second interval function to 60', t => { 22 | t.is(intervalFunctionExists(logTest), false); 23 | IntervalBus.set(logTest, 10); 24 | t.is(intervalFunctionExists(logTest), '10'); 25 | IntervalBus.set(logTest, 60); 26 | t.is(intervalFunctionExists(logTest), '60'); 27 | }); 28 | 29 | test('Setting an invalid duration', t => { 30 | let exception = false; 31 | 32 | try { 33 | IntervalBus.set(logTest, 12345); 34 | } catch(error) { 35 | exception = true; 36 | } 37 | 38 | t.truthy(exception); 39 | }); 40 | 41 | test('Getting the best interval', t => { 42 | t.is(IntervalBus.bestInterval(0.1), '10'); 43 | t.is(IntervalBus.bestInterval(44), '60'); 44 | t.is(IntervalBus.bestInterval(90), '180'); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/interval-bus.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import IntervalBus from '../../helpers/interval-bus'; 3 | import { intervalFunctionExists } from '../../helpers/interval-bus'; 4 | 5 | function logTest() { 6 | console.log("Test!"); 7 | } 8 | 9 | test('Add a 10 second interval function', t => { 10 | t.is(intervalFunctionExists(logTest), false); 11 | IntervalBus.set(logTest, 10); 12 | t.not(intervalFunctionExists(logTest), false); 13 | }); 14 | 15 | test('Remove an interval function', t => { 16 | t.not(intervalFunctionExists(logTest), false); 17 | IntervalBus.clear(logTest); 18 | t.is(intervalFunctionExists(logTest), false); 19 | }); 20 | 21 | test('Change a 10 second interval function to 60', t => { 22 | t.is(intervalFunctionExists(logTest), false); 23 | IntervalBus.set(logTest, 10); 24 | t.is(intervalFunctionExists(logTest), '10'); 25 | IntervalBus.set(logTest, 60); 26 | t.is(intervalFunctionExists(logTest), '60'); 27 | }); 28 | 29 | test('Setting an invalid duration', t => { 30 | let exception = false; 31 | 32 | try { 33 | IntervalBus.set(logTest, 12345); 34 | } catch(error) { 35 | exception = true; 36 | } 37 | 38 | t.truthy(exception); 39 | }); 40 | 41 | test('Getting the best interval', t => { 42 | t.is(IntervalBus.bestInterval(0.1), '10'); 43 | t.is(IntervalBus.bestInterval(44), '60'); 44 | t.is(IntervalBus.bestInterval(90), '180'); 45 | }); 46 | -------------------------------------------------------------------------------- /assets/bitcoin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /helpers/api.js: -------------------------------------------------------------------------------- 1 | // An object to store the ressponse time of completed API requests 2 | const responseTime = {}; 3 | 4 | // An object to store pending API requests 5 | const responsePending = {}; 6 | 7 | // Helper methods for making API requests 8 | const API = { 9 | async get(axios, url, query = {}) { 10 | let response; 11 | 12 | if(responsePending[url] === undefined || responsePending[url] === false) { 13 | responsePending[url] = true; 14 | 15 | try { 16 | const startTime = new Date(); 17 | response = (await axios.get(url, {params: query})).data; 18 | const endTime = new Date(); 19 | 20 | responseTime[url] = (endTime.getTime() - startTime.getTime()) / 1000; 21 | } catch(error) { 22 | // TODO: Trigger the EventBus to display user friendly error messages 23 | 24 | // Only display error messages in the browser console 25 | if(process.browser) { 26 | console.error(error); 27 | } 28 | 29 | response = false; 30 | } finally { 31 | responsePending[url] = false; 32 | } 33 | } else { 34 | console.warn(`Warning: A request to ${url} is already in progress. Duplicate connection skipped.`); 35 | } 36 | 37 | return response; 38 | }, 39 | 40 | // Return the response time if this URL has already been fetched 41 | responseTime(url) { 42 | let duration = -1; 43 | 44 | if(responseTime[url] !== undefined) { 45 | duration = responseTime[url]; 46 | } 47 | 48 | return duration; 49 | } 50 | } 51 | 52 | export default API; 53 | -------------------------------------------------------------------------------- /components/Settings/Modals/TorAddress.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 51 | -------------------------------------------------------------------------------- /pages/shutdown.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 46 | 47 | 77 | -------------------------------------------------------------------------------- /css/pages/dashboard.scss: -------------------------------------------------------------------------------- 1 | .launcher { 2 | border-radius: 1em; 3 | } 4 | 5 | .launcher-logo { 6 | margin-bottom: 0.65em; 7 | } 8 | 9 | .card-footer-title { 10 | -webkit-box-align: center; 11 | -ms-flex-align: center; 12 | align-items: center; 13 | color: #363636; 14 | display: -webkit-box; 15 | display: -ms-flexbox; 16 | display: flex; 17 | -webkit-box-flex: 1; 18 | -ms-flex-positive: 1; 19 | flex-grow: 1; 20 | font-weight: 700; 21 | padding: 0.75rem; 22 | } 23 | 24 | .card h2 { 25 | font-size: 26px; 26 | font-weight: bold; 27 | color: #0a0525; 28 | } 29 | 30 | .launcher .card { 31 | border-radius: 8px; 32 | background-color: #ffffff; 33 | box-shadow: 0 4px 13px 2px rgba(0, 0, 0, 0.05); 34 | } 35 | 36 | .launcher .card-content { 37 | padding-top: 0.5em; 38 | min-height: 160px; 39 | } 40 | 41 | .launcher .card-header img { 42 | height: 40px; 43 | width: 40px; 44 | margin-right: .30em; 45 | } 46 | 47 | .launcher .card-footer { 48 | cursor: pointer; 49 | min-height: 60px; 50 | } 51 | 52 | .launcher .card h3 { 53 | margin-top: 0.25em; 54 | margin-bottom: 0.25em; 55 | font-size: 22px; 56 | } 57 | 58 | .launcher .card h3 { 59 | font-size: 22px; 60 | } 61 | 62 | .launcher .card h3 { 63 | font-size: 22px; 64 | } 65 | 66 | .launcher .card .pending h3 { 67 | color: #3bccfc; 68 | } 69 | 70 | .launcher .card .pending h3 { 71 | color: #3bccfc; 72 | } 73 | 74 | .launcher .card .muted { 75 | color: #d2d4d6; 76 | font-size: 20px; 77 | font-weight: bold; 78 | } 79 | 80 | /* Desktop */ 81 | @media screen and (min-width: 1088px) { 82 | .launcher-logo { 83 | margin-top: 2em; 84 | margin-bottom: 1.25em; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /css/components/unit-switch.scss: -------------------------------------------------------------------------------- 1 | .unit-switch { 2 | position: absolute; 3 | right: 1em; 4 | top: 1.05em; 5 | } 6 | 7 | /* Toggle switch buttons */ 8 | .toggle { 9 | position: relative; 10 | display: inline-block; 11 | width: 128px; 12 | height: 35px; 13 | max-width: 100%; 14 | font-size: 14px; 15 | font-weight: 900; 16 | } 17 | 18 | .toggle input { 19 | opacity: 0; 20 | width: 0; 21 | height: 0; 22 | } 23 | 24 | .toggle-slider { 25 | position: absolute; 26 | cursor: pointer; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | background-color: #fff; 32 | border: 3px solid #e1e2e3; 33 | transition: 0.5s; 34 | border-radius: 20px; 35 | } 36 | 37 | .toggle-slider:before { 38 | position: absolute; 39 | content: ""; 40 | height: 100%; 41 | width: 50%; 42 | left: 0; 43 | top: -3px; 44 | left: -1px; 45 | background-color: #0a0525; 46 | transition: 0.5s; 47 | border: 3px solid #0a0525; 48 | box-sizing: content-box; 49 | box-shadow: 0 7px 14px 0 rgba(74, 74, 74, 0.09); 50 | border-radius: 20px; 51 | } 52 | 53 | .toggle input:checked + .toggle-slider:before { 54 | transform: translateX(calc(100% - 7px)); 55 | } 56 | 57 | .toggle-options { 58 | display: flex; 59 | text-decoration: none; 60 | } 61 | 62 | .toggle-option { 63 | position: relative; 64 | z-index: 1; 65 | cursor: pointer; 66 | top: -1em; 67 | transition: 0.5s; 68 | transition-property: font-weight, color; 69 | width: 50%; 70 | text-align: center; 71 | font-weight: bold; 72 | padding-left: 0.5em; 73 | } 74 | 75 | .toggle-option.two { 76 | padding-right: 0.5em; 77 | } 78 | 79 | .toggle-option.active { 80 | transition-delay: 0.2s; 81 | color: #fff; 82 | } 83 | -------------------------------------------------------------------------------- /components/Lightning/Modals/PaymentRequests/Request.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 64 | -------------------------------------------------------------------------------- /assets/green-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 14 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/blue-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 14 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Casa-Node", 3 | "version": "1.19.0", 4 | "description": "Your Casa Home Node", 5 | "author": "Casa Inc.", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate", 12 | "test:unit": "ava", 13 | "test:browser": "cypress open", 14 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 15 | "precommit": "npm run lint", 16 | "deploy": "s3-deploy './dist/**' --cwd './dist/' --region us-east-1 --bucket spacefleet.com", 17 | "ship": "nuxt build && s3-deploy './dist/**' --cwd './dist/' --region us-east-1 --bucket spacefleet.com" 18 | }, 19 | "dependencies": { 20 | "@fortawesome/free-solid-svg-icons": "^5.2.0", 21 | "@nuxtjs/auth": "^4.5.2", 22 | "@nuxtjs/axios": "^5.3.3", 23 | "@nuxtjs/dotenv": "^1.1.1", 24 | "bignumber.js": "^8.0.1", 25 | "node-sass": "^4.11.0", 26 | "nuxt": "^1.0.0", 27 | "nuxt-buefy": "^0.1.0", 28 | "nuxt-env": "^0.0.4", 29 | "nuxt-fontawesome": "^0.3.0", 30 | "sass-loader": "^7.1.0", 31 | "vee-validate": "^2.1.1", 32 | "vue-clipboard2": "^0.2.1", 33 | "vue-moment": "^4.0.0", 34 | "vue-numeric": "^2.3.0", 35 | "vue-offline": "^1.0.204", 36 | "vue-qriously": "^1.1.1", 37 | "vue2-slideout-panel": "^0.11.0" 38 | }, 39 | "devDependencies": { 40 | "ava": "^1.0.1", 41 | "babel-eslint": "^8.2.1", 42 | "cross-env": "^5.0.1", 43 | "cypress": "^3.3.1", 44 | "eslint": "^5.0.1", 45 | "eslint-loader": "^2.0.0", 46 | "eslint-plugin-vue": "^4.0.0", 47 | "jsdom": "^13.1.0", 48 | "s3-deploy": "^1.1.1" 49 | }, 50 | "ava": { 51 | "require": [ 52 | "babel-register" 53 | ], 54 | "files": [ 55 | "test/unit/**/*" 56 | ] 57 | }, 58 | "babel": { 59 | "presets": [ 60 | "env" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugins/filters.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueNumeric from 'vue-numeric' 3 | import {satsToBtc, btcToSats, formatSats} from '@/helpers/units'; 4 | import BitcoinData from '@/data/bitcoin'; 5 | import SystemData from '@/data/system'; 6 | 7 | // Convert Satoshis to Bitcoin 8 | Vue.filter('btc', value => satsToBtc(value)); 9 | 10 | // Convert Bitcoin to Satoshis 11 | Vue.filter('sats', value => btcToSats(value)); 12 | 13 | // Convert Satoshis to USD 14 | Vue.filter('usd', value => { 15 | // If the value passed is not a number, output it as is 16 | if(isNaN(parseInt(value))) { 17 | return value; 18 | } else { 19 | return '$' + (satsToBtc(value) * BitcoinData.price).toFixed(2); 20 | } 21 | }); 22 | 23 | // Display value in whatever unit that user has selected 24 | Vue.filter('inUnits', value => { 25 | if(SystemData.displayUnit === 'btc') { 26 | return satsToBtc(value); 27 | } else if(SystemData.displayUnit === 'sats') { 28 | return formatSats(value); 29 | } 30 | 31 | // Something must have gone wrong? 32 | return 0; 33 | }); 34 | 35 | // Display the currently selected unit as a suffix 36 | Vue.filter('withSuffix', value => { 37 | if(SystemData.displayUnit === 'btc') { 38 | return value + ' BTC'; 39 | } else if(SystemData.displayUnit === 'sats') { 40 | return value + ' sats'; 41 | } 42 | 43 | return value; 44 | }); 45 | 46 | /** 47 | * Vue filter to convert the given value to percent. 48 | * 49 | * @param {String} value The value string. 50 | * @param {Number} decimals The number of decimal places. 51 | */ 52 | Vue.filter('percentage', function(value, decimals) { 53 | if(!value) { 54 | value = 0; 55 | } 56 | 57 | if(!decimals) { 58 | decimals = 0; 59 | } 60 | 61 | value = value * 100; 62 | value = Math.floor(value * Math.pow(10, decimals)) / Math.pow(10, decimals); 63 | value = value + '%'; 64 | return value; 65 | }); 66 | 67 | Vue.use(VueNumeric); 68 | -------------------------------------------------------------------------------- /components/Settings/Alerts/ConfirmFactoryReset.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 51 | -------------------------------------------------------------------------------- /plugins/vee-validate.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VeeValidate, { Validator } from 'vee-validate'; 3 | 4 | // Custom error messages for validation 5 | const dictionary = { 6 | en: { 7 | attributes: { 8 | chansize: 'Minimum Channel Size', 9 | nickname: 'Node Nickname', 10 | nodeColor: 'Node Color', 11 | maxChanSize: 'Value per Channel', 12 | maxChannels: 'Number of Channels', 13 | peerName: 'Peer Name', 14 | channelPurpose: 'Channel Purpose', 15 | connectionCode: 'Connection Code', 16 | pubKey: 'Public Key', 17 | fundingAmount: 'Channel Funding', 18 | currentPassword: 'Current Password', 19 | newPassword: 'New Password', 20 | confirmPassword: 'Confirm Password', 21 | }, 22 | 23 | custom: { 24 | chansize: { 25 | decimal: 'Minimum Channel Size must be a number', 26 | integer: 'Minimum Channel Size must be an integer', 27 | }, 28 | 29 | nickname: { 30 | max: 'Node Nickname must be less than 32 characters', 31 | }, 32 | 33 | nodeColor: { 34 | regex: 'Node Color must be a six digit hex code or empty', 35 | }, 36 | 37 | maxChanSize: { 38 | decimal: 'Value per channel must be a number', 39 | integer: 'Value per channel must be an integer', 40 | }, 41 | 42 | confirmPassword: { 43 | confirmed: 'Confirm Password does not match New Password', 44 | }, 45 | } 46 | } 47 | }; 48 | 49 | Vue.use(VeeValidate); 50 | Validator.localize(dictionary); 51 | 52 | // Custom validation functions 53 | Validator.extend('max_bytes', { 54 | getMessage: (field, params) => { 55 | return `${field} must be less than ${params[0]} bytes. This can happen if your nickname uses emojis or other special characters`; 56 | }, 57 | 58 | validate: (input, params) => { 59 | const blob = new Blob([input]); 60 | const max = parseInt(params[0]); 61 | 62 | if(blob.size > max) { 63 | return false; 64 | } 65 | 66 | return true; 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /components/Lightning/Modals/PaymentRequests/ConfirmInvoice.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 68 | -------------------------------------------------------------------------------- /assets/casa-c-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | casa-c-logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /css/pages/lightning.scss: -------------------------------------------------------------------------------- 1 | .lightning-menu { 2 | .transaction-list-wrap { 3 | height: calc(100vh - 395px); 4 | overflow: auto; 5 | } 6 | 7 | .transactions { 8 | .stats { 9 | padding: 0; 10 | } 11 | 12 | .stats-col { 13 | display: flex; 14 | justify-content: space-between; 15 | margin: 0 0.25em; 16 | 17 | h1, h2 { 18 | font-size: 17px; 19 | } 20 | } 21 | } 22 | 23 | .tx-actions { 24 | display: block; 25 | 26 | a.button { 27 | margin: 0; 28 | margin-top: 0.75em; 29 | width: 100%; 30 | } 31 | } 32 | 33 | .custom-color { 34 | .control { 35 | text-align: right; 36 | position: relative; 37 | } 38 | 39 | input { 40 | padding-left: 4em !important; 41 | } 42 | 43 | .color-output { 44 | position: absolute; 45 | top: 1px; 46 | bottom: 1px; 47 | width: 3em; 48 | background-color: #8865DF; 49 | border-top-left-radius: 4px; 50 | border-bottom-left-radius: 4px; 51 | } 52 | } 53 | 54 | /* Tablet */ 55 | @media screen and (min-width: 769px) { 56 | .transaction-list-wrap { 57 | height: calc(100vh - 410px); 58 | } 59 | 60 | .transactions { 61 | .stats { 62 | padding: 0; 63 | } 64 | 65 | .stats-col { 66 | display: block; 67 | } 68 | } 69 | 70 | .custom-color { 71 | input { 72 | width: calc(100% - 3em) !important; 73 | } 74 | 75 | .color-output { 76 | left: calc(3em + 1px); 77 | } 78 | } 79 | } 80 | 81 | /* Desktop */ 82 | @media screen and (min-width: 1088px) { 83 | .transaction-list-wrap { 84 | height: calc(100vh - 440px); 85 | } 86 | 87 | .transactions { 88 | .stats-col { 89 | margin-bottom: 2em; 90 | 91 | h1 { 92 | font-size: 25px; 93 | } 94 | 95 | h2 { 96 | font-size: 20px; 97 | } 98 | } 99 | } 100 | 101 | .tx-actions { 102 | display: flex; 103 | 104 | a.button { 105 | margin: 0 1em; 106 | width: auto; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /components/Bitcoin/Modals/Withdraw/ConfirmSend.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 41 | 68 | -------------------------------------------------------------------------------- /components/Settings/Alerts/UpdateNotice.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 95 | -------------------------------------------------------------------------------- /css/components/bitcoin.scss: -------------------------------------------------------------------------------- 1 | .btc-calc h2 { 2 | font-size: 20px; 3 | font-weight: bold; 4 | color: #0a0525; 5 | } 6 | 7 | .btc-calc p.description { 8 | font-size: 18px; 9 | font-weight: 500; 10 | line-height: 1.33; 11 | color: #8d8e8e; 12 | } 13 | 14 | .btc-calc .field.is-grouped { 15 | margin-top: 2em; 16 | } 17 | 18 | .btc-calc input { 19 | font-size: 40px; 20 | font-weight: 500; 21 | text-align: center; 22 | height: 90px; 23 | 24 | &.medium-text { 25 | font-size: 30px; 26 | } 27 | 28 | &.small-text { 29 | font-size: 20px; 30 | } 31 | } 32 | 33 | .btc-calc .hide-input-box input { 34 | border: none; 35 | background: transparent; 36 | box-shadow: none; 37 | padding-bottom: 10px; 38 | } 39 | 40 | .btc-calc p.total { 41 | font-size: 40px; 42 | } 43 | 44 | .btc-calc p.operator { 45 | font-size: 40px; 46 | color: #d7d8d9; 47 | margin-top: 10px; 48 | text-align: center; 49 | } 50 | 51 | .btc-calc p.help { 52 | text-align: center; 53 | font-weight: bold; 54 | text-transform: uppercase; 55 | } 56 | 57 | .button.is-gray, 58 | .address-info .button { 59 | float: right; 60 | border-radius: 4px; 61 | background-color: #eeeeee; 62 | border-color: transparent; 63 | color: #8d8e8e; 64 | font-weight: bold; 65 | width: 180px; 66 | height: 60px; 67 | } 68 | 69 | .address-info p { 70 | margin: 0 0 2em 0 !important; 71 | font-weight: normal !important; 72 | font-size: 1.2em !important; 73 | } 74 | 75 | 76 | .bitcoin-in-wallet { 77 | font-size: 40px; 78 | font-weight: bold; 79 | text-align: center; 80 | } 81 | 82 | .bitcoin-balance.is-grouped > div:nth-child(1) { 83 | padding-right:1em; 84 | } 85 | 86 | .bitcoin-balance.is-grouped > div:nth-child(2) p { 87 | margin-top: 1em; 88 | padding-right: 1em; 89 | text-align: center; 90 | } 91 | 92 | .plain-text-address { 93 | display: inline-block; 94 | float: left; 95 | } 96 | 97 | .address-copy { 98 | display: inline-block; 99 | margin: 0.5em 0 0 0.5em; 100 | } 101 | 102 | /* Tablet */ 103 | @media screen and (min-width: 769px) { 104 | .bitcoin-balance.is-grouped > div:nth-child(1) { 105 | border-right: gray 1px solid; 106 | width: 225px; 107 | } 108 | 109 | .bitcoin-balance.is-grouped > div:nth-child(2) p { 110 | text-align: left; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Bitcoin/Modals/Withdraw/Confirm.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 86 | -------------------------------------------------------------------------------- /components/Bitcoin/Modals/ConnectionDetails.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 84 | -------------------------------------------------------------------------------- /helpers/redirects.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import API from '@/helpers/api'; 3 | 4 | // Helper function to check if the node is currently loading 5 | // If the node is loading, redirect to the loading screen 6 | export async function checkLoading(context) { 7 | // Check to see if the node is still booting up 8 | const loading = await API.get(axios, `${context.$env.API_MANAGER}/v1/telemetry/boot`); 9 | 10 | if(typeof sessionStorage !== 'undefined') { 11 | const session = sessionStorage.getItem('loading'); 12 | 13 | // This session data is used to bypass the loading page in case the node is unable to start 14 | if(session === 'ignored') { 15 | return false; 16 | } 17 | } 18 | 19 | // If there is a network failure, an exception will be thrown and loading will return false 20 | if(loading === false || (loading && parseInt(loading.percent) !== 100)) { 21 | context.$router.push('/loading'); 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | // Helper function to check if the node already has a user account 29 | // If no user account exists, redirect to the intro 30 | export async function checkAccount(context) { 31 | // Check to see if the user is registered. If the user is not registered, redirect them to the intro page. 32 | const registeredData = await API.get(axios, `${context.$env.API_MANAGER}/v1/accounts/registered`); 33 | 34 | if(registeredData && registeredData.registered === false) { 35 | 36 | // Check the manager's version. If The manager is running on less than 1.15.0, we will instruct the user to restart. 37 | // The user should have the latest version of the manager downloaded. Restarting will apply that new version. 38 | const pingData = await API.get(axios, `${context.$env.API_MANAGER}/ping`); 39 | 40 | if(pingData && pingData.version) { 41 | 42 | try { 43 | const semverParts = pingData.version.split('-')[1].split('.'); 44 | 45 | if(semverParts[0] === '1' && semverParts[1] <= 14) { 46 | context.$router.push('/please-restart'); 47 | return true; 48 | } 49 | 50 | // Ignore any parsing errors 51 | } catch (e) {} 52 | } 53 | 54 | context.$router.push('/intro'); 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | 61 | // Helper function to check if the user is logged in 62 | export async function checkLoggedIn(context) { 63 | // Make a JWT authenticated call to check if we're logged in 64 | const serial = await API.get(context.$axios, `${context.$env.API_MANAGER}/v1/telemetry/serial`); 65 | 66 | if(serial === false) { 67 | context.$router.push('/login'); 68 | return true; 69 | } 70 | 71 | return false; 72 | } 73 | -------------------------------------------------------------------------------- /components/Bitcoin/Modals/Deposit/Deposit.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 91 | -------------------------------------------------------------------------------- /css/pages/autopilot.scss: -------------------------------------------------------------------------------- 1 | .autopilot-toggle { 2 | padding-left: 0.5em; 3 | padding-right: 1em; 4 | } 5 | 6 | .autopilot-toggle .field { 7 | display: inline-block; 8 | line-height: 3em; 9 | vertical-align: middle; 10 | padding: 0 0.5em; 11 | 12 | &:first-child { 13 | padding-top: 1em; 14 | } 15 | } 16 | 17 | .autopilot-link { 18 | max-width: 440px; 19 | margin: 3em auto 0; 20 | } 21 | 22 | .autopilot-link a { 23 | color: #3bccfc; 24 | } 25 | 26 | .autopilot-settings .modal-card-head { 27 | font-size: 26px; 28 | font-weight: bold; 29 | color: #0a0525; 30 | } 31 | 32 | .autopilot-settings .description { 33 | padding: 0 1em 0; 34 | color: #8d8e8e; 35 | } 36 | 37 | .autopilot-settings .stats { 38 | padding-top: 0; 39 | padding-bottom: 1em; 40 | } 41 | 42 | .autopilot-settings .menu-navigation { 43 | padding: 1em; 44 | } 45 | 46 | .autopilot-settings .tx-list { 47 | padding: 1em; 48 | margin: 0; 49 | } 50 | 51 | .autopilot-settings .tx-row { 52 | display: block; 53 | } 54 | 55 | .autopilot-settings .tx-col-2 { 56 | margin: 0; 57 | } 58 | 59 | .autopilot-settings .tx-col-2 h2, 60 | .autopilot-settings .tx-col-2 h3 { 61 | max-width: 80%; 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | } 65 | 66 | .autopilot-settings .transaction-settings .section-title { 67 | text-transform: uppercase; 68 | color: #c3c5c7; 69 | letter-spacing: 1px; 70 | font-size: 13px; 71 | padding-left: 1rem; 72 | font-weight: bold; 73 | padding-bottom: 0.5rem; 74 | } 75 | 76 | .autopilot-settings .inactive-wrap { 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | min-height: 50vh; 81 | } 82 | 83 | .autopilot-settings .inactive { 84 | text-align: center; 85 | color: #c3c5c7; 86 | font-size: 26px; 87 | } 88 | 89 | .autopilot-settings .inactive h2 { 90 | margin-top: 1em; 91 | font-weight: bold; 92 | } 93 | 94 | .autopilot-settings .field.is-grouped { 95 | display: block; 96 | } 97 | 98 | /* Tablet */ 99 | @media screen and (min-width: 769px) { 100 | .autopilot-toggle { 101 | float: right; 102 | } 103 | 104 | .autopilot-settings .description { 105 | padding: 1em 3em 0; 106 | } 107 | 108 | .autopilot-settings .transaction-settings .section-title { 109 | padding-left: 3em; 110 | } 111 | 112 | .autopilot-settings .tx-list { 113 | padding: 1em 3em; 114 | } 115 | 116 | .autopilot-settings .tx-row { 117 | display: flex; 118 | } 119 | 120 | .autopilot-settings .tx-col-3 { 121 | margin-left: -3em; 122 | } 123 | 124 | .autopilot-settings .menu-navigation { 125 | padding: 1em 3em; 126 | } 127 | 128 | .autopilot-settings .field.is-grouped { 129 | display: flex; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /assets/qr-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /components/Lightning/Alerts/Unlock.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 79 | -------------------------------------------------------------------------------- /components/Settings/Alerts/ConfirmUpdate.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 82 | -------------------------------------------------------------------------------- /css/components/transactions.scss: -------------------------------------------------------------------------------- 1 | /* Transactions */ 2 | .transactions .stats { 3 | justify-content: flex-start; 4 | padding: 0 0 2rem 0; 5 | } 6 | 7 | .transactions .stats-col { 8 | flex-grow: 1; 9 | margin: 0 1rem; 10 | } 11 | 12 | .transactions .stats-col h1 { 13 | font-size: 25px; 14 | } 15 | 16 | .transactions .stats-col h2 { 17 | font-size: 20px; 18 | } 19 | 20 | .tx-actions { 21 | display: flex; 22 | justify-content: space-between; 23 | } 24 | 25 | .tx-actions .button { 26 | flex-grow: 1; 27 | height: 48px; 28 | border-radius: 4px; 29 | box-shadow: 0 3px 5px 0 #0000000c; 30 | background-color: #ffffff; 31 | border: solid 1px #d7d8d9; 32 | color: #3e3b53; 33 | font-weight: bold; 34 | margin: 0 1em; 35 | } 36 | 37 | .tx-actions .button span { 38 | font-size: 17px; 39 | } 40 | 41 | .tx-actions h3 { 42 | color: #0a0525; 43 | font-size: 18px; 44 | font-weight: bold; 45 | margin-bottom: .5em; 46 | } 47 | 48 | .tx-list { 49 | margin: 0 1rem; 50 | } 51 | 52 | .tx-list > h3 { 53 | font-size: 13px; 54 | font-weight: bold; 55 | letter-spacing: 1px; 56 | color: #c3c5c7; 57 | opacity: 0.87; 58 | text-transform: uppercase; 59 | } 60 | 61 | .tx-row { 62 | display: flex; 63 | justify-content: space-between; 64 | } 65 | 66 | .tx-item a { 67 | color: #4a4a4a; 68 | } 69 | 70 | .transactions li.tx-item { 71 | cursor: pointer; 72 | } 73 | 74 | .transactions li.tx-item:hover * { 75 | color: #3bccfc; 76 | } 77 | 78 | .tx-col-1 { 79 | flex: 0 0 auto; 80 | } 81 | 82 | .tx-col-2 { 83 | flex: 1 1 auto; 84 | margin-left: 2rem; 85 | 86 | h2 { 87 | font-weight: bold; 88 | } 89 | 90 | h3 { 91 | overflow-wrap: break-word; 92 | } 93 | } 94 | 95 | .tx-col-3 { 96 | flex: 0 0 auto; 97 | text-align: right; 98 | } 99 | 100 | .tx-info { 101 | margin-top: 1em; 102 | 103 | h2 { 104 | font-weight: bold; 105 | } 106 | 107 | h3 { 108 | overflow-wrap: break-word; 109 | } 110 | } 111 | 112 | .date-badge { 113 | width: 50px; 114 | height: 50px; 115 | border-radius: 12.5px; 116 | background-color: #ffffff; 117 | border: solid 1.3px #d7d8d9; 118 | display: flex; 119 | flex-direction: column; 120 | justify-content: center; 121 | text-align: center; 122 | } 123 | 124 | .date-badge span { 125 | font-size: 10px; 126 | font-weight: bold; 127 | text-transform: uppercase; 128 | } 129 | 130 | .date-badge .month { 131 | font-size: 10px; 132 | font-weight: 900; 133 | text-align: center; 134 | color: #8d8e8e; 135 | margin-top:2px; 136 | } 137 | 138 | .date-badge .day { 139 | font-size: 20px; 140 | font-weight: 500; 141 | text-align: center; 142 | color: #0a0525; 143 | } 144 | 145 | 146 | /* Tablet */ 147 | @media screen and (min-width: 769px) { 148 | .transactions .stats-col:first-child { 149 | margin-right: -1rem; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pages/loading-password.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 72 | 73 | 113 | -------------------------------------------------------------------------------- /pages/loading.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 81 | 82 | 122 | -------------------------------------------------------------------------------- /components/Lightning/Modals/ConnectionCode.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 88 | -------------------------------------------------------------------------------- /components/Settings/Alerts/ConfirmShutdown.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 95 | -------------------------------------------------------------------------------- /components/Lightning/Modals/ConnectionDetails.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 98 | -------------------------------------------------------------------------------- /assets/casa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/browser/integration/btc-transaction.spec.js: -------------------------------------------------------------------------------- 1 | describe('Making Bitcoin transactions', function() { 2 | beforeEach(function() { 3 | cy.server(); 4 | cy.route('POST', '/v1/accounts/login').as('login'); 5 | cy.route('GET', '/v1/bitcoind/info/sync').as('btc-sync'); 6 | cy.route('GET', '/v1/lnd/info/sync').as('lnd-sync'); 7 | cy.route('GET', '/v1/lnd/transaction').as('transactions'); 8 | 9 | // Login 10 | cy.visit('/'); 11 | 12 | // TODO: Figure out how to log in by directly sending data to the API 13 | cy.get('.login-form input[type=password]').type(Cypress.env('password')); 14 | cy.get('.login-form form').trigger('submit'); 15 | 16 | // Wait until we are logged in and synced 17 | cy.wait('@login').its('status').should('be', 200); 18 | cy.wait('@btc-sync').then((xhr) => { 19 | assert.equal(xhr.response.body.percent, '1.0000'); 20 | }); 21 | 22 | cy.wait('@lnd-sync').then((xhr) => { 23 | assert.equal(xhr.response.body.percent, '1.0000'); 24 | }); 25 | 26 | cy.get('.card .card-footer-title').contains('Transactions').click(); 27 | cy.wait('@transactions'); 28 | }); 29 | 30 | it('generates QR code', function() { 31 | // Open deposit modal 32 | cy.get('.tx-actions span').contains('Deposit').click(); 33 | 34 | // Check for QR code 35 | cy.get('.deposit img').should('be.visible'); 36 | }); 37 | 38 | it('requires address and amount', function() { 39 | // Open withdraw modal 40 | cy.get('.tx-actions span').contains('Withdraw').click(); 41 | 42 | cy.get('.toast.is-danger').should('not.exist'); 43 | 44 | // Try to submit without any information 45 | cy.get('form.withdraw .button.is-casa').click(); 46 | cy.get('.toast.is-danger').should('exist'); 47 | }); 48 | 49 | it('displays confirmation', function() { 50 | cy.route('GET', '/v1/lnd/transaction/estimateFee?*').as('estimates'); 51 | 52 | // Open withdraw modal 53 | cy.get('.tx-actions span').contains('Withdraw').click(); 54 | 55 | // Use sample data from cypress environment file 56 | cy.get('.field input').first().type(Cypress.env('btc_address')); 57 | 58 | // TODO: This test depends on 'sats' being selected as the display unit 59 | cy.get('.withdrawal-amount input').first().type('100000'); 60 | 61 | // Select cheapest withdrawal fee as it's less likely to change dramatically 62 | cy.get('.fee-option').last().click(); 63 | cy.wait('@estimates'); 64 | 65 | cy.get('form.withdraw .button.is-casa').click(); 66 | cy.get('.modal-card-title span').contains('Review Bitcoin Withdrawal').should('exist'); 67 | }); 68 | 69 | it('calculates maximum amount to send', function() { 70 | cy.route('GET', '/v1/lnd/transaction/estimateFee?*').as('estimates'); 71 | 72 | // Open withdraw modal 73 | cy.get('.tx-actions span').contains('Withdraw').click(); 74 | 75 | // Use sample data from cypress environment file 76 | cy.get('.field input').first().type(Cypress.env('btc_address')); 77 | cy.get('.send-max').click(); 78 | 79 | cy.wait('@estimates'); 80 | 81 | cy.get('.withdrawal-amount input').first().should(($input) => { 82 | const value = $input.val() 83 | 84 | expect(value).to.exist; 85 | 86 | // TODO: We should use mock data for the estimate and total amount of BTC so this test can be more accurate 87 | expect(value).to.be.above(1000); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /css/pages/connections.scss: -------------------------------------------------------------------------------- 1 | .connections { 2 | h3 { 3 | font-size: 23px; 4 | font-weight: bold; 5 | margin-top: 0.5em; 6 | } 7 | 8 | h4 { 9 | font-size: 20px; 10 | font-weight: bold; 11 | margin-bottom: 0.75em; 12 | 13 | img { 14 | height: 1.5em; 15 | vertical-align: middle; 16 | margin-right: 0.25em; 17 | } 18 | } 19 | 20 | hr { 21 | margin-top: 2.5em !important; 22 | margin-bottom: 2.5em !important; 23 | } 24 | 25 | p { 26 | color: #8d8e8e; 27 | } 28 | 29 | .links { 30 | a { 31 | margin-right: 1.5em; 32 | } 33 | } 34 | 35 | .column { 36 | strong { 37 | vertical-align: top; 38 | line-height: 2em; 39 | } 40 | 41 | .field { 42 | vertical-align: top; 43 | margin-left: 1em; 44 | } 45 | } 46 | 47 | .switch { 48 | display: inline-block; 49 | } 50 | 51 | .tor-description { 52 | margin: 1em 0; 53 | border-radius: 4px; 54 | border: 2px solid #f4f4f4; 55 | align-items: center; 56 | justify-content: center; 57 | 58 | .column { 59 | padding: 0 0.5em 1em 0.5em; 60 | text-align: center; 61 | } 62 | 63 | .column.is-2 { 64 | padding-top: 2em; 65 | } 66 | 67 | img { 68 | max-height: 46px; 69 | } 70 | } 71 | } 72 | 73 | .connection-details section.modal-card-body { 74 | padding-bottom: 0; 75 | } 76 | 77 | .connection-details h2 img { 78 | vertical-align: middle; 79 | margin-top: -4px; 80 | margin-right: 0.5em; 81 | } 82 | 83 | .connection-details h2.is-danger { 84 | color: #f0649e !important; 85 | text-align: left; 86 | line-height: 24px; 87 | font-size: 20px; 88 | } 89 | 90 | .connection-details p.is-danger { 91 | font-size: 1.2em !important; 92 | text-align: left; 93 | font-weight: normal !important; 94 | color: #f0649e !important; 95 | } 96 | 97 | .connection-details .discovery-problem p { 98 | text-align: left; 99 | font-weight: normal !important; 100 | font-size: 1.2em !important; 101 | color: #636363; 102 | } 103 | 104 | .connection-details .modal-card-body .address { 105 | display: inline-block; 106 | max-width: 350px; 107 | margin-left: 1em; 108 | margin-top: -0.3em; 109 | } 110 | 111 | .connection-details .address-info .button { 112 | float: none; 113 | margin: 0 1em 1em 1em; 114 | } 115 | 116 | .connection-details hr { 117 | background-color: #c3c5c7; 118 | height: 1px; 119 | clear: both; 120 | } 121 | 122 | .connection-details .modal-card-foot .button { 123 | margin: 0; 124 | } 125 | 126 | /* Tablet */ 127 | @media screen and (min-width: 769px) { 128 | .connections { 129 | h3 { 130 | font-size: 27px; 131 | } 132 | 133 | h4 { 134 | font-size: 23px; 135 | } 136 | 137 | .tor-description { 138 | .column { 139 | padding: 1.5em 1em; 140 | text-align: left; 141 | } 142 | 143 | .column.is-2 { 144 | padding-top: 1em; 145 | } 146 | 147 | img { 148 | margin-left: 1em; 149 | } 150 | } 151 | } 152 | 153 | .connection-details .modal-card-foot { 154 | display: flex; 155 | } 156 | 157 | .connection-details .modal-card-foot .button { 158 | width: auto; 159 | height: 60px; 160 | font-size: 18px; 161 | font-weight: bold; 162 | flex-grow: 1; 163 | margin: 0 0.5em; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const HOST_IP = process.env.DEVICE_HOST; 3 | const CASA_NODE_HIDDEN_SERVICE = process.env.CASA_NODE_HIDDEN_SERVICE; 4 | 5 | module.exports = { 6 | env: { 7 | DEVICE_HOST: HOST_IP, 8 | CASA_NODE_HIDDEN_SERVICE: CASA_NODE_HIDDEN_SERVICE, 9 | 10 | // Default to mainnet explorers 11 | BITCOIN_EXPLORER: process.env.BITCOIN_EXPLORER || 'https://blockstream.info/tx/', 12 | LIGHTNING_EXPLORER: process.env.LIGHTNING_EXPLORER || 'https://explore.casa/nodes/' 13 | }, 14 | mode: 'universal', 15 | 16 | /* HTML Page Headers */ 17 | head: { 18 | title: 'Casa Node', 19 | meta: [ 20 | { charset: 'utf-8' }, 21 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 22 | { hid: 'description', name: 'description', content: 'Casa Lightning Node' } 23 | ], 24 | link: [ 25 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 26 | ] 27 | }, 28 | 29 | /* Customize progress-bar color */ 30 | loading: { color: '#572bf9' }, 31 | 32 | /* Global CSS */ 33 | css: ['~/css/main.scss'], 34 | 35 | /* Plugins to load before mounting the App */ 36 | plugins: [ 37 | { src: '~/plugins/vue-slideout', ssr: false }, 38 | { src: '~/plugins/vue-qr-code', ssr: false }, 39 | { src: '~/plugins/vue-moment', ssr: false }, 40 | { src: '~/plugins/vue-clipboard.js', ssr: false }, 41 | { src: '~/plugins/vue-offline.js', ssr: false }, 42 | { src: '~/plugins/filters.js', ssr: false }, 43 | { src: '~/plugins/interceptor', ssr: false }, 44 | { src: '~/plugins/vee-validate.js', ssr: false } 45 | ], 46 | 47 | /* Load Modules */ 48 | modules: [ 49 | // Docs: https://github.com/samtgarson/nuxt-env 50 | ['nuxt-env', {keys: ['DEVICE_HOST', 'CASA_NODE_HIDDEN_SERVICE']}], 51 | // Docs: https://auth.nuxtjs.org/middleware.html 52 | '@nuxtjs/auth', 53 | // Doc: https://github.com/nuxt-community/axios-module#usage 54 | '@nuxtjs/axios', 55 | // Doc: https://buefy.github.io/#/documentation 56 | ['nuxt-buefy', { css: true, materialDesignIcons: false }], 57 | // Doc: https://github.com/vaso2/nuxt-fontawesome 58 | 'nuxt-fontawesome' 59 | ], 60 | 61 | /* Axios module configuration */ 62 | axios: { 63 | // baseUrl: HOST_IP 64 | }, 65 | 66 | auth: { 67 | strategies: { 68 | local: { 69 | endpoints: { 70 | login: false, // Configure at layout level 71 | user: false, // Configure at layout level 72 | logout: false, 73 | loading: false, 74 | }, 75 | tokenType: 'JWT' 76 | } 77 | }, 78 | redirect: { 79 | login: '/login', 80 | logout: '/login', 81 | user: '/', 82 | home: '/' 83 | }, 84 | resetOnError: true, 85 | watchLoggedIn: false 86 | }, 87 | 88 | fontawesome: { 89 | imports: [ 90 | { 91 | set: '@fortawesome/free-solid-svg-icons', 92 | icons: ['faChevronRight', 'faChevronLeft', 'faExternalLinkAlt'] 93 | } 94 | ] 95 | }, 96 | 97 | /* Build configuration */ 98 | build: { 99 | // You can extend webpack config here 100 | extend(config, ctx) { 101 | // Run ESLint on save 102 | if (ctx.isDev && ctx.isClient) { 103 | config.module.rules.push({ 104 | enforce: 'pre', 105 | test: /\.(js|vue)$/, 106 | loader: 'eslint-loader', 107 | exclude: /(node_modules)/ 108 | }) 109 | } 110 | } 111 | }, 112 | 113 | /* Register middleware */ 114 | router: { 115 | middleware: ['teardown'] 116 | }, 117 | 118 | } 119 | -------------------------------------------------------------------------------- /helpers/interval-bus.js: -------------------------------------------------------------------------------- 1 | // An object to contain intervals, in case they need to be cleared 2 | const intervals = {}; 3 | 4 | // An object to store functions that will be called on intervals 5 | const intervalFunctions = {}; 6 | 7 | // Supported intervals range from 10 seconds to 1 hour 8 | const supportedIntervals = ['10', '30', '60', '90', '180', '300', '600', '900', '1800', '3600']; 9 | 10 | // Loop through supported intervals to populate the actual interval objects 11 | supportedIntervals.forEach(function(seconds) { 12 | intervals[seconds] = setInterval(function() { processInterval(seconds) }, parseInt(seconds) * 1000); 13 | intervalFunctions[seconds] = []; 14 | }); 15 | 16 | // The value of "this" when calling an interval function 17 | let intervalScope = false; 18 | 19 | // Call every function in the array on each interval 20 | function processInterval(seconds) { 21 | intervalFunctions[seconds].forEach(function(callback, index) { 22 | setTimeout(function() { 23 | if(intervalScope) { 24 | callback.call(intervalScope); 25 | } else { 26 | callback(); 27 | } 28 | }, index * 1000); // Add a slight delay between each function call 29 | }); 30 | } 31 | 32 | function isValidDuration(seconds) { 33 | if(intervals[seconds] !== undefined) { 34 | return true; 35 | } 36 | 37 | throw new Error('Invalid interval duration, supported intervals are: ' + supportedIntervals.toString()); 38 | } 39 | 40 | function intervalFunctionExists(callback) { 41 | for(const [seconds, functions] of Object.entries(intervalFunctions)) { 42 | if(functions.indexOf(callback) > -1) { 43 | return seconds; 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | // Loop through every duration of intervals and make sure this callback is removed 51 | function clearIntervalFunction(callback) { 52 | for(const [seconds, functions] of Object.entries(intervalFunctions)) { 53 | if(functions.indexOf(callback) > -1) { 54 | intervalFunctions[seconds].splice(functions.indexOf(callback), 1); 55 | } 56 | } 57 | } 58 | 59 | // Public functions to control the interval bus 60 | const IntervalBus = { 61 | scope(scope) { 62 | intervalScope = scope; 63 | }, 64 | 65 | set(callback, seconds) { 66 | if(isValidDuration(seconds)) { 67 | // Does this function already exist? Remove it and re-add it with the new duration 68 | if(intervalFunctionExists(callback)) { 69 | clearIntervalFunction(callback); 70 | } 71 | 72 | intervalFunctions[seconds].push(callback); 73 | } 74 | }, 75 | 76 | clear(callback) { 77 | if(intervalFunctionExists(callback)) { 78 | clearIntervalFunction(callback); 79 | } 80 | }, 81 | 82 | // Helper function to pick the smallest interval that is greater than a given number of seconds 83 | // For example, if an API request takes approximately 44 seconds to complete, you'd want to use an interval of 60 seconds 84 | bestInterval(seconds, minimum = false) { 85 | // Select the largest interval by default 86 | let bestInterval = Math.max(...supportedIntervals); 87 | 88 | // Loop through supported intervals in descending order, without reorganzing the original array 89 | [...supportedIntervals].sort(function(a, b) {return b - a}).forEach(function(interval) { 90 | if(seconds < interval) { 91 | bestInterval = interval; 92 | } 93 | }); 94 | 95 | // Is there a minimum interval? Is the current interval less than it? 96 | if(minimum && isValidDuration(minimum)) { 97 | if(bestInterval < minimum) { 98 | bestInterval = minimum; 99 | } 100 | } 101 | 102 | return bestInterval; 103 | }, 104 | 105 | longestInterval() { 106 | return Math.max(...supportedIntervals); 107 | }, 108 | } 109 | 110 | export default IntervalBus; 111 | 112 | // Required export for testing 113 | export { intervalFunctionExists }; 114 | -------------------------------------------------------------------------------- /components/Lightning/Modals/PaymentRequests/RequestLnd.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 109 | -------------------------------------------------------------------------------- /css/pages/withdraw.scss: -------------------------------------------------------------------------------- 1 | .withdraw .field.hide-label .field-label { 2 | display: none; 3 | } 4 | 5 | .withdrawal-amount .label { 6 | display: none; 7 | } 8 | 9 | .withdrawal-amount p, 10 | .withdrawal-fee p { 11 | font-size: 18px; 12 | line-height: 1.33; 13 | letter-spacing: -0.2px; 14 | color: #8d8e8e; 15 | padding-top: 0.5em; 16 | padding-bottom: 1em; 17 | } 18 | 19 | .withdrawal-amount.request-amount h2 { 20 | margin-bottom: 0.5em; 21 | } 22 | 23 | .withdrawal-amount.request-amount .control input { 24 | font-size: 20px; 25 | } 26 | 27 | .withdrawal-amount.request-amount p { 28 | padding-top: 0; 29 | } 30 | 31 | .withdraw .label, .withdraw h2 { 32 | font-size: 20px; 33 | font-weight: bold; 34 | line-height: 1.2; 35 | color: #0a0525; 36 | } 37 | 38 | .withdraw .modal-card-foot { 39 | background-color: #FFFFFF; 40 | display: block; 41 | border-top: none; 42 | padding-top: 1em; 43 | } 44 | 45 | .withdraw .field .help { 46 | display: none; 47 | } 48 | 49 | .withdraw { 50 | .withdrawal-amount { 51 | position: relative; 52 | } 53 | 54 | .send-max { 55 | position: absolute; 56 | z-index: 1; 57 | top: 1.5em; 58 | right: 0; 59 | border-width: 2px; 60 | 61 | &.is-active { 62 | color: #fff; 63 | background-color: #0a0525 !important; 64 | } 65 | } 66 | } 67 | 68 | .withdrawal-fee { 69 | margin-top: 2.5em; 70 | 71 | .fee-options { 72 | position: relative; 73 | display: flex; 74 | color: #b5b4bd; 75 | 76 | &::before { 77 | position: absolute; 78 | width: 75%; 79 | left: 10%; 80 | top: 0.75em; 81 | content: ''; 82 | border-top: 3px solid rgba(181, 180, 189, 0.5); 83 | } 84 | } 85 | 86 | .fee-option { 87 | position: relative; 88 | flex-grow: 1; 89 | padding-top: 2.4em; 90 | font-weight: bold; 91 | text-align: center; 92 | cursor: pointer; 93 | 94 | .fee-time { 95 | font-size: 15px; 96 | font-weight: normal; 97 | } 98 | 99 | &::before { 100 | position: absolute; 101 | top: 0; 102 | left: calc(50% - 0.85em); 103 | width: 1.7em; 104 | height: 1.7em; 105 | background-color: #fff; 106 | content: ''; 107 | border-radius: 100%; 108 | border: 3px solid rgba(181, 180, 189, 0.5); 109 | } 110 | } 111 | 112 | .fee-option:hover { 113 | .fee-cost { 114 | color: #865efc; 115 | } 116 | 117 | &::before { 118 | border-color: #865efc; 119 | opacity: 1; 120 | } 121 | } 122 | 123 | .fee-option.active { 124 | .fee-cost { 125 | color: #865efc; 126 | } 127 | 128 | &::before { 129 | border-color: #865efc; 130 | background-color: #865efc; 131 | background-image: url('~assets/checkmark.svg'); 132 | background-size: 1em; 133 | background-repeat: no-repeat; 134 | background-position: center; 135 | opacity: 1; 136 | } 137 | } 138 | } 139 | 140 | /* Send Lightning Payment */ 141 | .send.withdraw input { 142 | font-size: 1.25em; 143 | } 144 | 145 | .confirm-withdraw { 146 | .modal-card-body { 147 | text-align: center; 148 | padding: 2em 1em; 149 | } 150 | 151 | h2 { 152 | font-size: 48px; 153 | font-weight: bold; 154 | letter-spacing: -1.2px; 155 | color: #0a0525; 156 | } 157 | 158 | h3 { 159 | font-size: 24px; 160 | font-weight: 500; 161 | letter-spacing: -0.6px; 162 | color: #0a0525; 163 | } 164 | 165 | .recipient { 166 | font-size: 18px; 167 | font-weight: 500; 168 | 169 | span { 170 | background-color: #f5f5f5; 171 | border-radius: 100%; 172 | padding: 0.75em; 173 | margin-right: 0.5em; 174 | font-size: 13px; 175 | font-weight: 900; 176 | text-transform: uppercase; 177 | } 178 | } 179 | 180 | .amount { 181 | margin-bottom: 4em; 182 | } 183 | 184 | .column { 185 | label { 186 | display: block; 187 | color: rgba(10, 5, 37, 0.4); 188 | text-transform: uppercase; 189 | font-size: 14px; 190 | font-weight: 900; 191 | } 192 | 193 | .tooltip { 194 | text-transform: none; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /components/Lightning/Modals/Channels/ConfirmCloseChannel.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 120 | -------------------------------------------------------------------------------- /css/components/modals.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | z-index: 1001; 3 | 4 | .animation-content .modal-card { 5 | max-width: calc(100vw - 40px); 6 | } 7 | } 8 | 9 | .modal-card { 10 | width: auto; 11 | max-height: calc(100vh - 80px); 12 | } 13 | 14 | .modal-card-head { 15 | display: block; 16 | 17 | 18 | hr { 19 | border-bottom: 1px solid #dbdbdb; 20 | margin: 0.5em 0; 21 | } 22 | } 23 | 24 | .modal-card .modal-card-title { 25 | font-size: 22px; 26 | font-weight: bold; 27 | color: #0a0525; 28 | flex-grow: 0; 29 | } 30 | 31 | .modal-card-title .modal-title-right { 32 | font-size: 20px; 33 | letter-spacing: -0.2px; 34 | text-align: right; 35 | color: #0a0525; 36 | } 37 | 38 | .modal-card-title .tag { 39 | vertical-align: middle; 40 | margin-left: 1em; 41 | } 42 | 43 | .modal-card-foot { 44 | background-color: #FFFFFF; 45 | display: block; 46 | border-top: none; 47 | padding-top: 2em; 48 | } 49 | 50 | .modal-card-foot button, 51 | .modal-card-foot .button { 52 | min-width: 180px; 53 | height: 60px; 54 | font-size: 18px; 55 | font-weight: bold; 56 | width: 100%; 57 | } 58 | 59 | .modal-card-foot .cancel { 60 | color: #8d8e8e; 61 | background-color: whitesmoke; 62 | border-color: transparent; 63 | color: #363636; 64 | margin-bottom: 1em; 65 | } 66 | 67 | /* Alerts (inherits from modal) */ 68 | .alert .modal-card-foot button { 69 | width: 100%; 70 | height: 60px; 71 | font-size: 18px; 72 | font-weight: bold; 73 | } 74 | 75 | .alert .modal-card.body p { 76 | color: #8d8e8e; 77 | font-size: 20px; 78 | } 79 | 80 | .alert .modal-card-foot { 81 | padding-top: 0.5em; 82 | } 83 | 84 | .alert-icon-container { 85 | position: relative; 86 | } 87 | 88 | .alert-icon-container img { 89 | position: absolute; 90 | z-index: 999; 91 | top: 7px; 92 | right: 7px; 93 | width: 50px; 94 | height: 50px; 95 | } 96 | 97 | .alert-status .modal-card-head { 98 | border: none; 99 | } 100 | 101 | .alert-status .modal-card-body { 102 | text-align: center; 103 | padding: 0.5em 1.5em; 104 | } 105 | 106 | .alert-status .modal-card-body h2 { 107 | font-size: 26px; 108 | font-weight: bold; 109 | color: #0a0525; 110 | } 111 | 112 | .alert-status .modal-card-body p { 113 | font-size: 22px; 114 | font-weight: 500; 115 | margin-top: 18px; 116 | line-height: 1.45; 117 | color: #8d8e8e; 118 | } 119 | 120 | .alert-status .modal-card-body > p:first-of-type { 121 | margin-top: 0; 122 | } 123 | 124 | .modal-card-head > p:nth-child(2) { 125 | margin-right: 2.5em; 126 | } 127 | 128 | .generic-modal .modal-card-head { 129 | font-size: 26px; 130 | font-weight: bold; 131 | color: #0a0525; 132 | } 133 | 134 | .generic-modal .modal-card-body { 135 | padding-top: 1em; 136 | text-align: left; 137 | } 138 | 139 | .generic-modal .modal-card-body p { 140 | font-size: 18px; 141 | line-height: 24px; 142 | color: #636363; 143 | font-weight: normal; 144 | } 145 | 146 | .generic-modal .modal-card-body strong { 147 | padding-top: 1.2em; 148 | font-size: 20px; 149 | display: block; 150 | } 151 | 152 | .generic-modal .modal-card-body input { 153 | height: 3em; 154 | font-size: 20px; 155 | } 156 | 157 | .generic-modal .modal-card-body .columns { 158 | margin: -1.5em -0.7em 0 -0.7em; 159 | } 160 | 161 | .generic-modal .modal-card-body .column { 162 | padding-right: 0.5em; 163 | padding-left: 0.5em; 164 | } 165 | 166 | .modal-title-right .amount { 167 | font-size: 18px; 168 | font-weight: bold; 169 | letter-spacing: -0.2px; 170 | color: #0a0525; 171 | } 172 | 173 | .modal-title-right .amount-description { 174 | font-size: 14px; 175 | font-weight: bold; 176 | letter-spacing: -0.2px; 177 | color: #c3c5c7; 178 | } 179 | 180 | .modal-close { 181 | top: 7px; 182 | right: 14px; 183 | } 184 | 185 | /* Tablet */ 186 | @media screen and (min-width: 769px) { 187 | .modal .animation-content { 188 | width: 640px; 189 | } 190 | 191 | .modal-card-head { 192 | display: flex; 193 | justify-content: space-between; 194 | } 195 | 196 | .modal-card .modal-card-title { 197 | font-size: 26px; 198 | } 199 | 200 | .alert .modal-card-foot { 201 | padding-top: 1.5em; 202 | } 203 | 204 | .modal-card-foot button, 205 | .modal-card-foot .button, 206 | .alert .modal-card-foot button { 207 | width: auto; 208 | } 209 | 210 | .modal-card-foot .cancel { 211 | margin-bottom: 0; 212 | } 213 | 214 | .modal-card-foot .is-casa:nth-child(2) { 215 | float: right; 216 | } 217 | 218 | .alert-status .modal-card-body { 219 | padding: 2em; 220 | } 221 | 222 | .modal-close { 223 | top: 20px; 224 | right: 20px; 225 | } 226 | 227 | .alert-icon-container img { 228 | position: absolute; 229 | z-index: 999; 230 | top: -50px; 231 | right: auto; 232 | left: calc(50% - 40px); 233 | width: 100px; 234 | height: 100px; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /layouts/login.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 241 | -------------------------------------------------------------------------------- /css/components/slideout.scss: -------------------------------------------------------------------------------- 1 | .slideout { 2 | width: 100vw; 3 | 4 | hr { 5 | margin: 0.75em 0; 6 | } 7 | } 8 | 9 | .slideout-panel-open { 10 | overflow: hidden; 11 | height: 100vh; 12 | width: 100vw; 13 | } 14 | 15 | .slideout-panel-bg { 16 | background-color: #01090c!important; 17 | opacity: 0.8!important; 18 | } 19 | 20 | .slideout-header { 21 | display: -webkit-flex; 22 | display: -ms-flexbox; 23 | display: flex; 24 | -webkit-align-items: center; 25 | -ms-flex-align: center; 26 | align-items: center; 27 | background-color: #EEEEEE; 28 | height: 70px; 29 | padding: 0 1em; 30 | border-bottom: 1px solid rgba(151, 151, 151, 0.21); 31 | } 32 | 33 | .slideout-header a { 34 | text-decoration: none; 35 | color: #9b9b9b; 36 | font-size: 16px; 37 | font-weight: bold; 38 | } 39 | 40 | .app-slideout { 41 | padding: 1em; 42 | } 43 | 44 | .app-slideout > .app-title { 45 | display: flex; 46 | align-items: center; 47 | } 48 | 49 | .app-slideout .app-content { 50 | margin-left: 0.25em; 51 | margin-right: 0.25em; 52 | } 53 | 54 | .app-title img { 55 | max-width: 2.5em; 56 | margin-right: .4em; 57 | } 58 | 59 | .app-title h2 { 60 | font-size: 25px; 61 | font-weight: bold; 62 | } 63 | 64 | .btc .app-title h2 { 65 | color: #f7941a; 66 | } 67 | 68 | .lnd .app-title h2 { 69 | color: #0a0525; 70 | } 71 | 72 | .settings .app-title h2 { 73 | color: #0a0525; 74 | } 75 | 76 | /* Input Fields and Buttons */ 77 | .app-settings { 78 | padding: 0 0.5em; 79 | } 80 | 81 | .app-settings .field { 82 | padding: 0.25em 0; 83 | } 84 | 85 | .toggle-settings .field-label { 86 | padding-top: 0; 87 | text-align: left; 88 | align-self: flex-start; 89 | padding-right: 3em; 90 | } 91 | 92 | .app-slideout .field-label p { 93 | color: #8d8e8e; 94 | margin-bottom: 1em; 95 | } 96 | 97 | .app-slideout .field-label .button { 98 | padding: 1em 2.5em; 99 | text-transform: uppercase; 100 | letter-spacing: 1px; 101 | font-size: 13px; 102 | } 103 | 104 | .app-slideout .field-label .button.is-light { 105 | color: #c3c5c7; 106 | } 107 | 108 | .app-slideout .field-label .button.is-danger { 109 | border: 2px solid #f0649e; 110 | background-color: #fff; 111 | color: #f0649e; 112 | font-weight: bold; 113 | } 114 | 115 | .app-slideout .field-label .button.is-success { 116 | border: 2px solid #2fb4b5; 117 | background-color: #fff; 118 | color: #2fb4b5; 119 | font-weight: bold; 120 | } 121 | 122 | .app-slideout .field-body input { 123 | padding: 1.5em 1em; 124 | } 125 | 126 | .app-slideout .field-body .button { 127 | padding: 1.5em; 128 | width: 100%; 129 | } 130 | 131 | .toggle-settings .field-body { 132 | flex-grow: 1; 133 | justify-content: flex-end; 134 | } 135 | 136 | .toggle-settings .field-label label { 137 | color: #0a0525; 138 | font-size: 20px; 139 | font-weight: bold; 140 | } 141 | 142 | .toggle-settings span { 143 | color: #c3c5c7; 144 | font-size: 18px; 145 | font-weight: bold; 146 | } 147 | 148 | .toggle-settings .field-body { 149 | flex-grow: unset; 150 | flex-basis: auto; 151 | justify-content: flex-end; 152 | } 153 | 154 | .toggle-settings .button.cancel, 155 | .toggle-settings .button.save { 156 | width: 100%; 157 | height: 60px; 158 | font-size: 18px; 159 | margin-top: 1rem; 160 | } 161 | 162 | .toggle-settings .button.is-light { 163 | color: #8d8e8e; 164 | font-weight: bold; 165 | } 166 | 167 | .menu-navigation a { 168 | border-radius: 24px; 169 | box-shadow: 0 3px 5px 0 #0000000c; 170 | background-color: #ffffff; 171 | } 172 | 173 | .menu-navigation span { 174 | color: #0a0525; 175 | } 176 | 177 | .menu-navigation .icon { 178 | color: #8d8e8e; 179 | } 180 | 181 | .slideout-header > a > span { 182 | padding: 1rem 0rem 1rem 0.5rem; 183 | vertical-align: middle; 184 | } 185 | 186 | .slideout-panel .slideout-panel-bg { 187 | z-index: 39; 188 | } 189 | 190 | .slideout-wrapper > #slide-out-panel { 191 | z-index: 42; 192 | } 193 | 194 | /* Tablet */ 195 | @media screen and (min-width: 769px) { 196 | .app-slideout { 197 | padding: 2em; 198 | } 199 | 200 | .app-slideout .app-content { 201 | margin-left: 3em; 202 | margin-right: 3em; 203 | } 204 | 205 | .app-settings { 206 | padding: 0 3em; 207 | } 208 | 209 | .app-title h2 { 210 | font-size: 34px; 211 | } 212 | 213 | .slideout-header { 214 | padding: 0 2em; 215 | } 216 | 217 | .toggle-settings .is-casa:nth-child(2) { 218 | float: right; 219 | } 220 | 221 | .field-body > div:nth-child(2) { 222 | display: inline; 223 | float: right; 224 | flex-grow: unset!important; 225 | } 226 | 227 | .toggle-settings .button.cancel, 228 | .toggle-settings .button.save { 229 | width: 200px; 230 | } 231 | } 232 | 233 | 234 | /* Desktop */ 235 | @media screen and (min-width: 1088px) { 236 | .slideout { 237 | width: 900px; 238 | 239 | hr { 240 | margin: 1.5em 0; 241 | } 242 | } 243 | 244 | .app-slideout .field-body .button { 245 | width: auto; 246 | } 247 | 248 | .field-body > div:nth-child(1) { 249 | display: inline-block; 250 | float: right; 251 | margin-right: 0; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /components/Lightning/Modals/Channels/ManageChannel.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 133 | -------------------------------------------------------------------------------- /css/components/channels.scss: -------------------------------------------------------------------------------- 1 | /* Channels */ 2 | .channel .transaction-settings .button { 3 | width: 100%; 4 | color: #3e3b53; 5 | font-weight: bold; 6 | height: 48px; 7 | font-size: 18px; 8 | border-radius: 4px; 9 | box-shadow: 0 3px 5px 0 #0000000c; 10 | background-color: #ffffff; 11 | border: solid 1px #d7d8d9; 12 | } 13 | 14 | .channel .tx-col-2 h2 { 15 | font-weight: bold; 16 | color: #0a0525; 17 | } 18 | 19 | .channel .tx-col-3 { 20 | text-align: right; 21 | } 22 | 23 | .channel-status { 24 | height: 24px; 25 | font-size: 20px; 26 | font-weight: 600; 27 | color: #2dcccd; 28 | } 29 | 30 | .channel.custom .channel-status { 31 | margin-left: 90px; 32 | margin-bottom: 5px; 33 | } 34 | 35 | .channel-balance { 36 | text-align: right; 37 | color: #95939f; 38 | 39 | span { 40 | color: #8d8e8e; 41 | display: inline-block; 42 | width: 130px; 43 | } 44 | 45 | strong { 46 | display: inline-block; 47 | margin-right: 8px; 48 | width: 80px; 49 | } 50 | } 51 | 52 | .channel-icon { 53 | width: 50px; 54 | height: 50px; 55 | border-radius: 12.5px; 56 | background-color: #ffffff; 57 | display: flex; 58 | flex-direction: column; 59 | justify-content: center; 60 | text-align: center; 61 | } 62 | 63 | .channel-icon img { 64 | width: 30px; 65 | height: 30px; 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: center; 69 | text-align: center; 70 | } 71 | 72 | .channel-online-status { 73 | border-radius: 16.5px; 74 | border: solid 2px #2dcccd; 75 | } 76 | 77 | .channel-offline-status { 78 | border-radius: 16.5px; 79 | border: solid 2px #f0649e; 80 | } 81 | 82 | .channel-online-status > span { 83 | font-size: 11px; 84 | font-weight: 900; 85 | letter-spacing: 0.8px; 86 | text-align: center; 87 | color: #2dcccd; 88 | padding: 1em; 89 | } 90 | 91 | .channel-offline-status > span { 92 | font-size: 11px; 93 | font-weight: 900; 94 | letter-spacing: 0.8px; 95 | text-align: center; 96 | color: #f0649e; 97 | padding: 1em; 98 | } 99 | 100 | .channel-manager h2 { 101 | margin-bottom: 0.5em; 102 | } 103 | 104 | .channel-manager a.is-pink { 105 | border-radius: 4px; 106 | background-color: #f0649e; 107 | font-size: 20px; 108 | } 109 | 110 | .channel-manager a.is-pink:hover { 111 | border-radius: 4px; 112 | background-color: #f0649e; 113 | font-size: 20px; 114 | } 115 | 116 | .channel-manager .input-wrapper { 117 | margin: 0; 118 | } 119 | 120 | .channel-manager .peer-name { 121 | font-size: 20px; 122 | } 123 | 124 | .channel-manager .channel-purpose { 125 | font-size: 20px; 126 | } 127 | 128 | .channel-manager .input-wrapper input { 129 | font-size: 40px; 130 | font-weight: 700; 131 | text-align: center; 132 | border: none; 133 | box-shadow: none; 134 | height: 2em; 135 | } 136 | 137 | .channel-manager .input-wrapper .help { 138 | font-size: 13px; 139 | font-weight: 900; 140 | letter-spacing: 0.5px; 141 | text-align: center; 142 | color: #0a0525; 143 | margin-top: 0; 144 | margin-bottom: 1em; 145 | } 146 | 147 | .channel-manager .input-wrapper { 148 | border-radius: 4px; 149 | background-color: #ffffff; 150 | border: solid 1px #d7d8d9; 151 | } 152 | 153 | .request-amount { 154 | .field.is-grouped { 155 | display: block; 156 | 157 | .is-expanded { 158 | margin: 0.5em 0; 159 | } 160 | } 161 | } 162 | 163 | .channel { 164 | .tx-row { 165 | cursor: pointer; 166 | } 167 | 168 | .is-casa { 169 | margin-left: auto; 170 | } 171 | 172 | .is-casa.mobile-only { 173 | margin: 1em 0.5em 0.5em 0.5em; 174 | width: 100%; 175 | max-width: 320px; 176 | } 177 | 178 | .filter-buttons { 179 | a { 180 | border-width: 3px; 181 | margin-right: 0.75em; 182 | color: #0a0525; 183 | font-weight: 900; 184 | text-transform: uppercase; 185 | font-size: 14px; 186 | } 187 | 188 | a.is-active { 189 | border-color: #0a0525; 190 | background-color: #0a0525; 191 | color: #fff; 192 | } 193 | } 194 | 195 | .tx-row { 196 | a.button { 197 | color: #c3c5c7; 198 | border-color: #e5e3e9; 199 | border-width: 2px; 200 | font-weight: 900; 201 | text-transform: uppercase; 202 | font-size: 12px; 203 | margin-left: -0.2em; 204 | margin-bottom: 0.5em; 205 | display: block; 206 | max-width: 100px; 207 | } 208 | } 209 | 210 | .help { 211 | margin-top: 0.5em; 212 | font-size: 16px; 213 | } 214 | } 215 | 216 | .channel.custom { 217 | .tx-col-2 { 218 | margin-left: 0; 219 | } 220 | } 221 | 222 | .channel.open { 223 | section { 224 | h2 { 225 | font-size: 19px; 226 | font-weight: bold; 227 | color: #0a0525; 228 | padding: 0.25em 0; 229 | } 230 | 231 | input { 232 | padding: 1.75em 0.75em; 233 | } 234 | 235 | a.blue { 236 | padding-top: 0.5em; 237 | } 238 | } 239 | } 240 | 241 | /* Tablet */ 242 | @media screen and (min-width: 768px) { 243 | .channel.custom { 244 | section { 245 | margin-top: 1em; 246 | margin-left: 3em; 247 | } 248 | } 249 | 250 | .request-amount { 251 | .field.is-grouped { 252 | display: flex; 253 | justify-content: space-between; 254 | 255 | .is-expanded:first-of-type { 256 | margin-right: 1em; 257 | } 258 | } 259 | } 260 | 261 | .channel { 262 | .tx-row { 263 | a.button { 264 | margin-left: 0.5em; 265 | margin-top: -0.25em; 266 | display: inline-block; 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 83 | 84 | 85 | 170 | -------------------------------------------------------------------------------- /components/Lightning/Modals/SendPayment/ConfirmPayment.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 168 | -------------------------------------------------------------------------------- /assets/logo-casa-extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------