├── .ackrc ├── .dockerignore ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SHA256SUMS.asc ├── arm32v7.Dockerfile ├── arm64v8.Dockerfile ├── babel.config.js ├── client ├── babel.config.js ├── build.sh ├── fonts │ ├── .gitignore │ ├── install-fonts.sh │ ├── list-fonts.sh │ ├── npm-shrinkwrap.json │ ├── package.json │ └── rewrite-css.sh ├── index.pug ├── npm-shrinkwrap.json ├── package.json ├── serve.js ├── src │ ├── app.js │ ├── driver │ │ ├── conf.js │ │ ├── cordova-notification.js │ │ ├── cordova-qrscanner.js │ │ ├── cordova-urihandler.js │ │ ├── electron-ipc.js │ │ ├── electron-notification.js │ │ ├── electron-urihandler.js │ │ ├── instascan.js │ │ ├── route.js │ │ ├── screen-orient.js │ │ ├── sse.js │ │ └── web-notification.js │ ├── intent.js │ ├── load-theme.js │ ├── model.js │ ├── rpc.js │ ├── rxjs.js │ ├── server-settings.js │ ├── util.js │ ├── view.js │ ├── views │ │ ├── channels.js │ │ ├── expert.js │ │ ├── home.js │ │ ├── index.js │ │ ├── layout.js │ │ ├── node.js │ │ ├── offers.js │ │ ├── onchain.js │ │ ├── pay.js │ │ ├── recv.js │ │ ├── server-settings.js │ │ └── util.js │ └── worker.js ├── styl │ ├── .gitignore │ ├── channels.styl │ ├── fancy-checkbox.styl │ ├── icons.styl │ ├── loader.styl │ ├── noscript.styl │ ├── qr.styl │ ├── scrollbar.styl │ ├── style.styl │ └── theme-touchups.styl ├── swatch │ ├── .gitignore │ └── dark │ │ ├── _bootswatch.scss │ │ ├── _variables.scss │ │ ├── bootstrap.css │ │ └── bootstrap.min.css ├── theme-colors.json └── www │ ├── .gitignore │ ├── favicon.ico │ ├── manifest │ ├── icon-16.png │ ├── icon-167.png │ ├── icon-180.png │ ├── icon-192.png │ ├── icon-32.png │ ├── icon-512.png │ └── manifest.json │ └── notification.png ├── cordova ├── .gitignore ├── build.sh ├── config.xml ├── npm-shrinkwrap.json ├── package.json └── res │ └── android │ ├── icon-144-xxhdpi.png │ ├── icon-192-xxxhdpi.png │ ├── icon-36-ldpi.png │ ├── icon-48-mdpi.png │ ├── icon-72-hdpi.png │ └── icon-96-xhdpi.png ├── doc ├── dev-regtest-env.md ├── docker.md ├── img │ ├── gui-controls.gif │ ├── payment.gif │ └── spark-header.png ├── onion.md ├── privacy.md ├── reproducible-builds.md ├── startup-systemd.md └── tls.md ├── electron ├── .gitignore ├── babel.config.js ├── build.sh ├── build │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── hook-afterPack.js ├── main.js ├── npm-shrinkwrap.json ├── package.json ├── preload.js └── server-controller.js ├── npm-shrinkwrap.json ├── package.json ├── scripts ├── build.sh ├── builder.Dockerfile ├── dist-shasums.sh ├── docker-entrypoint.sh ├── release.sh ├── spark-wallet.service └── start.sh └── src ├── app.js ├── auth.js ├── cli.js ├── cmd.js ├── exchange-rate.js ├── stream.js ├── transport ├── granax-dep │ ├── index.js │ ├── npm-shrinkwrap.json │ └── package.json ├── http.js ├── onion.js └── tls.js └── webui.js /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=dist 2 | --ignore-dir=client/dist 3 | --ignore-dir=client/swatch 4 | --ignore-dir=client/fonts 5 | --ignore-dir=electron/dist 6 | --ignore-dir=electron/www 7 | --ignore-dir=cordova/dist 8 | --ignore-dir=cordova/www 9 | --ignore-dir=cordova/platforms 10 | --ignore-dir=cordova/plugins 11 | --ignore-dir=docker-builds 12 | --ignore-file=is:server.bundle.js 13 | --ignore-file=is:npm-shrinkwrap.json 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | cordova/platforms 3 | cordova/www 4 | cordova/plugins 5 | electron/dist 6 | electron/www 7 | electron/server.bundle.js 8 | client/dist 9 | client/www/*.css 10 | dist 11 | spark-wallet-*-npm.tgz 12 | node_modules 13 | **/node_modules 14 | doc 15 | docker-builds 16 | .dockerignore 17 | Dockerfile 18 | *.Dockerfile 19 | scripts/builder.Dockerfile 20 | scripts/release.sh 21 | .env 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | TODO 4 | dist 5 | spark-wallet-*-npm.tgz 6 | docker-builds 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: minimal 2 | before_install: sudo modprobe fuse 3 | before_script: docker build -f scripts/builder.Dockerfile -t spark-builder . 4 | script: docker run --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined spark-builder 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.8-bullseye-slim as builder 2 | 3 | ARG DEVELOPER 4 | ARG STANDALONE 5 | ENV STANDALONE=$STANDALONE 6 | 7 | # Install build dependencies for third-party packages (c-lightning/bitcoind) 8 | RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates dirmngr wget \ 9 | $([ -n "$STANDALONE" ] || echo "autoconf automake build-essential gettext gpg gpg-agent libtool libgmp-dev \ 10 | libsqlite3-dev python python3 python3-mako python3-pip wget zlib1g-dev unzip") 11 | 12 | ENV LIGHTNINGD_VERSION=0.10.2 13 | ENV LIGHTNINGD_SHA256=3c9dcb686217b2efe0e988e90b95777c4591e3335e259e01a94af87e0bf01809 14 | 15 | RUN [ -n "$STANDALONE" ] || ( \ 16 | wget -O /tmp/lightning.zip https://github.com/ElementsProject/lightning/releases/download/v$LIGHTNINGD_VERSION/clightning-v$LIGHTNINGD_VERSION.zip \ 17 | && echo "$LIGHTNINGD_SHA256 /tmp/lightning.zip" | sha256sum -c \ 18 | && unzip /tmp/lightning.zip -d /tmp/lightning \ 19 | && cd /tmp/lightning/clightning* \ 20 | && pip3 install mrkd \ 21 | && DEVELOPER=$DEVELOPER ./configure --prefix=/opt/lightning \ 22 | && make && make install) 23 | 24 | # Install bitcoind 25 | ENV BITCOIN_VERSION 22.0 26 | ENV BITCOIN_FILENAME bitcoin-$BITCOIN_VERSION-x86_64-linux-gnu.tar.gz 27 | ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/$BITCOIN_FILENAME 28 | ENV BITCOIN_SHA256 59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16 29 | RUN [ -n "$STANDALONE" ] || \ 30 | (mkdir /opt/bitcoin && cd /opt/bitcoin \ 31 | && wget -qO "$BITCOIN_FILENAME" "$BITCOIN_URL" \ 32 | && echo "$BITCOIN_SHA256 $BITCOIN_FILENAME" | sha256sum -c - \ 33 | && BD=bitcoin-$BITCOIN_VERSION/bin \ 34 | && tar -xzvf "$BITCOIN_FILENAME" $BD/bitcoind $BD/bitcoin-cli --strip-components=1) 35 | 36 | RUN mkdir -p /opt/bin /opt/bitcoin/bin /opt/lightning 37 | 38 | # npm doesn't normally like running as root, allow it since we're in docker 39 | RUN npm config set unsafe-perm true 40 | 41 | # Install tini 42 | RUN wget -O /opt/bin/tini "https://github.com/krallin/tini/releases/download/v0.18.0/tini-amd64" \ 43 | && echo "12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855 /opt/bin/tini" | sha256sum -c - \ 44 | && chmod +x /opt/bin/tini 45 | 46 | RUN ls -l /opt/lightning 47 | 48 | # Install Spark 49 | WORKDIR /opt/spark/client 50 | COPY client/package.json client/npm-shrinkwrap.json ./ 51 | COPY client/fonts ./fonts 52 | RUN npm install 53 | 54 | WORKDIR /opt/spark 55 | COPY package.json npm-shrinkwrap.json ./ 56 | RUN npm install 57 | COPY . . 58 | 59 | # Build production NPM package 60 | RUN npm run dist:npm \ 61 | && npm prune --production \ 62 | && find . -mindepth 1 -maxdepth 1 \ 63 | ! -name '*.json' ! -name dist ! -name LICENSE ! -name node_modules ! -name scripts \ 64 | -exec rm -r "{}" \; 65 | 66 | # Prepare final image 67 | 68 | FROM node:16.8-bullseye-slim 69 | 70 | ARG STANDALONE 71 | ENV STANDALONE=$STANDALONE 72 | 73 | WORKDIR /opt/spark 74 | 75 | RUN apt-get update && apt-get install -y --no-install-recommends xz-utils inotify-tools netcat-openbsd \ 76 | $([ -n "$STANDALONE" ] || echo libgmp-dev libsqlite3-dev) \ 77 | && rm -rf /var/lib/apt/lists/* \ 78 | && ln -s /opt/spark/dist/cli.js /usr/bin/spark-wallet \ 79 | && mkdir /data \ 80 | && ln -s /data/lightning $HOME/.lightning 81 | 82 | COPY --from=builder /opt/spark /opt/spark 83 | COPY --from=builder /opt/lightning /opt/lightning 84 | COPY --from=builder /opt/bitcoin/bin/ /usr/bin 85 | COPY --from=builder /opt/bin/ /usr/bin 86 | RUN ln -s /opt/lightning/bin/* /usr/bin 87 | 88 | ENV CONFIG=/data/spark/config TLS_PATH=/data/spark/tls TOR_PATH=/data/spark/tor COOKIE_FILE=/data/spark/cookie HOST=0.0.0.0 89 | 90 | # link the granax (Tor Control client) node_modules installation directory 91 | # inside /data/spark/tor/, to persist the Tor Bundle download in the user-mounted volume 92 | RUN ln -s $TOR_PATH/tor-installation/node_modules dist/transport/granax-dep/node_modules 93 | 94 | VOLUME /data 95 | ENTRYPOINT [ "tini", "-g", "--", "scripts/docker-entrypoint.sh" ] 96 | 97 | EXPOSE 9735 9737 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2018 Nadav Ivgi 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 | -------------------------------------------------------------------------------- /SHA256SUMS.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNED MESSAGE----- 2 | Hash: SHA256 3 | 4 | 3d0aae43e18bf3d45323c571d9477480e290ea05f6f82bf21a621bdc42dc5fe0 spark-wallet-0.3.1-npm.tgz 5 | b7aa4b2d2805400243dff66c46313d5155d60081d8cff01a6da3567729032d2c spark-wallet-0.3.1-linux-x86_64.AppImage 6 | 074554cbccbf2af7d804272bdb1dee0e266ce8ef5157973996876ff49022fe32 spark-wallet-0.3.1-linux-amd64.deb 7 | b2fffb8b4261ddb74cb320ea6ccfc06d617d34b80e1c52767771474625114c00 spark-wallet-0.3.1-linux-amd64.snap 8 | d3792e7a4a5a36821218c5773783a75d8c58daac7cc707042c9c5052d0a592c3 spark-wallet-0.3.1-linux-x64.tar.gz 9 | 89ddd5284cd74eb5d8731b56fdb5dbb685b4d9574dfb57915178eb5687eaa0bf spark-wallet-0.3.1-win-portable.exe 10 | 992aad31bf7a755ff7c6390093aeb6ddd63eede6e280c70ad303a3a002672c4e spark-wallet-0.3.1-win-setup.exe 11 | bf78836f45cf3b23ee41ca63d4dc206ea3874d54dc50a8b13726350c92538269 spark-wallet-0.3.1-mac-x64.zip 12 | 05b22a4cb2f0b3fd5949b77f01f4a9aad97556cf5a0eb88965c27ec26cda4155 spark-wallet-0.3.1-android-debug.apk 13 | 63de86d5dfacb8632edabd29fcf6503e378bc996ee178bb77354a76f80ad2ef5 spark-wallet-0.3.1-android-release.apk 14 | 0b8d3baf22260078d7f845b1c4224519c3b5f83ede559a92973210ff0cab890d spark-wallet-docker-0.3.1-amd64 15 | 7fa379525232b60a37b57b65e1f2b254537b1983f45e8e3a3a598bd7b399be68 spark-wallet-docker-0.3.1-standalone-amd64 16 | 6dbf6660dbf339b2cb7cec8003f146501c80e1eae6ee8b8248d4c7ae27b2b078 spark-wallet-docker-0.3.1-standalone-arm32v7 17 | 2c565c4929b9d68f1a8e5efde84f1a1645acbe07eb29488ea33bdfc13b670007 spark-wallet-docker-0.3.1-standalone-arm64v8 18 | -----BEGIN PGP SIGNATURE----- 19 | 20 | iQEzBAEBCAAdFiEE/PGbZ4ZlYvCKQ6rWgfYQTNDxUPwFAmGJL/sACgkQgfYQTNDx 21 | UPy1NAgAt3vFNBSqP7Bl9PNPJr3sWAObqIpsGorvonmIoxp1YIBHKef6bT2iboJe 22 | 8n1r2u45dzg6Yw0Y/Du/JQ4CQYuIZGz+z3qilezgKrS/IbBbpo6XIcpVhRbD8x6v 23 | gIj4afsfG9FmbgIKTH9g5gqtgzciKuXdp2meZT1XZ9esHaVCdvQmgpslmtpm20/0 24 | rn0rr8+47VAqrHWzas2m1OE5yr5sJmJoiYXi43u1EBIva7i/AWV0eHtrYP0NbHad 25 | G1kT9HUiBpUPp+FC4rfbERihb7v56LMMESVkYCopLAwLij2X142j6icUuDMLb9qF 26 | wp+dDJVFipeG0V2iL+YLHpJ07cFTeg== 27 | =ctMI 28 | -----END PGP SIGNATURE----- 29 | -------------------------------------------------------------------------------- /arm32v7.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16-slim as builder 2 | 3 | ARG DEVELOPER 4 | ENV STANDALONE=1 5 | 6 | # Install build c-lightning for third-party packages (c-lightning/bitcoind) 7 | RUN apt-get update && apt-get install -y --no-install-recommends git wget ca-certificates \ 8 | qemu qemu-user-static qemu-user binfmt-support 9 | 10 | RUN npm config set unsafe-perm true 11 | 12 | # Install tini 13 | RUN mkdir /opt/bin && wget -qO /opt/bin/tini "https://github.com/krallin/tini/releases/download/v0.18.0/tini-armhf" \ 14 | && echo "01b54b934d5f5deb32aa4eb4b0f71d0e76324f4f0237cc262d59376bf2bdc269 /opt/bin/tini" | sha256sum -c - \ 15 | && chmod +x /opt/bin/tini 16 | 17 | # Install Spark 18 | WORKDIR /opt/spark/client 19 | COPY client/package.json client/npm-shrinkwrap.json ./ 20 | COPY client/fonts ./fonts 21 | RUN npm install 22 | 23 | WORKDIR /opt/spark 24 | COPY package.json npm-shrinkwrap.json ./ 25 | RUN npm install 26 | COPY . . 27 | 28 | # Build production NPM package 29 | RUN npm run dist:npm \ 30 | && npm prune --production \ 31 | && find . -mindepth 1 -maxdepth 1 \ 32 | ! -name '*.json' ! -name dist ! -name LICENSE ! -name node_modules ! -name scripts \ 33 | -exec rm -r "{}" \; 34 | 35 | # Prepare final image 36 | 37 | FROM arm32v7/node:12.16-slim 38 | 39 | ENV STANDALONE=1 40 | 41 | WORKDIR /opt/spark 42 | COPY --from=builder /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static 43 | RUN apt-get update && apt-get install -y --no-install-recommends xz-utils inotify-tools netcat-openbsd \ 44 | && rm -rf /var/lib/apt/lists/* \ 45 | && ln -s /opt/spark/dist/cli.js /usr/bin/spark-wallet \ 46 | && mkdir /data \ 47 | && ln -s /data/lightning $HOME/.lightning 48 | 49 | COPY --from=builder /opt/bin /usr/bin 50 | COPY --from=builder /opt/spark /opt/spark 51 | 52 | ENV CONFIG=/data/spark/config TLS_PATH=/data/spark/tls TOR_PATH=/data/spark/tor COOKIE_FILE=/data/spark/cookie HOST=0.0.0.0 53 | 54 | # link the granax (Tor Control client) node_modules installation directory 55 | # inside /data/spark/tor/, to persist the Tor Bundle download in the user-mounted volume 56 | RUN ln -s $TOR_PATH/tor-installation/node_modules dist/transport/granax-dep/node_modules 57 | 58 | VOLUME /data 59 | ENTRYPOINT [ "tini", "-g", "--", "scripts/docker-entrypoint.sh" ] 60 | 61 | EXPOSE 9737 62 | -------------------------------------------------------------------------------- /arm64v8.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16-slim as builder 2 | 3 | ARG DEVELOPER 4 | ENV STANDALONE=1 5 | 6 | # Install build c-lightning for third-party packages (c-lightning/bitcoind) 7 | RUN apt-get update && apt-get install -y --no-install-recommends git wget ca-certificates \ 8 | qemu qemu-user-static qemu-user binfmt-support 9 | 10 | RUN npm config set unsafe-perm true 11 | 12 | # Install tini 13 | RUN mkdir /opt/bin && wget -qO /opt/bin/tini "https://github.com/krallin/tini/releases/download/v0.18.0/tini-arm64" \ 14 | && echo "7c5463f55393985ee22357d976758aaaecd08defb3c5294d353732018169b019 /opt/bin/tini" | sha256sum -c - \ 15 | && chmod +x /opt/bin/tini 16 | 17 | # Install Spark 18 | WORKDIR /opt/spark/client 19 | COPY client/package.json client/npm-shrinkwrap.json ./ 20 | COPY client/fonts ./fonts 21 | RUN npm install 22 | 23 | WORKDIR /opt/spark 24 | COPY package.json npm-shrinkwrap.json ./ 25 | RUN npm install 26 | COPY . . 27 | 28 | # Build production NPM package 29 | RUN npm run dist:npm \ 30 | && npm prune --production \ 31 | && find . -mindepth 1 -maxdepth 1 \ 32 | ! -name '*.json' ! -name dist ! -name LICENSE ! -name node_modules ! -name scripts \ 33 | -exec rm -r "{}" \; 34 | 35 | # Prepare final image 36 | 37 | FROM arm64v8/node:12.16-slim 38 | 39 | ENV STANDALONE=1 40 | 41 | WORKDIR /opt/spark 42 | COPY --from=builder /usr/bin/qemu-aarch64-static /usr/bin/qemu-aarch64-static 43 | RUN apt-get update && apt-get install -y --no-install-recommends xz-utils inotify-tools netcat-openbsd \ 44 | && rm -rf /var/lib/apt/lists/* \ 45 | && ln -s /opt/spark/dist/cli.js /usr/bin/spark-wallet \ 46 | && mkdir /data \ 47 | && ln -s /data/lightning $HOME/.lightning 48 | 49 | COPY --from=builder /opt/bin /usr/bin 50 | COPY --from=builder /opt/spark /opt/spark 51 | 52 | ENV CONFIG=/data/spark/config TLS_PATH=/data/spark/tls TOR_PATH=/data/spark/tor COOKIE_FILE=/data/spark/cookie HOST=0.0.0.0 53 | 54 | # link the granax (Tor Control client) node_modules installation directory 55 | # inside /data/spark/tor/, to persist the Tor Bundle download in the user-mounted volume 56 | RUN ln -s $TOR_PATH/tor-installation/node_modules dist/transport/granax-dep/node_modules 57 | 58 | VOLUME /data 59 | ENTRYPOINT [ "tini", "-g", "--", "scripts/docker-entrypoint.sh" ] 60 | 61 | EXPOSE 9737 62 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/env"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/env"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeo pipefail 3 | shopt -s extglob 4 | 5 | : ${DEST:=dist} 6 | : ${NODE_ENV:=production} 7 | : ${BUILD_TARGET:=web} 8 | 9 | export BUILD_TARGET 10 | export NODE_ENV 11 | export VERSION=`node -p 'require("../package").version'` 12 | 13 | rm -rf $DEST/* 14 | mkdir -p $DEST $DEST/lib $DEST/fonts $DEST/swatch 15 | 16 | [[ -d node_modules ]] || NODE_ENV=development npm install 17 | 18 | # Static assets 19 | cp -r www/* $DEST/ 20 | cp -r fonts/node_modules/typeface-* $DEST/fonts/ 21 | cp -r node_modules/bootswatch/dist/!(darkly|litera|minty|sketchy|journal|pulse) $DEST/swatch/ 22 | cp -r swatch/*/ $DEST/swatch/ 23 | find $DEST/swatch -type f ! -name '*.min.css' -delete 24 | find $DEST/fonts -type f -name "*.md" -delete 25 | find $DEST/fonts -type f -name "*.json" -delete 26 | 27 | ./fonts/rewrite-css.sh $DEST/swatch/*/*.css 28 | 29 | if [[ "$BUILD_TARGET" == "web" ]] || [[ "$BUILD_TARGET" == "electron" ]]; then 30 | cp node_modules/instascan/dist/instascan.min.js $DEST/lib/instascan.js 31 | fi 32 | 33 | if [[ "$BUILD_TARGET" != "web" ]]; then 34 | rm -r $DEST/manifest 35 | fi 36 | 37 | # Transpile pug and stylus 38 | pug index.pug -o $DEST 39 | stylus -u nib -c styl/style.styl -o $DEST 40 | 41 | if [[ "$BUILD_TARGET" == "web" ]]; then 42 | stylus -u nib -c styl/noscript.styl -o $DEST 43 | fi 44 | 45 | # Browserify bundles 46 | 47 | bundle() { 48 | browserify $BROWSERIFY_OPT $1 \ 49 | | ( [[ "$NODE_ENV" != "development" ]] && terser --compress warnings=false --mangle || cat ) 50 | } 51 | 52 | # Primary wallet application bundle 53 | bundle src/app.js > $DEST/app.js 54 | 55 | # Theme loader 56 | bundle src/load-theme.js > $DEST/load-theme.js 57 | 58 | # Service worker 59 | if [[ "$BUILD_TARGET" == "web" ]]; then 60 | bundle src/worker.js > $DEST/worker.js 61 | fi 62 | 63 | # Settings page for Cordova/Electron 64 | if [[ "$BUILD_TARGET" == "cordova" ]] || [[ "$BUILD_TARGET" == "electron" ]]; then 65 | bundle src/server-settings.js > $DEST/settings.js 66 | pug -O '{"bundle":"settings.js"}' < index.pug > $DEST/settings.html 67 | fi 68 | -------------------------------------------------------------------------------- /client/fonts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /client/fonts/install-fonts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Download fonts via https://github.com/KyleAMathews/typefaces 4 | 5 | ./list-fonts.sh | while read font; do 6 | pkg=typeface-$font 7 | if [[ "`npm owner ls $pkg`" == "kylemathews " ]]; then 8 | npm install typeface-$font@latest 9 | else 10 | echo "[ERR] $pkg has unexpected owner" 11 | fi 12 | done 13 | -------------------------------------------------------------------------------- /client/fonts/list-fonts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Extract the list of web fonts used by all the themes 4 | 5 | cat ../node_modules/bootswatch/dist/*/*.scss | grep 'web-font-path:' | 6 | cut -d= -f2 | cut -d'"' -f1 | # extract "family" arg" 7 | tr '|' '\n' | cut -d: -f1 | # split multiple fonts, remove sub-styles 8 | sort -u | # drop dups 9 | tr '+' '-' | tr '[:upper:]' '[:lower:]' | # normalize to package-style names 10 | sed 's/[^a-z0-9-]//g' # remove unexpected characters, so we don't get anything funny 11 | 12 | -------------------------------------------------------------------------------- /client/fonts/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark-fonts", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "spark-fonts", 8 | "license": "MIT", 9 | "dependencies": { 10 | "typeface-lato": "^1.1.13", 11 | "typeface-nunito-sans": "^1.1.13", 12 | "typeface-open-sans": "^1.1.13", 13 | "typeface-roboto": "^1.1.13", 14 | "typeface-source-sans-pro": "^1.1.5", 15 | "typeface-ubuntu": "^1.1.13" 16 | } 17 | }, 18 | "node_modules/typeface-lato": { 19 | "version": "1.1.13", 20 | "resolved": "https://registry.npmjs.org/typeface-lato/-/typeface-lato-1.1.13.tgz", 21 | "integrity": "sha512-sTn5k3+fagGOi8FQfN2MPeUiTdvG0Z3LVqCaQdsg2sYa0fzNteFZussizdwiPxF45OoFN3zdU/ci+PtjolNSPQ==" 22 | }, 23 | "node_modules/typeface-nunito-sans": { 24 | "version": "1.1.13", 25 | "resolved": "https://registry.npmjs.org/typeface-nunito-sans/-/typeface-nunito-sans-1.1.13.tgz", 26 | "integrity": "sha512-wpRTCwHdHcq4CKBBGzTDNAjck64O1prpCjKiUJyMFHn+E9k1aELJuSVeMBsgScdR1b5u7HFP1Px7Gi2OUMu33w==" 27 | }, 28 | "node_modules/typeface-open-sans": { 29 | "version": "1.1.13", 30 | "resolved": "https://registry.npmjs.org/typeface-open-sans/-/typeface-open-sans-1.1.13.tgz", 31 | "integrity": "sha512-lVGVHvYl7UJDFB9vN8r7NHw3sVm7Rjeow6b9AeABc/J+2mDaCkmcdVtw3QZnsJW39P+xm5zeggIj9gLHYGn9Iw==" 32 | }, 33 | "node_modules/typeface-roboto": { 34 | "version": "1.1.13", 35 | "resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-1.1.13.tgz", 36 | "integrity": "sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==" 37 | }, 38 | "node_modules/typeface-source-sans-pro": { 39 | "version": "1.1.13", 40 | "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-1.1.13.tgz", 41 | "integrity": "sha512-eCczLh0FYByjVoMxZVDfSSTI8A6qJOBJftgWBVJL63AuemQTTvMU8DEk6ud/TQhkbIwWZ3A7ll/NfS9yI0lIrQ==" 42 | }, 43 | "node_modules/typeface-ubuntu": { 44 | "version": "1.1.13", 45 | "resolved": "https://registry.npmjs.org/typeface-ubuntu/-/typeface-ubuntu-1.1.13.tgz", 46 | "integrity": "sha512-Rg1WdJd8fXr2nKF4/UqFw4Y7IKhomEBI7z9ZEyRf0YRV6ue+OipamCNEIEyhXZlhymde0RQrWMmagbyeyOQwSA==" 47 | } 48 | }, 49 | "dependencies": { 50 | "typeface-lato": { 51 | "version": "1.1.13", 52 | "resolved": "https://registry.npmjs.org/typeface-lato/-/typeface-lato-1.1.13.tgz", 53 | "integrity": "sha512-sTn5k3+fagGOi8FQfN2MPeUiTdvG0Z3LVqCaQdsg2sYa0fzNteFZussizdwiPxF45OoFN3zdU/ci+PtjolNSPQ==" 54 | }, 55 | "typeface-nunito-sans": { 56 | "version": "1.1.13", 57 | "resolved": "https://registry.npmjs.org/typeface-nunito-sans/-/typeface-nunito-sans-1.1.13.tgz", 58 | "integrity": "sha512-wpRTCwHdHcq4CKBBGzTDNAjck64O1prpCjKiUJyMFHn+E9k1aELJuSVeMBsgScdR1b5u7HFP1Px7Gi2OUMu33w==" 59 | }, 60 | "typeface-open-sans": { 61 | "version": "1.1.13", 62 | "resolved": "https://registry.npmjs.org/typeface-open-sans/-/typeface-open-sans-1.1.13.tgz", 63 | "integrity": "sha512-lVGVHvYl7UJDFB9vN8r7NHw3sVm7Rjeow6b9AeABc/J+2mDaCkmcdVtw3QZnsJW39P+xm5zeggIj9gLHYGn9Iw==" 64 | }, 65 | "typeface-roboto": { 66 | "version": "1.1.13", 67 | "resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-1.1.13.tgz", 68 | "integrity": "sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==" 69 | }, 70 | "typeface-source-sans-pro": { 71 | "version": "1.1.13", 72 | "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-1.1.13.tgz", 73 | "integrity": "sha512-eCczLh0FYByjVoMxZVDfSSTI8A6qJOBJftgWBVJL63AuemQTTvMU8DEk6ud/TQhkbIwWZ3A7ll/NfS9yI0lIrQ==" 74 | }, 75 | "typeface-ubuntu": { 76 | "version": "1.1.13", 77 | "resolved": "https://registry.npmjs.org/typeface-ubuntu/-/typeface-ubuntu-1.1.13.tgz", 78 | "integrity": "sha512-Rg1WdJd8fXr2nKF4/UqFw4Y7IKhomEBI7z9ZEyRf0YRV6ue+OipamCNEIEyhXZlhymde0RQrWMmagbyeyOQwSA==" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client/fonts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark-fonts", 3 | "private": true, 4 | "author": "Nadav Ivgi", 5 | "license": "MIT", 6 | "dependencies": { 7 | "typeface-lato": "^1.1.13", 8 | "typeface-nunito-sans": "^1.1.13", 9 | "typeface-open-sans": "^1.1.13", 10 | "typeface-roboto": "^1.1.13", 11 | "typeface-source-sans-pro": "^1.1.5", 12 | "typeface-ubuntu": "^1.1.13" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/fonts/rewrite-css.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Rewrite @import statements in CSS themes to use local fonts, 4 | # instead of loading them from Google. 5 | 6 | perl -i -pe ' 7 | s/\@import url\("https:\/\/fonts\.googleapis\.com\/css\?family=([^"]+)"\);/ 8 | join "", map { 9 | s\/:.*\/\/; s\/\+\/-\/g; 10 | "\@import url(..\/..\/fonts\/typeface-".lc($_)."\/index.css);" 11 | } split "\\|", $1 12 | /ge' $@ 13 | -------------------------------------------------------------------------------- /client/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | title Spark 4 | 5 | meta(charset='utf-8') 6 | meta(name='viewport', content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover') 7 | link(rel='stylesheet', href='style.css') 8 | meta(name='theme-color', content='#375a7f') 9 | 10 | if process.env.BUILD_TARGET == 'web' 11 | meta(name='access-key', content=(settings ? settings.accessKey : '{{accessKey}}')) 12 | link(rel='manifest', href='manifest-'+(settings ? settings.manifestKey : '{{manifestKey}}')+'/manifest.json') 13 | 14 | if process.env.BUILD_TARGET != 'web' 15 | //- in web environment, this is set using an HTTP header instead of a meta tag 16 | //- unsafe-eval is only required for instascan, which is not used in cordova builds 17 | - unsafeEval = process.env.BUILD_TARGET !== 'cordova' ? "script-src 'self' 'unsafe-eval'; " : '' 18 | meta(http-equiv='Content-Security-Policy', content=unsafeEval+"default-src 'self' gap:; font-src 'self' data:; img-src 'self' data:; connect-src *") 19 | 20 | body(class='build-'+process.env.BUILD_TARGET) 21 | 22 | #app: .loading 23 | nav.navbar.navbar-dark.bg-primary.mb-3: .container: span.navbar-brand Spark 24 | .loader.fixed 25 | 26 | script(src='load-theme.js') 27 | 28 | script(src=bundle || 'app.js', defer) 29 | 30 | if process.env.BUILD_TARGET == 'cordova' 31 | script(src='cordova.js', defer) 32 | 33 | if process.env.BUILD_TARGET == 'web' 34 | noscript 35 | link(rel='stylesheet', href='noscript.css') 36 | p.noscript-msg Spark requires JavaScript to be turned on. 37 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark-client", 3 | "private": true, 4 | "scripts": { 5 | "dist": "./build.sh", 6 | "postinstall": "(cd fonts && npm install) && ./fonts/rewrite-css.sh node_modules/bootswatch/dist/*/*.min.css" 7 | }, 8 | "author": "Nadav Ivgi", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@babel/polyfill": "^7.12.1", 12 | "@cycle/dom": "^23.0.0", 13 | "@cycle/history": "^8.0.0", 14 | "@cycle/http": "^15.4.0", 15 | "@cycle/rxjs-run": "^10.5.0", 16 | "@cycle/storage": "^5.1.2", 17 | "big.js": "^6.1.1", 18 | "bootswatch": "^4.1.3", 19 | "form-serialize": "^0.7.2", 20 | "instascan": "github:shesek/instascan#packaged-lib", 21 | "js-yaml": "^4.1.0", 22 | "nanoid": "^3.1.25", 23 | "numbro": "^2.3.5", 24 | "path-to-regexp": "^6.2.0", 25 | "pwacompat": "^2.0.17", 26 | "qrcode": "^1.4.4", 27 | "rxjs": "^6.6.7", 28 | "rxjs-compat": "^6.6.7", 29 | "string-argv": "^0.3.1", 30 | "vague-time": "^2.4.2", 31 | "webrtc-adapter": "^8.1.0", 32 | "xstream": "^11.14.0" 33 | }, 34 | "browserify": { 35 | "transform": [ 36 | "babelify", 37 | "pugify", 38 | "envify", 39 | "uglifyify", 40 | [ 41 | "browserify-package-json", 42 | { 43 | "only": "version" 44 | } 45 | ] 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.15.4", 50 | "@babel/core": "^7.15.4", 51 | "@babel/preset-env": "^7.15.4", 52 | "babelify": "^10.0.0", 53 | "browserify": "^17.0.0", 54 | "browserify-middleware": "^8.1.1", 55 | "browserify-package-json": "^1.0.1", 56 | "envify": "^4.1.0", 57 | "nib": "^1.1.2", 58 | "pug": "^3.0.2", 59 | "pug-cli": "^1.0.0-alpha6", 60 | "pugify": "^2.2.0", 61 | "stylus": "^0.54.8", 62 | "terser": "^5.7.2", 63 | "uglifyify": "^5.0.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/serve.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import nib from 'nib' 4 | import stylus from 'stylus' 5 | import express from 'express' 6 | import browserify from 'browserify-middleware' 7 | 8 | const compileStyl = (str, filename) => stylus(str).set('filename', filename).use(nib()) 9 | , bswatchPath = path.resolve(require.resolve('bootswatch/package'), '..', 'dist') 10 | , scanPath = require.resolve('instascan/dist/instascan.min.js') 11 | , rpath = p => path.join(__dirname, p) 12 | 13 | process.env.BUILD_TARGET = 'web' 14 | process.env.VERSION = require('../package').version 15 | 16 | module.exports = app => { 17 | 18 | app.engine('pug', require('pug').__express) 19 | 20 | app.get('/', (req, res) => res.render(rpath('index.pug'))) 21 | 22 | app.get('/app.js', browserify(rpath('src/app.js'))) 23 | app.get('/worker.js', browserify(rpath('src/worker.js'))) 24 | app.get('/load-theme.js', browserify(rpath('src/load-theme.js'))) 25 | 26 | app.get('/*.css', stylus.middleware({ src: rpath('styl'), dest: rpath('www'), compile: compileStyl })) 27 | app.get('/lib/instascan.js', (req, res) => res.sendFile(scanPath)) 28 | 29 | app.use('/', express.static(rpath('www'))) 30 | app.use('/swatch', express.static(bswatchPath), express.static(rpath('swatch'))) 31 | app.use('/fonts', express.static(rpath('fonts/node_modules'))) 32 | } 33 | -------------------------------------------------------------------------------- /client/src/app.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | import 'webrtc-adapter' 3 | 4 | import run from '@cycle/rxjs-run' 5 | 6 | import storageDriver from '@cycle/storage' 7 | import { makeDOMDriver } from '@cycle/dom' 8 | import { makeHTTPDriver } from '@cycle/http' 9 | import { makeHashHistoryDriver, captureClicks } from '@cycle/history' 10 | 11 | import makeSSEDriver from './driver/sse' 12 | import makeRouteDriver from './driver/route' 13 | import makeConfDriver from './driver/conf' 14 | import orientDriver from './driver/screen-orient' 15 | 16 | import { Observable as O } from './rxjs' 17 | import { dbg } from './util' 18 | 19 | import intent from './intent' 20 | import model from './model' 21 | import view from './view' 22 | import rpc from './rpc' 23 | 24 | if (process.env.BUILD_TARGET === 'web') { 25 | require('pwacompat') 26 | } 27 | 28 | // Send Cordova/Electron users directly to server settings if there are none 29 | if (process.env.BUILD_TARGET !== 'web' && !localStorage.serverInfo) { 30 | location.href = 'settings.html' // @xxx side-effects outside of drivers 31 | throw new Error('Missing server settings, redirecting') 32 | } 33 | 34 | // Get cyclejs to use rxjs-compat-enabled streams 35 | require("@cycle/run/lib/adapt").setAdapt(stream$ => O.from(stream$)) 36 | 37 | const serverInfo = process.env.BUILD_TARGET === 'web' 38 | ? { serverUrl: '.', accessKey: document.querySelector('[name=access-key]').content } 39 | : JSON.parse(localStorage.serverInfo) 40 | 41 | const main = ({ DOM, HTTP, SSE, route, conf$, scan$, urihandler$ }) => { 42 | 43 | const resps = rpc.parseRes({ HTTP, SSE }) 44 | , actions = intent({ DOM, route, conf$, scan$, urihandler$, ...resps }) 45 | 46 | , state$ = model({ HTTP, ...actions, ...resps }) 47 | 48 | , rpc$ = rpc.makeReq(actions) 49 | , vdom$ = view.vdom({ state$, ...actions, ...resps }) 50 | , navto$ = view.navto({ ...resps, ...actions }) 51 | , notif$ = view.notif({ state$, ...resps }) 52 | , orient$ = view.orient(actions.page$) 53 | , scanner$ = view.scanner(actions) 54 | 55 | dbg({ conf$, scan$, urihandler$ }, 'spark:source') 56 | dbg(actions, 'spark:intent') 57 | dbg(resps, 'spark:rpc') 58 | dbg({ state$ }, 'spark:model') 59 | dbg({ rpc$, vdom$, navto$, notif$, orient$, scanner$ }, 'spark:sinks') 60 | 61 | return { 62 | DOM: vdom$ 63 | , HTTP: rpc.toHttp(serverInfo, rpc$) 64 | , route: navto$ 65 | , conf$: state$.map(s => s.conf) 66 | , scan$: scanner$ 67 | , orient$ 68 | , notif$ 69 | } 70 | } 71 | 72 | run(main, { 73 | DOM: makeDOMDriver('#app') 74 | , SSE: makeSSEDriver(serverInfo) 75 | , HTTP: makeHTTPDriver() 76 | , route: makeRouteDriver(captureClicks(makeHashHistoryDriver())) 77 | 78 | , conf$: makeConfDriver(storageDriver) 79 | , orient$: orientDriver 80 | 81 | , ...( 82 | process.env.BUILD_TARGET == 'cordova' ? { 83 | urihandler$: require('./driver/cordova-urihandler') 84 | , scan$: require('./driver/cordova-qrscanner') 85 | , notif$: require('./driver/cordova-notification') 86 | } 87 | 88 | : process.env.BUILD_TARGET == 'electron' ? { 89 | urihandler$: require('./driver/electron-urihandler') 90 | , scan$: require('./driver/instascan')({ mirror: false, backgroundScan: false }) 91 | , notif$: require('./driver/electron-notification') 92 | } 93 | 94 | : process.env.BUILD_TARGET == 'web' ? { 95 | urihandler$: _ => O.empty() 96 | , scan$: require('./driver/instascan')({ mirror: false, backgroundScan: false }) 97 | , notif$: require('./driver/web-notification') 98 | } 99 | 100 | : {}) 101 | }) 102 | 103 | if (process.env.BUILD_TARGET == 'web' && navigator.serviceWorker) 104 | window.addEventListener('load', _ => navigator.serviceWorker.register('worker.js')) 105 | 106 | -------------------------------------------------------------------------------- /client/src/driver/conf.js: -------------------------------------------------------------------------------- 1 | import dropRepeats from 'xstream/extra/dropRepeats' 2 | import { dbg } from '../util' 3 | 4 | // @XXX the code below mixes rxjs and xstream observables together, normalize them? 5 | // (cyclejs provides conf$ as an xstream, while the storage driver returns rxjs streams) 6 | 7 | module.exports = storage => conf$ => 8 | storage(conf$.map(JSON.stringify).compose(dropRepeats()).map(conf => ({ key: 'conf', value: conf }))) 9 | .local.getItem('conf').distinctUntilChanged().map(JSON.parse).map(conf => conf || {}).shareReplay(1) 10 | -------------------------------------------------------------------------------- /client/src/driver/cordova-notification.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | let plugin, enabled=false, isActive=true 4 | 5 | document.addEventListener('deviceready', _ => { 6 | plugin = cordova.plugins.notification.local 7 | 8 | plugin.hasPermission(granted => 9 | enabled = granted || plugin.registerPermission(granted => enabled = granted)) 10 | 11 | document.addEventListener('pause', _ => isActive = false, false) 12 | document.addEventListener('resume', _ => isActive = true, false) 13 | 14 | }, false) 15 | 16 | function display(msg) { 17 | if (enabled && !isActive) 18 | plugin.schedule({ title: 'Spark', text: msg, foreground: true, vibrate: true }) 19 | } 20 | 21 | module.exports = msg$ => ( 22 | O.from(msg$).subscribe(display) 23 | , O.empty() 24 | ) 25 | -------------------------------------------------------------------------------- /client/src/driver/cordova-qrscanner.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import { Observable as O } from '../rxjs' 3 | 4 | const em = new EventEmitter 5 | , scan$ = O.fromEvent(em, 'scan').share() 6 | 7 | function handleScan(err, contents) { 8 | if (err && err.code == 6) em.emit('cancel') 9 | else if (err) em.emit('error', err) 10 | else em.emit('scan', contents) 11 | } 12 | 13 | function startScan() { 14 | document.body.classList.add('qr-scanning') 15 | QRScanner.scan(handleScan) 16 | QRScanner.show() 17 | } 18 | 19 | function stopScan() { 20 | document.body.classList.remove('qr-scanning') 21 | QRScanner.destroy() 22 | } 23 | 24 | 25 | // Receives a stream of scan start/stop requests, 26 | // returns a stream of scanned QR texts 27 | function scanDriver(_mode$) { 28 | O.from(_mode$) 29 | .filter(_ => !!window.QRScanner) // skip requests if QRScanner is not yet loaded 30 | .distinctUntilChanged() 31 | .subscribe(mode => mode ? startScan() : stopScan()) 32 | 33 | // @todo destroy() QR scanner after some time of no activity 34 | 35 | return scan$ 36 | } 37 | 38 | module.exports = scanDriver 39 | -------------------------------------------------------------------------------- /client/src/driver/cordova-urihandler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import { Observable as O } from '../rxjs' 3 | 4 | const em = new EventEmitter 5 | 6 | window.handleOpenURL = url => em.emit('handle', url) 7 | 8 | window.addEventListener('deviceready', _ => 9 | window.plugins.launchmyapp.getLastIntent(url => em.emit('handle', url)) 10 | , false) 11 | 12 | const urihandler$ = O.fromEvent(em, 'handle') 13 | .windowTime(1000).switchMap(u$ => u$.distinctUntilChanged()) // ignore repetitions in 1s window 14 | .shareReplay(1) 15 | 16 | module.exports = _ => urihandler$ 17 | -------------------------------------------------------------------------------- /client/src/driver/electron-ipc.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | const { ipcRenderer } = window 4 | 5 | ipcRenderer.addListener('serverError', err => console.error('Spark server error:', err)) 6 | 7 | module.exports = cmd$ => ( 8 | O.from(cmd$).subscribe(cmd => ipcRenderer.send(...cmd)) 9 | , topic => O.fromEvent(ipcRenderer, topic, (e, arg) => arg).share() 10 | ) 11 | -------------------------------------------------------------------------------- /client/src/driver/electron-notification.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | function display(msg) { 4 | if (!document.hasFocus()) { 5 | const notif = new Notification('Spark', { body: msg, tag: 'spark-msg', icon: 'notification.png' }) 6 | notif.onclick = _ => window.focus() 7 | } 8 | } 9 | 10 | module.exports = msg$ => ( 11 | O.from(msg$).subscribe(display) 12 | , O.empty() 13 | ) 14 | -------------------------------------------------------------------------------- /client/src/driver/electron-urihandler.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | const { ipcRenderer } = window 4 | 5 | // handle-uri events are dispatched to us by the main process 6 | const urihandler$ = O.fromEvent(ipcRenderer, 'handle-uri', (e, uri) => uri) 7 | .windowTime(1000).switchMap(u$ => u$.distinctUntilChanged()) // ignore repetitions in 1s window 8 | .shareReplay(1) 9 | 10 | module.exports = _ => urihandler$ 11 | -------------------------------------------------------------------------------- /client/src/driver/instascan.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | // check for WebRTC camera support 4 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 5 | 6 | // load instascan.js on demand, only if needed 7 | const script = document.createElement('script') 8 | script.src = 'lib/instascan.js' 9 | document.body.appendChild(script) 10 | 11 | const Instascan$ = O.fromEvent(script, 'load').map(_ => window.Instascan).share() 12 | , Scanner$ = Instascan$.map(Instascan => Instascan.Scanner) 13 | , Camera$ = Instascan$.map(Instascan => Instascan.Camera) 14 | 15 | const makeScanDriver = (opt={}) => { 16 | const video = document.createElement('video') 17 | , scanner$ = Scanner$.map(Scanner => new Scanner({ ...opt, video })).shareReplay(1) 18 | , active$ = scanner$.flatMap(scanner => O.fromEvent(scanner, 'active')).share() 19 | , scan$ = scanner$.flatMap(scanner => O.fromEvent(scanner, 'scan')) 20 | .map(x => Array.isArray(x) ? x[0] : x) 21 | .share() 22 | 23 | video.className = 'qr-video' 24 | document.body.appendChild(video) 25 | 26 | function startScan(Camera, scanner) { 27 | Camera.getCameras().then(pickCam).then(cam => { 28 | document.body.classList.add('qr-scanning') 29 | scanner.start(cam) 30 | }) 31 | } 32 | 33 | function stopScan(scanner) { 34 | document.body.classList.remove('qr-scanning') 35 | scanner.stop() 36 | } 37 | 38 | return _mode$ => { 39 | const mode$ = O.from(_mode$) 40 | 41 | // start/stop scanner according to mode$ 42 | O.combineLatest(mode$, Camera$, scanner$).subscribe(([ mode, Camera, scanner ]) => 43 | mode ? startScan(Camera, scanner) : stopScan(scanner)) 44 | 45 | // if the scanner becomes active while mode$ is off, turn it off again 46 | // without this, starting the scanner then quickly stopping it before it fully initialized could get it stuck on screen 47 | active$.withLatestFrom(mode$, scanner$) 48 | .subscribe(([ active, mode, scanner ]) => (!mode && setTimeout(_ => scanner.stop(), 100))) 49 | 50 | return scan$ 51 | } 52 | } 53 | 54 | const pickCam = cams => 55 | cams.find(cam => cam.name && cam.name.includes('back')) 56 | || cams[0] 57 | 58 | module.exports = makeScanDriver 59 | } 60 | 61 | else { 62 | // if we don't have WebTC camera support, return a driver that 63 | // redirects the user to /payreq for manually pasting the bolt11 string 64 | module.exports = _ => _mode$ => ( 65 | O.from(_mode$).subscribe(mode => mode && (location.hash = '/payreq')) 66 | , O.empty() 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /client/src/driver/route.js: -------------------------------------------------------------------------------- 1 | import { pathToRegexp } from 'path-to-regexp' 2 | import { Observable as O } from '../rxjs' 3 | 4 | const isStr = x => typeof x === 'string' 5 | 6 | module.exports = history => goto$ => { 7 | const history$ = O.from(history( 8 | // default the `type` to push, but allow to override it 9 | goto$.map(goto => ({ type: 'push', ...goto })) 10 | )) 11 | 12 | return (path, re=path && pathToRegexp (path)) => 13 | path ? history$.map(loc => ({ ...loc, params: loc.pathname.match(re) })).filter(loc => !!loc.params) 14 | : history$ 15 | } 16 | -------------------------------------------------------------------------------- /client/src/driver/screen-orient.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | module.exports = _orient$ => { 4 | 5 | O.from(_orient$).distinctUntilChanged().subscribe(orient => { 6 | const screenOrient = window.screen && screen.orientation 7 | if (!screenOrient || !screenOrient.lock) return; 8 | 9 | orient == 'unlock' ? screenOrient.unlock() 10 | : screenOrient.lock(orient).catch(_ => null) 11 | }) 12 | 13 | return O.empty() 14 | } 15 | -------------------------------------------------------------------------------- /client/src/driver/sse.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | import { Observable as O } from '../rxjs' 3 | 4 | module.exports = serverInfo => { 5 | const srcUrl = url.resolve(serverInfo.serverUrl, `stream?access-key=${serverInfo.accessKey}`) 6 | 7 | const es = new EventSource(srcUrl) 8 | return _ => (ev='message') => O.fromEvent(es, ev).map(r => r.data).filter(Boolean).map(JSON.parse).share() 9 | } 10 | -------------------------------------------------------------------------------- /client/src/driver/web-notification.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | // HTML5 based system notifications for desktop and mobile (non-Cordova/Electron builds). 4 | // this only works if we have an open tab. on mobile chrome, this also requires chrome 5 | // to be active (but possibly on a different tab). real background notifications require 6 | // using the Web Push API and routing notifications over Google's/Mozilla's servers, 7 | // which is not implemented but might be in a future release. 8 | 9 | if (!window.Notification || !navigator.serviceWorker) { 10 | module.exports = _ => O.empty() 11 | } else { 12 | 13 | if (Notification.permission !== 'granted') { 14 | // don't overwhelm the user with prompts immediately, wait a bit first 15 | setTimeout(_ => Notification.requestPermission(), 15000) 16 | } 17 | 18 | let worker 19 | navigator.serviceWorker.ready.then(reg => worker = reg) 20 | 21 | function display(msg) { 22 | if (worker && !document.hasFocus() && Notification.permission === 'granted') 23 | worker.showNotification('Spark', { body: msg, tag: 'spark-msg', icon: 'notification.png' }) 24 | } 25 | 26 | module.exports = msg$ => ( 27 | O.from(msg$).subscribe(display) 28 | , O.empty() 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /client/src/intent.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from './rxjs' 2 | import serialize from 'form-serialize' 3 | import { nanoid } from 'nanoid' 4 | import { dbg, parseUri, parseRpcCmd } from './util' 5 | 6 | module.exports = ({ DOM, route, conf$, scan$, urihandler$, offerInv$ }) => { 7 | const 8 | on = (sel, ev, pd=false) => DOM.select(sel).events(ev, { preventDefault: pd }) 9 | , click = sel => on(sel, 'click') 10 | , submit = sel => on(sel, 'submit', true).map(e => ({ ...e.target.dataset, ...serialize(e.target, { hash: true }) })) 11 | 12 | // Page routes 13 | , page$ = route() 14 | , goHome$ = route('/') 15 | , goScan$ = route('/scan') 16 | , goSend$ = route('/payreq') 17 | , goRecv$ = route('/recv') 18 | , goNode$ = route('/node') 19 | , goLogs$ = route('/logs').merge(click('[do=refresh-logs]')) 20 | , goRpc$ = route('/rpc') 21 | 22 | , goChan$ = route('/channels') 23 | , goNewChan$ = route('/channels/new') 24 | , goDeposit$ = route('/deposit') 25 | 26 | // Display and confirm payment requests (from QR, lightning: URIs and manual entry) 27 | , viewPay$ = O.merge(scan$, urihandler$).map(parseUri).filter(x => !!x) 28 | .merge(submit('[do=decode-pay]').map(r => r.paystr.trim()).map(paystr => parseUri(paystr) || paystr)) 29 | , confPay$ = submit('[do=confirm-pay]') 30 | // Automatically pay the offer's invoice when there are no changes 31 | .merge(offerInv$.filter(inv => !Object.keys(inv.changes).length)) 32 | 33 | // RPC console actions 34 | , clrHist$ = click('[do=clear-console-history]') 35 | , execRpc$ = submit('[do=exec-rpc]').map(r => parseRpcCmd(r.cmd)) 36 | .merge(click('[do=rpc-help]').mapTo([ 'help' ])) 37 | 38 | // New invoice/offer action 39 | , newInv$ = submit('[do=new-invoice]').map(r => ({ 40 | label: nanoid() 41 | , msatoshi: r.msatoshi || 'any' 42 | , description: r.description || '⚡' 43 | , reusable_offer: !!r['reusable-offer'] })) 44 | , invUseOffer$ = on('[do=new-invoice] [name=reusable-offer]', 'input') 45 | .map(e => e.target.checked) 46 | .merge(goRecv$.mapTo(false)) 47 | .startWith(false) 48 | 49 | // Payment amount field, shared for creating new invoices and for paying custom amounts 50 | , amtVal$ = on('[name=amount]', 'input').map(e => e.target.value) 51 | 52 | // Config page and toggle buttons 53 | , togTheme$ = click('.toggle-theme') 54 | , togUnit$ = click('.toggle-unit') 55 | , togExp$ = click('.toggle-exp') 56 | 57 | // Dismiss alert message 58 | , dismiss$ = O.merge(click('[data-dismiss=alert], a.navbar-brand, .content a, .content button') 59 | , page$.filter(p => p.search != '?r')) 60 | 61 | // Payments feed page navigation and click-to-toggle 62 | , feedStart$ = click('[data-feed-start]').map(e => +e.ownerTarget.dataset.feedStart).startWith(0) 63 | , togFeed$ = click('ul.feed [data-feed-toggle]') 64 | .filter(e => e.target.closest('ul').classList.contains('feed')) // ignore clicks inside nested