diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/fedimint-docker.yml b/.github/workflows/fedimint-docker.yml new file mode 100644 index 000000000..6bbfe9983 --- /dev/null +++ b/.github/workflows/fedimint-docker.yml @@ -0,0 +1,57 @@ +name: fedimint-thunderhub + +on: + push: + tags: ['v*'] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + fedimintui/fedimint-thunderhub + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Login to Docker Hub + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@v2 + with: + username: fedimintui + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + file: Dockerfile + push: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Checkout repository content + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + uses: actions/checkout@v4 + + # This workflow requires the repository content to be locally available to read the README + - name: Update the Docker Hub description + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + uses: peter-evans/dockerhub-description@v3 + with: + username: fedimintui + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + repository: fedimintui/fedimint-thunderhub + readme-filepath: ./README.md diff --git a/Dockerfile b/Dockerfile index ed5be7a96..bfe52f361 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,10 @@ ENV BASE_PATH=${BASE_PATH} ARG NODE_ENV="production" ENV NODE_ENV=${NODE_ENV} ENV NEXT_TELEMETRY_DISABLED=1 +ARG FM_GATEWAY_API="" +ENV FM_GATEWAY_API=${FM_GATEWAY_API} +ARG FM_GATEWAY_PASSWORD="" +ENV FM_GATEWAY_PASSWORD=${FM_GATEWAY_PASSWORD} # Build the NestJS and NextJS application COPY . . @@ -51,6 +55,10 @@ ENV BASE_PATH=${BASE_PATH} ARG NODE_ENV="production" ENV NODE_ENV=${NODE_ENV} ENV NEXT_TELEMETRY_DISABLED=1 +ARG FM_GATEWAY_API="" +ENV FM_GATEWAY_API=${FM_GATEWAY_API} +ARG FM_GATEWAY_PASSWORD="" +ENV FM_GATEWAY_PASSWORD=${FM_GATEWAY_PASSWORD} COPY --from=build /app/package.json ./ COPY --from=build /app/node_modules/ ./node_modules diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..f25f3c9fa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,105 @@ +# Important: 127.0.0.1 should be replaced by the external ip address of the machine +version: "3" + +services: + gatewayd: + image: fedimint/gatewayd:v0.2.1 + command: gatewayd lnd + environment: + # Path to folder containing gateway config and data files + - FM_GATEWAY_DATA_DIR=/gateway_data + # Gateway webserver listen address + - FM_GATEWAY_LISTEN_ADDR=0.0.0.0:8175 + # Public URL from which the webserver API is reachable + - FM_GATEWAY_API_ADDR=http://127.0.0.1:8175 + # Gateway webserver authentication password + - FM_GATEWAY_PASSWORD=thereisnosecondbest + # Configured gateway routing fees Format: , + - FM_GATEWAY_FEES=0,0 + # LND RPC address + - FM_LND_RPC_ADDR=https://lnd:10009 + # LND TLS cert file path + - FM_LND_TLS_CERT=/lnd_data/tls.cert + # LND macaroon file path + - FM_LND_MACAROON=/lnd_data/data/chain/bitcoin/signet/admin.macaroon + volumes: + - gateway_datadir:/gateway_data + - lnd_datadir:/lnd_data:ro + ports: + - "8175:8175" + - "8080:8080" + depends_on: + - lnd + restart: always + platform: linux/amd64 + + lnd: + image: lightninglabs/lnd:v0.17.3-beta + entrypoint: bash + command: + - -c + - lnd --bitcoin.active --bitcoin.signet --bitcoin.dnsseed=0 --bitcoin.node=bitcoind --protocol.wumbo-channels --bitcoind.rpchost=bitcoind --bitcoind.rpcuser=bitcoin --bitcoind.rpcpass=bitcoin --bitcoind.zmqpubrawblock=tcp://bitcoind:48332 --bitcoind.zmqpubrawtx=tcp://bitcoind:48333 --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --tlsextradomain=lnd --noseedbackup + ports: + - "10009:10009" + - "0.0.0.0:9735:9735" + volumes: + - lnd_datadir:/root/.lnd + depends_on: + - bitcoind + restart: always + + bitcoind: + image: fedimint/mutinynet-bitcoind:master + command: --rpcuser=bitcoin --rpcpassword=bitcoin -zmqpubrawblock=tcp://[::]:48332 -zmqpubrawtx=tcp://[::]:48333 + ports: + - 0.0.0.0:38333:38333 + volumes: + - "bitcoin_datadir:/root/.bitcoin" + restart: always + platform: linux/amd64 + + thunderhub_config_writer: + image: alpine:latest + command: + - sh + - -c + - | + cat < /thconfig/accounts.yaml + masterPassword: "thereisnosecondbest" + accounts: + - name: "main lnd" + serverUrl: "lnd:10009" + lndDir: "/root/.lnd" + network: signet + EOF + tail -f /dev/null + volumes: + - thunderhub_datadir:/thconfig + depends_on: + - lnd + restart: always + + thunderhub: + build: + context: . + dockerfile: Dockerfile + environment: + - ACCOUNT_CONFIG_PATH=/thconfig/accounts.yaml + - HOST=0.0.0.0 + - PORT=3002 + - FM_GATEWAY_API=http://127.0.0.1:8175 + - FM_GATEWAY_PASSWORD=thereisnosecondbest + ports: + - "0.0.0.0:3002:3002" + volumes: + - lnd_datadir:/root/.lnd + - thunderhub_datadir:/thconfig + depends_on: + - thunderhub_config_writer + restart: always + +volumes: + bitcoin_datadir: + lnd_datadir: + gateway_datadir: + thunderhub_datadir: diff --git a/flake.lock b/flake.lock index c4f8c2bc0..d68559cd8 100644 --- a/flake.lock +++ b/flake.lock @@ -43,27 +43,24 @@ }, "crane": { "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils_3", "nixpkgs": [ "fedimint", "flakebox", "nixpkgs" - ], - "rust-overlay": "rust-overlay" + ] }, "locked": { - "lastModified": 1697596235, - "narHash": "sha256-4VTrrTdoA1u1wyf15krZCFl3c29YLesSNioYEgfb2FY=", - "owner": "dpc", + "lastModified": 1699217310, + "narHash": "sha256-xpW3VFUG7yE6UE6Wl0dhqencuENSkV7qpnpe9I8VbPw=", + "owner": "ipetkov", "repo": "crane", - "rev": "c97a0c0d83bfdf01c29113c5592a3defc27cb315", + "rev": "d535642bbe6f377077f7c23f0febb78b1463f449", "type": "github" }, "original": { - "owner": "dpc", + "owner": "ipetkov", "repo": "crane", - "rev": "c97a0c0d83bfdf01c29113c5592a3defc27cb315", + "rev": "d535642bbe6f377077f7c23f0febb78b1463f449", "type": "github" } }, @@ -101,17 +98,17 @@ "nixpkgs-unstable": "nixpkgs-unstable_2" }, "locked": { - "lastModified": 1699573846, - "narHash": "sha256-4xKNhUE7e3GjjMbNib35B7eJWjuDIbYtwoHBOWYtqFA=", + "lastModified": 1704316564, + "narHash": "sha256-cuAIJniPQKXfwfl2RBon5iHAaB88GUBjQ1g0IpJwipk=", "owner": "fedimint", "repo": "fedimint", - "rev": "f58f1913e62b6529a2ff36ec5c89a3852aba7ca7", + "rev": "a8422b84102ab5fc768307215d5b20d807143f27", "type": "github" }, "original": { "owner": "fedimint", "repo": "fedimint", - "rev": "f58f1913e62b6529a2ff36ec5c89a3852aba7ca7", + "rev": "a8422b84102ab5fc768307215d5b20d807143f27", "type": "github" } }, @@ -138,22 +135,6 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-utils": { "locked": { "lastModified": 1676283394, @@ -188,24 +169,6 @@ } }, "flake-utils_3": { - "inputs": { - "systems": "systems_3" - }, - "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_4": { "inputs": { "systems": [ "fedimint", @@ -227,9 +190,9 @@ "type": "github" } }, - "flake-utils_5": { + "flake-utils_4": { "inputs": { - "systems": "systems_5" + "systems": "systems_4" }, "locked": { "lastModified": 1694529238, @@ -250,23 +213,23 @@ "android-nixpkgs": "android-nixpkgs", "crane": "crane", "fenix": "fenix", - "flake-utils": "flake-utils_4", + "flake-utils": "flake-utils_3", "nixpkgs": "nixpkgs", "nixpkgs-unstable": "nixpkgs-unstable", - "systems": "systems_4" + "systems": "systems_3" }, "locked": { - "lastModified": 1697820035, - "narHash": "sha256-l+rxi/P5qt+Ud+qQCyOiKB001P/A3J0Hsh7PNg7FyWM=", + "lastModified": 1699681206, + "narHash": "sha256-vg/aDhDPV8QUi2rE3O5C/FgSaxOPZRsuRNvto5aY/JM=", "owner": "rustshop", "repo": "flakebox", - "rev": "0d866b57cd09e30e8385150e846885236ea33bdb", + "rev": "390c23bc911b354f16db4d925dbe9b1f795308ed", "type": "github" }, "original": { "owner": "rustshop", "repo": "flakebox", - "rev": "0d866b57cd09e30e8385150e846885236ea33bdb", + "rev": "390c23bc911b354f16db4d925dbe9b1f795308ed", "type": "github" } }, @@ -320,17 +283,17 @@ }, "nixpkgs-unstable_2": { "locked": { - "lastModified": 1697059129, - "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "lastModified": 1700794826, + "narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8", "type": "github" }, "original": { "owner": "NixOS", + "ref": "nixos-unstable", "repo": "nixpkgs", - "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", "type": "github" } }, @@ -352,16 +315,16 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1700097215, - "narHash": "sha256-ODQ3gBTv1iHd7lG21H+ErVISB5wVeOhd/dEogOqHs/I=", + "lastModified": 1704420045, + "narHash": "sha256-C36QmoJd5tdQ5R9MC1jM7fBkZW9zBUqbUCsgwS6j4QU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9fb122519e9cd465d532f736a98c1e1eb541ef6f", + "rev": "c1be43e8e837b8dbee2b3665a007e761680f0c3d", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.05", + "ref": "nixos-23.11", "repo": "nixpkgs", "type": "github" } @@ -369,7 +332,7 @@ "root": { "inputs": { "fedimint": "fedimint", - "flake-utils": "flake-utils_5", + "flake-utils": "flake-utils_4", "nixpkgs": "nixpkgs_3" } }, @@ -390,35 +353,6 @@ "type": "github" } }, - "rust-overlay": { - "inputs": { - "flake-utils": [ - "fedimint", - "flakebox", - "crane", - "flake-utils" - ], - "nixpkgs": [ - "fedimint", - "flakebox", - "crane", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1695003086, - "narHash": "sha256-d1/ZKuBRpxifmUf7FaedCqhy0lyVbqj44Oc2s+P5bdA=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "b87a14abea512d956f0b89d0d8a1e9b41f3e20ff", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1681028828, @@ -478,21 +412,6 @@ "repo": "default", "type": "github" } - }, - "systems_5": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index b9a59cd0c..71e28641d 100644 --- a/flake.nix +++ b/flake.nix @@ -1,15 +1,27 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; flake-utils.url = "github:numtide/flake-utils"; + fedimint = { + url = + "github:fedimint/fedimint?rev=a8422b84102ab5fc768307215d5b20d807143f27"; + }; }; outputs = { self, nixpkgs, flake-utils, fedimint }: flake-utils.lib.eachDefaultSystem (system: - let pkgs = import nixpkgs { inherit system; }; + let + pkgs = import nixpkgs { inherit system; }; + fmLib = fedimint.lib.${system}; in { devShells = fmLib.devShells // { default = fmLib.devShells.default.overrideAttrs (prev: { - nativeBuildInputs = [ pkgs.nodejs ] ++ prev.nativeBuildInputs; + nativeBuildInputs = [ + pkgs.mprocs + pkgs.nodejs + fedimint.packages.${system}.devimint + fedimint.packages.${system}.gateway-pkgs + fedimint.packages.${system}.fedimint-pkgs + ] ++ prev.nativeBuildInputs; shellHook = '' npm install ''; diff --git a/mprocs-nix-gateway.yml b/mprocs-nix-gateway.yml new file mode 100644 index 000000000..2f2f1811d --- /dev/null +++ b/mprocs-nix-gateway.yml @@ -0,0 +1,30 @@ +procs: + user: + shell: bash --init-file scripts/mprocs-user-shell.sh + stop: SIGKILL + thunderhub: + shell: bash --init-file scripts/mprocs-nix-gateway.sh + stop: SIGKILL + env: + PORT: '3000' + BROWSER: none + fedimint0: + shell: tail -n +0 -F $FM_LOGS_DIR/fedimintd-0.log + fedimint1: + shell: tail -n +0 -F $FM_LOGS_DIR/fedimintd-1.log + fedimint2: + shell: tail -n +0 -F $FM_LOGS_DIR/fedimintd-2.log + fedimint3: + shell: tail -n +0 -F $FM_LOGS_DIR/fedimintd-3.log + cln-gw: + shell: tail -n +0 -F $FM_LOGS_DIR/gatewayd-cln.log + lnd-gw: + shell: tail -n +0 -F $FM_LOGS_DIR/gatewayd-lnd.log + cln: + shell: tail -n +0 -F $FM_LOGS_DIR/lightningd.log + lnd: + shell: tail -n +0 -F $FM_LOGS_DIR/lnd.log + bitcoind: + shell: tail -n +0 -F $FM_LOGS_DIR/bitcoind.log + devimint: + shell: tail -n +0 -F $FM_LOGS_DIR/devimint.log diff --git a/package.json b/package.json index d306c8d19..4d92b229c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "update": "sh ./scripts/updateToLatest.sh", "build:image": "docker build --pull --rm -f Dockerfile -t thunderhub:testing '.'", "build:image:base": "docker build --pull --rm -f Dockerfile --build-arg BASE_PATH=/thub -t thunderhub:testing .", - "prepare": "husky install" + "prepare": "husky install", + "nix-gateway": "./scripts/mprocs-nix.sh dev-fed mprocs-nix-gateway.yml" }, "dependencies": { "@apollo/client": "^3.8.8", diff --git a/scripts/mprocs-nix-gateway.sh b/scripts/mprocs-nix-gateway.sh new file mode 100644 index 000000000..1bd376bc9 --- /dev/null +++ b/scripts/mprocs-nix-gateway.sh @@ -0,0 +1,36 @@ +# shellcheck shell=bash + +set -eo pipefail + +eval "$(devimint env)" + +echo Waiting for devimint to start up fedimint and gateway + +STATUS="$(devimint wait)" +if [ "$STATUS" = "ERROR" ] +then + echo "fedimint didn't start correctly" + echo "See other panes for errors" + exit 1 +fi + +TEMP_FILE=$(mktemp) +FM_LND_RPC_STRIPPED=$(echo $FM_LND_RPC_ADDR | sed 's/http[s]\?:\/\///') + +cat << EOF > "$TEMP_FILE" +masterPassword: password +accounts: + - name: test-regtest + serverUrl: $FM_LND_RPC_STRIPPED + macaroonPath: $FM_LND_MACAROON + certificatePath: $FM_LND_TLS_CERT +EOF + +# ----------- +# Fedimint Config +# ----------- +export ACCOUNT_CONFIG_PATH=$TEMP_FILE +export FM_GATEWAY_API=$FM_GATEWAY_API_ADDR +export FM_GATEWAY_PASSWORD=$FM_GATEWAY_PASSWORD + +npm run start:dev diff --git a/scripts/mprocs-nix-guardian.sh b/scripts/mprocs-nix-guardian.sh new file mode 100644 index 000000000..6cfc17ecb --- /dev/null +++ b/scripts/mprocs-nix-guardian.sh @@ -0,0 +1,21 @@ +# shellcheck shell=bash + +set -eo pipefail + +eval "$(devimint env)" + +echo Waiting for devimint to start up fedimint and gateway + +STATUS="$(devimint wait)" +if [ "$STATUS" = "ERROR" ] +then + echo "fedimint didn't start correctly" + echo "See other panes for errors" + exit 1 +fi + +# Conigure UI env from devimint env +CONFIG_PORT=$(($FM_PORT_FEDIMINTD_BASE + 1)) # Fedimintd 0 config AOU port is always base + 1 +export REACT_APP_FM_CONFIG_API="ws://127.0.0.1:$CONFIG_PORT" + +yarn dev:guardian-ui diff --git a/scripts/mprocs-nix.sh b/scripts/mprocs-nix.sh new file mode 100755 index 000000000..80ab44a3b --- /dev/null +++ b/scripts/mprocs-nix.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +if [[ -z "${IN_NIX_SHELL:-}" ]]; then + echo "Run "nix develop" first" + exit 1 +fi + +DEVIMINT_COMMAND=$1 +MPROCS_PATH=$2 + +export FM_TEST_DIR="$TMP/fm-$(LC_ALL=C tr -dc A-Za-z0-9 /dev/null & +echo $! >> $FM_PID_FILE + + +# Function for killing processes stored in FM_PID_FILE in reverse-order they were created in +function kill_fedimint_processes { + echo "Killing fedimint processes" + PIDS=$(cat $FM_PID_FILE | sed '1!G;h;$!d') # sed reverses order + if [ -n "$PIDS" ] + then + kill $PIDS 2>/dev/null + fi + rm -f $FM_PID_FILE +} + +trap kill_fedimint_processes EXIT + +echo "PATH: $MPROCS_PATH" +mprocs -c $MPROCS_PATH diff --git a/scripts/mprocs-user-shell.sh b/scripts/mprocs-user-shell.sh new file mode 100644 index 000000000..5a14e0793 --- /dev/null +++ b/scripts/mprocs-user-shell.sh @@ -0,0 +1,41 @@ +# shellcheck shell=bash + +eval "$(devimint env)" + +echo Waiting for devimint to start up fedimint + +STATUS="$(devimint wait)" +if [ "$STATUS" = "ERROR" ] +then + echo "fedimint didn't start correctly" + echo "See other panes for errors" + exit 1 +fi + +alias lightning-cli="\$FM_LIGHTNING_CLI" +alias lncli="\$FM_LNCLI" +alias bitcoin-cli="\$FM_BTC_CLIENT" +alias fedimint-cli="\$FM_MINT_CLIENT" +alias gateway-cln="\$FM_GWCLI_CLN" +alias gateway-lnd="\$FM_GWCLI_LND" + +eval "$(fedimint-cli completion bash)" || true +eval "$(gateway-cli completion bash)" || true + +echo Done! +echo +echo "This shell provides the following aliases:" +echo "" +echo " fedimint-cli - cli client to interact with the federation" +echo " lightning-cli - cli client for Core Lightning" +echo " lncli - cli client for LND" +echo " bitcoin-cli - cli client for bitcoind" +echo " gateway-cln - cli client for the CLN gateway" +echo " gateway-lnd - cli client for the LND gateway" +echo +echo "Use '--help' on each command for more information" +echo "" +echo "Important mprocs key sequences:" +echo "" +echo " ctrl+a - switching between panels" +echo " ctrl+a q - quit mprocs" diff --git a/scripts/replace-react-env.js b/scripts/replace-react-env.js new file mode 100644 index 000000000..4388c21e4 --- /dev/null +++ b/scripts/replace-react-env.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); + +const targetDir = process.argv[2]; + +if (!targetDir) { + console.log('Please provide a directory path.'); + process.exit(1); +} + +/** + * Give a file path, replace all {{REACT_APP_ENV}} in its contents with + * their actual environment variable value. + */ +function processFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const envVariableRegex = /\{\{REACT_APP_([^}]+)\}\}/g; + const matches = content.match(envVariableRegex); + + if (!matches) { + return; + } + + let replacedContent = content; + matches.forEach((match) => { + // Trim off {{ and }} from match + const variableName = match.slice(2, -2); + const envValue = process.env[variableName] || ''; + replacedContent = replacedContent.replace(match, envValue); + }); + + fs.writeFileSync(filePath, replacedContent, 'utf8'); +} + +function processDirectory(directoryPath) { + const files = fs.readdirSync(directoryPath); + + files.forEach((file) => { + const filePath = path.join(directoryPath, file); + const stats = fs.statSync(filePath); + + if (stats.isDirectory()) { + processDirectory(filePath); + } else { + processFile(filePath); + } + }); +} + +try { + processDirectory(targetDir); + console.log('Environment variables replaced successfully.'); +} catch (error) { + console.error( + 'An error occurred while replacing environment variables:', + error + ); + process.exit(1); +} diff --git a/src/client/next.config.js b/src/client/next.config.js index 29589dc1f..dd08183b2 100644 --- a/src/client/next.config.js +++ b/src/client/next.config.js @@ -28,5 +28,7 @@ module.exports = { disableLnMarkets: process.env.DISABLE_LNMARKETS === 'true', noVersionCheck: process.env.NO_VERSION_CHECK === 'true', logoutUrl: process.env.LOGOUT_URL || '', + fmGatewayUrl: process.env.FM_GATEWAY_API || '', + fmGatewayPassword: process.env.FM_GATEWAY_PASSWORD || '', }, }; diff --git a/src/client/pages/fedimints.tsx b/src/client/pages/fedimints.tsx new file mode 100644 index 000000000..37dddf271 --- /dev/null +++ b/src/client/pages/fedimints.tsx @@ -0,0 +1,129 @@ +import React, { useMemo } from 'react'; +import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; +import { NextPageContext } from 'next'; +import { getProps } from '../src/utils/ssr'; +import { + CardWithTitle, + SubTitle, + Card, + DarkSubTitle, +} from '../src/components/generic/Styled'; +import { X } from 'react-feather'; +import { AddMint } from '../src/views/fedimints/AddMint'; +import Table from '../src/components/table'; +import { useGatewayFederations } from '../src/hooks/UseGatewayFederations'; +import { Federation } from '../src/api/types'; +import { CellContext } from '@tanstack/react-table'; +import { toast } from 'react-toastify'; +import { Price } from '../src/components/price/Price'; + +const FedimintsView = () => { + const federations = useGatewayFederations(); + + const tableData = useMemo(() => { + const federationData = federations || []; + + return federationData.map(f => { + return { + ...f, + alias: f.federation_id, + }; + }); + }, [federations]); + + const columns = useMemo( + () => [ + { + header: 'Federation Name', + accessorKey: 'federation_name', + cell: (props: CellContext) => ( +
+ {props.row.original.config.meta.federation_name || '-'} +
+ ), + }, + { + header: 'Balance', + accessorKey: 'balance_msat', + cell: (props: CellContext) => ( +
+ +
+ ), + }, + { + header: 'Suported Modules', + accessorKey: 'modules', + cell: (props: CellContext) => ( +
+ {Object.values(props.row.original.config.modules) + .map(module => module.kind) + .join(', ')} +
+ ), + }, + { + header: 'Leave', + accessorKey: 'leave', + cell: (props: CellContext) => ( +
{ + if (props.row.original.balance_msat > 0) { + toast.error("Can't leave a federation you've got sats in!"); + } else { + toast.warn('Not implemented yet!'); + } + }} + > + +
+ ), + }, + ], + [] + ); + + if (!federations || !federations?.length) { + return ( + + No Connected Federations! + + ); + } + + return ( + + Fedimints + + 1 ? 'federations' : 'federation' + } + /> + + + ); +}; + +const Wrapped = () => ( + + + + +); + +export default Wrapped; + +export async function getServerSideProps(context: NextPageContext) { + return await getProps(context); +} diff --git a/src/client/pages/index.tsx b/src/client/pages/index.tsx index 79ea89811..a2e88998c 100644 --- a/src/client/pages/index.tsx +++ b/src/client/pages/index.tsx @@ -7,24 +7,30 @@ import { MempoolReport } from '../src/views/home/reports/mempool'; import { LiquidityGraph } from '../src/views/home/reports/liquidReport/LiquidityGraph'; import { AccountButtons } from '../src/views/home/account/AccountButtons'; import { AccountInfo } from '../src/views/home/account/AccountInfo'; -import { QuickActions } from '../src/views/home/quickActions/QuickActions'; -import { FlowBox } from '../src/views/home/reports/flow'; import { ForwardBox } from '../src/views/home/reports/forwardReport'; import { ConnectCard } from '../src/views/home/connect/Connect'; +import { QuickActions } from '../src/views/home/quickActions/QuickActions'; +import { useGatewayState } from '../src/context/GatewayContext'; +import { Network } from '../src/api/types'; +import { FaucetActions } from '../src/views/home/faucetActions/FaucetActions'; -const HomeView = () => ( - <> - - - - - - - - - - -); +const HomeView = () => { + const { gatewayInfo } = useGatewayState(); + + return ( + <> + + + + + + {gatewayInfo?.network === Network.Signet ? : null} + + + + + ); +}; const Wrapped = () => ( diff --git a/src/client/src/api/FaucetApi.ts b/src/client/src/api/FaucetApi.ts new file mode 100644 index 000000000..6ac94be65 --- /dev/null +++ b/src/client/src/api/FaucetApi.ts @@ -0,0 +1,91 @@ +// GatewayApi is an implementation of the ApiInterface +class FaucetApi { + private baseUrl: string | undefined = 'https://faucet.mutinynet.com/api'; + + private post = async (api: string, body: unknown): Promise => { + return fetch(`${this.baseUrl}/${api}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + }; + + onchain = async (body: { address: string; sats: number }) => { + try { + if (body.sats < 10000 || body.sats > 200000) + throw new Error('Amount must be between 10000 and 200000'); + + const res = await this.post('onchain', body); + + if (res.ok) { + const result = await res.json(); + return Promise.resolve(result); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ + message: (error as Error).message, + error, + }); + } + }; + + payInvoice = async (body: { bolt11: string }) => { + try { + const res = await this.post('lightning', body); + + if (res.ok) { + const result = await res.json(); + return Promise.resolve(result); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: (error as Error).message, error }); + } + }; + + refundFaucet = async (body: { amount_sats: number }) => { + try { + const res = await this.post('bolt11', body); + + if (res.ok) { + const result = await res.text(); + return Promise.resolve(result); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: (error as Error).message, error }); + } + }; + + requestChannel = async (body: { + capacity: number; + push_amount: number; + pubkey: string; + host: string; + }) => { + try { + const res = await this.post('channel', body); + + if (res.ok) { + const result = await res.json(); + return Promise.resolve(result); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: (error as Error).message, error }); + } + }; +} + +const responseToError = (res: Response): Error => { + return new Error(`Status : ${res.status} \nReason : ${res.statusText}\n`); +}; + +export const faucetApi = new FaucetApi(); diff --git a/src/client/src/api/GatewayApi.ts b/src/client/src/api/GatewayApi.ts new file mode 100644 index 000000000..7800b0192 --- /dev/null +++ b/src/client/src/api/GatewayApi.ts @@ -0,0 +1,118 @@ +// import { publicRuntimeConfig } from '../../next.config'; +// import { publicRuntimeConfig } from '../../next.config'; +import getConfig from 'next/config'; +import { GatewayInfo, Federation } from './types'; + +// const SESSION_STORAGE_KEY = 'gateway-ui-key'; + +const { publicRuntimeConfig } = getConfig(); + +// GatewayApi is an implementation of the ApiInterface +class GatewayApi { + private baseUrl: string | undefined = publicRuntimeConfig.fmGatewayUrl; + private password: string | undefined = publicRuntimeConfig.fmGatewayPassword; + + private post = async (api: string, body: unknown): Promise => { + if (this.baseUrl === undefined) { + throw new Error( + 'Misconfigured Gateway API. Make sure FM_GATEWAY_API is configured appropriately' + ); + } + + if (this.password === undefined) { + throw new Error( + 'Misconfigured Gateway API. Make sure gateway password is configured appropriately' + ); + } + + return fetch(`${this.baseUrl}/${api}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.password}`, + }, + body: JSON.stringify(body), + }); + }; + + fetchInfo = async (): Promise => { + try { + const res: Response = await this.post('info', null); + + if (res.ok) { + const info: GatewayInfo = await res.json(); + return Promise.resolve(info); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: 'Error fetching gateway info', error }); + } + }; + + fetchAddress = async (federationId: string): Promise => { + try { + const res: Response = await this.post('address', { + federation_id: federationId, + }); + + if (res.ok) { + const address: string = (await res.text()).replace(/"/g, '').trim(); + return Promise.resolve(address); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ + message: 'Error fetching deposit address', + error, + }); + } + }; + + connectFederation = async (inviteCode: string): Promise => { + try { + const res: Response = await this.post('connect-fed', { + invite_code: inviteCode, + }); + + if (res.ok) { + const federation: Federation = await res.json(); + return Promise.resolve(federation); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: 'Error connecting federation', error }); + } + }; + + requestWithdrawal = async ( + federationId: string, + amountSat: number | 'all', + address: string + ): Promise => { + try { + const res: Response = await this.post('withdraw', { + federation_id: federationId, + amount: amountSat, + address, + }); + + if (res.ok) { + const txid: string = await res.text(); + return Promise.resolve(txid); + } + + throw responseToError(res); + } catch (error) { + return Promise.reject({ message: 'Error requesting withdrawal', error }); + } + }; +} + +const responseToError = (res: Response): Error => { + return new Error(`Status : ${res.status} \nReason : ${res.statusText}\n`); +}; + +export const gatewayApi = new GatewayApi(); diff --git a/src/client/src/api/types.ts b/src/client/src/api/types.ts new file mode 100644 index 000000000..16d4867d6 --- /dev/null +++ b/src/client/src/api/types.ts @@ -0,0 +1,89 @@ +interface Fees { + base_msat: number; + proportional_millionths: number; +} + +export enum ModuleKind { + Ln = 'ln', + Mint = 'mint', + Wallet = 'wallet', +} + +interface FedimintModule { + config: string; + kind: ModuleKind; + version: number; +} + +interface ApiEndpoint { + name: string; + url: string; +} + +export type MetaConfig = { federation_name?: string }; + +export type ConsensusVersion = { + major: number; + minor: number; +}; + +export interface ClientConfig { + consensus_version: ConsensusVersion; + epoch_pk: string; + federation_id: string; + api_endpoints: Record; + modules: Record; + meta: MetaConfig; +} + +export interface Federation { + federation_id: string; + balance_msat: number; + config: ClientConfig; +} + +export interface GatewayInfo { + federations: Federation[]; + fees: Fees; + lightning_alias: string; + lightning_pub_key: string; + version_hash: string; + network?: Network; +} + +// Type adaptation from https://docs.rs/bitcoin/latest/bitcoin/network/enum.Network.html +export enum Network { + Bitcoin = 'bitcoin', + Testnet = 'testnet', + Signet = 'signet', + Regtest = 'regtest', +} + +export type TransactionId = string; + +// For testing +export const dummyFederation = { + federation_id: 'test_federation_id', + balance_msat: 1000, + config: { + consensus_version: 1, + epoch_pk: 'test_epoch_pk', + federation_id: 'test_federation_id', + api_endpoints: { + 0: { + name: 'test_api_endpoint_name', + url: 'test_api_endpoint_url', + }, + }, + modules: { + 0: { + config: 'test_module_config', + kind: ModuleKind.Ln, + version: 1, + }, + }, + meta: { + federation_name: 'test_federation_name', + }, + }, +}; diff --git a/src/client/src/components/buttons/colorButton/ColorButton.tsx b/src/client/src/components/buttons/colorButton/ColorButton.tsx index cdae81a40..9c386bafb 100644 --- a/src/client/src/components/buttons/colorButton/ColorButton.tsx +++ b/src/client/src/components/buttons/colorButton/ColorButton.tsx @@ -28,6 +28,7 @@ const GeneralButton = styled.button` min-height: 38px; display: flex; justify-content: center; + gap: 8px; align-items: center; cursor: pointer; outline: none; diff --git a/src/client/src/components/chart/BarChart.tsx b/src/client/src/components/chart/BarChart.tsx index 096c7db6d..4e90ba03a 100644 --- a/src/client/src/components/chart/BarChart.tsx +++ b/src/client/src/components/chart/BarChart.tsx @@ -58,7 +58,7 @@ export const BarChart = ({ const dates = data.map((d: any) => d.date); return { dates, series }; - }, [data, title]); + }, [data, title, dataKey]); const option = useMemo(() => { const fontColor = themeContext?.mode === 'light' ? 'black' : 'white'; diff --git a/src/client/src/components/chart/HorizontalBarChart.tsx b/src/client/src/components/chart/HorizontalBarChart.tsx index 12cb5ba06..0231284f2 100644 --- a/src/client/src/components/chart/HorizontalBarChart.tsx +++ b/src/client/src/components/chart/HorizontalBarChart.tsx @@ -136,7 +136,7 @@ export const HorizontalBarChart = ({ legend: { show: true }, series: seriesData, }; - }, [colorRange, themeContext, seriesData, yLabels]); + }, [colorRange, themeContext, seriesData, yLabels, maxValue]); if (!keys.length) return null; diff --git a/src/client/src/components/generic/Styled.tsx b/src/client/src/components/generic/Styled.tsx index d2a96c3c0..6c98fddb5 100644 --- a/src/client/src/components/generic/Styled.tsx +++ b/src/client/src/components/generic/Styled.tsx @@ -154,6 +154,14 @@ export const RightAlign = styled.div` align-items: center; `; +export const LeftAlign = styled.div` + width: 100%; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 8px; +`; + export const ColumnLine = styled.div` display: flex; flex-direction: column; diff --git a/src/client/src/components/logo/GhostIcon.tsx b/src/client/src/components/logo/GhostIcon.tsx new file mode 100644 index 000000000..20abaa4ae --- /dev/null +++ b/src/client/src/components/logo/GhostIcon.tsx @@ -0,0 +1,21 @@ +import { forwardRef } from 'react'; + +export const GhostLogo = forwardRef( + ({ color = 'currentColor', size = 100, children, ...rest }, ref) => { + return ( + + {children} + + + ); + } +); + +GhostLogo.displayName = 'GhostLogo'; diff --git a/src/client/src/components/table/DebouncedInput.tsx b/src/client/src/components/table/DebouncedInput.tsx index d3c268e82..18b0e0640 100644 --- a/src/client/src/components/table/DebouncedInput.tsx +++ b/src/client/src/components/table/DebouncedInput.tsx @@ -27,7 +27,7 @@ export function DebouncedInput({ }, debounce); return () => clearTimeout(timeout); - }, [value]); + }, [value, debounce, onChange]); return ( = ({ children, @@ -12,7 +13,9 @@ export const ContextProvider: React.FC<{ children?: ReactNode }> = ({ - {children} + + {children} + diff --git a/src/client/src/context/GatewayContext.tsx b/src/client/src/context/GatewayContext.tsx new file mode 100644 index 000000000..9fda1f747 --- /dev/null +++ b/src/client/src/context/GatewayContext.tsx @@ -0,0 +1,87 @@ +import React, { + createContext, + useContext, + useReducer, + ReactNode, + useEffect, +} from 'react'; +import { GatewayInfo } from '../api/types'; +import { gatewayApi } from '../api/GatewayApi'; + +type State = { + gatewayInfo: GatewayInfo | null; + loading: boolean; + error: string | null; +}; + +type ActionType = + | { + type: 'connected'; + state: GatewayInfo; + } + | { + type: 'error'; + state: string; + }; + +type Dispatch = (action: ActionType) => void; + +export const StateContext = createContext(undefined); +export const DispatchContext = createContext(undefined); + +const initialState: State = { + gatewayInfo: null, + loading: true, + error: null, +}; + +const stateReducer = (state: State, action: ActionType): State => { + switch (action.type) { + case 'connected': + return { ...state, gatewayInfo: action.state, loading: false }; + case 'error': + return { ...state, error: action.state, loading: false }; + default: + return state; + } +}; + +const GatewayProvider: React.FC<{ children?: ReactNode }> = ({ children }) => { + const [state, dispatch] = useReducer(stateReducer, initialState); + + useEffect(() => { + gatewayApi + .fetchInfo() + .then(info => { + dispatch({ type: 'connected', state: info }); + }) + .catch(({ error }) => { + console.log('fetchInfo rejected', error); + dispatch({ type: 'connected', state: error.message }); + }); + }, []); + + return ( + + {children} + + ); +}; + +const useGatewayState = () => { + const context = useContext(StateContext); + if (context === undefined) { + throw new Error('useGatewayState must be used within a GatewayProvider'); + } + return context; +}; + +const useGatewayDispatch = () => { + const context = useContext(DispatchContext); + if (context === undefined) { + throw new Error('useGatewayDispatch must be used within a GatewayProvider'); + } + return context; +}; + +export { GatewayProvider, useGatewayState, useGatewayDispatch }; diff --git a/src/client/src/hooks/UseGatewayEcashTotal.tsx b/src/client/src/hooks/UseGatewayEcashTotal.tsx new file mode 100644 index 000000000..0c9b0cd3f --- /dev/null +++ b/src/client/src/hooks/UseGatewayEcashTotal.tsx @@ -0,0 +1,15 @@ +import Big from 'big.js'; +import { useGatewayState } from '../context/GatewayContext'; + +export const useGatewayEcashTotalSats = () => { + const { gatewayInfo } = useGatewayState(); + + if (!gatewayInfo || !gatewayInfo.federations) { + return new Big(0).toString(); + } + + return gatewayInfo.federations + .reduce((acc, federation) => acc.add(federation.balance_msat), new Big(0)) + .div(1000) // Convert from millisatoshis to satoshis + .toString(); +}; diff --git a/src/client/src/hooks/UseGatewayFederations.tsx b/src/client/src/hooks/UseGatewayFederations.tsx new file mode 100644 index 000000000..b1a1bcff9 --- /dev/null +++ b/src/client/src/hooks/UseGatewayFederations.tsx @@ -0,0 +1,11 @@ +import { useGatewayState } from '../context/GatewayContext'; + +export const useGatewayFederations = () => { + const { gatewayInfo } = useGatewayState(); + + if (!gatewayInfo) { + return []; + } + + return gatewayInfo.federations; +}; diff --git a/src/client/src/layouts/navigation/Navigation.tsx b/src/client/src/layouts/navigation/Navigation.tsx index 4fdd8e7f4..4d2a1850d 100644 --- a/src/client/src/layouts/navigation/Navigation.tsx +++ b/src/client/src/layouts/navigation/Navigation.tsx @@ -2,6 +2,7 @@ import React, { FC, SVGAttributes } from 'react'; import styled, { css } from 'styled-components'; import { Home, + Sun, Cpu, Server, Settings, @@ -123,6 +124,7 @@ const BurgerNav = styled.a` `; const HOME = '/'; +const FEDIMINTS = '/fedimints'; const DASHBOARD = '/dashboard'; const PEERS = '/peers'; const CHANNEL = '/channels'; @@ -180,6 +182,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { const renderLinks = () => ( {renderNavButton('Home', HOME, Home, sidebar)} + {renderNavButton('Fedimints', FEDIMINTS, Sun, sidebar)} {renderNavButton('Dashboard', DASHBOARD, Grid, sidebar)} {renderNavButton('Peers', PEERS, Users, sidebar)} {renderNavButton('Channels', CHANNEL, Cpu, sidebar)} @@ -197,6 +200,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { const renderBurger = () => ( {renderBurgerNav('Home', HOME, Home)} + {renderBurgerNav('Fedimints', FEDIMINTS, Sun)} {renderBurgerNav('Dashboard', DASHBOARD, Grid)} {renderBurgerNav('Peers', PEERS, Users)} {renderBurgerNav('Channels', CHANNEL, Cpu)} diff --git a/src/client/src/layouts/navigation/nodeInfo/NodeInfo.tsx b/src/client/src/layouts/navigation/nodeInfo/NodeInfo.tsx index d8606ee1e..aea54d23d 100644 --- a/src/client/src/layouts/navigation/nodeInfo/NodeInfo.tsx +++ b/src/client/src/layouts/navigation/nodeInfo/NodeInfo.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Zap, Anchor, Circle } from 'react-feather'; +import { Zap, Link, Circle, Sun } from 'react-feather'; import { Tooltip as ReactTooltip } from 'react-tooltip'; import styled from 'styled-components'; import { getPrice, Price } from '../../../components/price/Price'; @@ -16,6 +16,7 @@ import { } from '../../../components/generic/Styled'; import { useConfigState } from '../../../context/ConfigContext'; import { usePriceState } from '../../../context/PriceContext'; +import { useGatewayEcashTotalSats } from '../../../hooks/UseGatewayEcashTotal'; const Closed = styled.div` display: flex; @@ -45,8 +46,9 @@ const Info = styled.div<{ bottomColor: string }>` const Balance = styled.div` display: flex; - justify-content: center; + justify-content: space-between; align-items: center; + width: 100%; margin: 2px 0; padding: 0 5px; cursor: default; @@ -78,6 +80,7 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => { } = useNodeInfo(); const { onchain, lightning } = useNodeBalances(); + const totalFedimintEcash = useGatewayEcashTotalSats(); const { currency, displayValues } = useConfigState(); const priceContext = usePriceState(); @@ -118,16 +121,17 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => { size={18} color={channelPending === 0 ? '#FFD300' : '#652EC7'} fill={channelPending === 0 ? '#FFD300' : '#652EC7'} - /> + />{' '} - + + + + + ); } @@ -154,7 +158,7 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => { color={channelPending === 0 ? '#FFD300' : '#652EC7'} /> - @@ -179,6 +183,9 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => { {renderLine('Closed Channels', closedChannelCount)} {renderLine('Peers', peersCount)} + + {renderLine('Fedimint Balance', totalFedimintEcash)} + ); } @@ -196,9 +203,13 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => { - + + + + + - {breakAmount} - + {breakAmount} {'sats'} ); } diff --git a/src/client/src/views/dashboard/widgets/lightning/balances.tsx b/src/client/src/views/dashboard/widgets/lightning/balances.tsx index 4006e74ec..a7b018cff 100644 --- a/src/client/src/views/dashboard/widgets/lightning/balances.tsx +++ b/src/client/src/views/dashboard/widgets/lightning/balances.tsx @@ -3,6 +3,7 @@ import { useNodeBalances } from '../../../../hooks/UseNodeBalances'; import { unSelectedNavButton } from '../../../../styles/Themes'; import styled from 'styled-components'; import Big from 'big.js'; +import { useGatewayEcashTotalSats } from '../../../../hooks/UseGatewayEcashTotal'; const S = { wrapper: styled.div` @@ -82,3 +83,16 @@ export const ChainBalance = () => { ); }; + +export const FedimintBalance = () => { + const totalFedimintEcash = useGatewayEcashTotalSats(); + + return ( + + Fedimint Balance + + + + + ); +}; diff --git a/src/client/src/views/dashboard/widgets/widgetList.tsx b/src/client/src/views/dashboard/widgets/widgetList.tsx index c8447f0f4..e6199e90c 100644 --- a/src/client/src/views/dashboard/widgets/widgetList.tsx +++ b/src/client/src/views/dashboard/widgets/widgetList.tsx @@ -3,6 +3,7 @@ import { MempoolWidget } from './external/mempool'; import { ChainBalance, ChannelBalance, + FedimintBalance, TotalBalance, } from './lightning/balances'; import { ChannelListWidget } from './lightning/channels'; @@ -111,6 +112,14 @@ export const widgetList: WidgetProps[] = [ component: ChainBalance, default: { ...defaultProps, w: 2, h: 3 }, }, + { + id: 6, + name: 'Fedimint Balance', + group: 'Lightning', + subgroup: 'Info', + component: FedimintBalance, + default: { ...defaultProps, w: 2, h: 3 }, + }, { id: 7, name: 'Alias', diff --git a/src/client/src/views/fedimints/AddMint.tsx b/src/client/src/views/fedimints/AddMint.tsx new file mode 100644 index 000000000..f814f6524 --- /dev/null +++ b/src/client/src/views/fedimints/AddMint.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { X } from 'react-feather'; +import { toast } from 'react-toastify'; +import { InputWithDeco } from '../../components/input/InputWithDeco'; +import { + CardWithTitle, + SubTitle, + Card, + SingleLine, + DarkSubTitle, + Separation, +} from '../../components/generic/Styled'; +import { ColorButton } from '../../components/buttons/colorButton/ColorButton'; +import { gatewayApi } from '../../api/GatewayApi'; +import { useGatewayDispatch } from '../../context/GatewayContext'; + +export const AddMint = () => { + const gatewayDispatch = useGatewayDispatch(); + const [isAdding, setIsAdding] = useState(false); + const [inviteCode, setInviteCode] = useState(''); + + const handleEnter = () => { + gatewayApi.connectFederation(inviteCode).then(() => { + gatewayApi + .fetchInfo() + .then(info => gatewayDispatch({ type: 'connected', state: info })) + .catch(({ error }) => { + toast.error(error.message); + }); + }); + }; + + return ( + + Mint Management + + + Connect to a new Federation + setIsAdding(prev => !prev)} + > + {isAdding ? : 'Add'} + + + {isAdding && ( + <> + + + setInviteCode(value)} + placeholder={'Paste Invite Code'} + /> +
+ + Connect + + + + )} + + + ); +}; diff --git a/src/client/src/views/home/account/AccountButtons.tsx b/src/client/src/views/home/account/AccountButtons.tsx index 86f488b7f..426334379 100644 --- a/src/client/src/views/home/account/AccountButtons.tsx +++ b/src/client/src/views/home/account/AccountButtons.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Anchor, X, Zap } from 'react-feather'; +import { Link, X, Zap, Sun } from 'react-feather'; import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; import { Card } from '../../../components/generic/Styled'; import { mediaWidths } from '../../../styles/Themes'; @@ -8,17 +8,21 @@ import { CreateInvoiceCard } from './createInvoice/CreateInvoice'; import { PayCard } from './pay/Payment'; import { ReceiveOnChainCard } from './receiveOnChain/ReceiveOnChain'; import { SendOnChainCard } from './sendOnChain/SendOnChain'; +import { PegInEcashCard } from './pegInEcash/PegInEcash'; +import { PegOutEcashCard } from './pegOutEcash/PegOutEcash'; +import { useGatewayState } from '../../../context/GatewayContext'; const SECTION_COLOR = '#FFD300'; const S = { - grid: styled.div` + grid: styled.div<{ federations: number }>` display: grid; grid-gap: 8px; - grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-columns: ${({ federations }) => + federations > 0 ? '1fr 1fr 1fr 1fr 1fr 1fr' : '1fr 1fr 1fr 1fr'}; margin-bottom: 32px; - @media (${mediaWidths.mobile}) { + @media (${mediaWidths.modifiedMobile}) { grid-template-columns: 1fr 1fr; } `, @@ -26,6 +30,7 @@ const S = { export const AccountButtons = () => { const [state, setState] = useState('none'); + const { gatewayInfo } = useGatewayState(); const renderContent = () => { switch (state) { @@ -37,6 +42,10 @@ export const AccountButtons = () => { return setState('none')} />; case 'receive_chain': return ; + case 'pegout_ecash': + return setState('none')} />; + case 'pegin_ecash': + return ; default: return null; } @@ -44,7 +53,7 @@ export const AccountButtons = () => { return ( <> - + setState(state === 'send_ln' ? 'none' : 'send_ln')} @@ -78,7 +87,7 @@ export const AccountButtons = () => { {state === 'send_chain' ? ( ) : ( - + )} Send @@ -91,10 +100,40 @@ export const AccountButtons = () => { {state === 'receive_chain' ? ( ) : ( - + )} Receive + {gatewayInfo?.federations && gatewayInfo?.federations.length > 0 && ( + + setState(state === 'pegout_ecash' ? 'none' : 'pegout_ecash') + } + > + {state === 'pegout_ecash' ? ( + + ) : ( + + )} + Peg Out + + )} + {gatewayInfo?.federations && gatewayInfo?.federations.length > 0 && ( + + setState(state === 'pegin_ecash' ? 'none' : 'pegin_ecash') + } + > + {state === 'pegin_ecash' ? ( + + ) : ( + + )} + Peg In + + )} {state !== 'none' && {renderContent()}} diff --git a/src/client/src/views/home/account/AccountInfo.tsx b/src/client/src/views/home/account/AccountInfo.tsx index c341942a7..82738148d 100644 --- a/src/client/src/views/home/account/AccountInfo.tsx +++ b/src/client/src/views/home/account/AccountInfo.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { Zap, Anchor, Pocket } from 'react-feather'; +import { Zap, Link } from 'react-feather'; import { useNodeBalances } from '../../../hooks/UseNodeBalances'; import Big from 'big.js'; import { renderLine } from '../../../components/generic/helpers'; @@ -9,55 +9,36 @@ import { CardWithTitle, SubTitle, Separation, - DarkSubTitle, - ResponsiveLine, - SingleLine, + // DarkSubTitle, + // ResponsiveLine, + LeftAlign, } from '../../../components/generic/Styled'; import { Price } from '../../../components/price/Price'; import { mediaWidths } from '../../../styles/Themes'; +import { FedimintGatewayCard } from './gateway/FedimintGatewayCard'; +// import { useGatewayEcashTotal } from '../../../hooks/UseGatewayEcashTotal'; +import { useGatewayState } from '../../../context/GatewayContext'; +import { GatewayInfo } from '../../../api/types'; +import { NetworkIndicator } from './NetworkIndicator'; const S = { - grid: styled.div` + grid: styled.div<{ gatewayInfo?: GatewayInfo | null }>` display: grid; grid-gap: 16px; - grid-template-columns: 1fr 1fr; + grid-template-columns: ${({ gatewayInfo }) => + gatewayInfo ? '1fr 1fr 1fr' : '1fr 1fr'}; - @media (${mediaWidths.mobile}) { + @media (${mediaWidths.modifiedMobile}) { display: block; } `, }; -const Tile = styled.div<{ startTile?: boolean }>` - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: ${({ startTile }) => (startTile ? 'flex-start' : 'flex-end')}; - - @media (${mediaWidths.mobile}) { - width: 100%; - flex-direction: row; - align-items: flex-end; - margin: 0 0 8px; - } -`; - const sectionColor = '#FFD300'; export const AccountInfo = () => { const { onchain, lightning } = useNodeBalances(); - - const totalAmount = new Big(onchain.confirmed) - .add(onchain.pending) - .add(onchain.closing) - .add(lightning.confirmed) - .add(lightning.pending) - .toString(); - - const totalChain = new Big(onchain.confirmed).add(onchain.pending).toString(); - const totalLightning = new Big(lightning.confirmed) - .add(lightning.pending) - .toString(); + const { gatewayInfo } = useGatewayState(); const activeLightning = new Big(lightning.active) .sub(lightning.commit) @@ -74,17 +55,11 @@ export const AccountInfo = () => { return ( <> - Resume - + + + + {/* - Total
@@ -92,30 +67,50 @@ export const AccountInfo = () => {
- Bitcoin + Lightning +
+ +
+
+ + Onchain Bitcoin
- Lightning + Fedimint Ecash
+ + Onchain Bitcoin +
+ +
+
+ {gatewayInfo && ( + + Fedimint Ecash +
+ +
+
+ )}
-
+
*/}
- + - + Lightning - + {renderLine('Available', )} {renderLine('Not Available', )} @@ -124,19 +119,20 @@ export const AccountInfo = () => { - - + Bitcoin - + {renderLine('Available', )} {renderLine('Pending', )} {renderLine('Force Closures', )} + {gatewayInfo ? : null} ); diff --git a/src/client/src/views/home/account/NetworkIndicator.tsx b/src/client/src/views/home/account/NetworkIndicator.tsx new file mode 100644 index 000000000..a07c6fef5 --- /dev/null +++ b/src/client/src/views/home/account/NetworkIndicator.tsx @@ -0,0 +1,51 @@ +import React, { FC } from 'react'; + +interface NetworkIndicatorProps { + network: string; + // bitcoinRpcUrl: string; +} + +const getNetworkDetails = ( + network: string + // isMutinynet: boolean +) => { + const networkDetails: { [key: string]: { color: string; name: string } } = { + bitcoin: { color: '#FF9900', name: 'Mainnet' }, + main: { color: '#FF9900', name: 'Mainnet' }, + testnet: { color: '#6BED33', name: 'Testnet' }, + test: { color: '#6BED33', name: 'Testnet' }, + signet: { color: 'purple', name: 'Signet' }, + // isMutinynet + // ? { color: 'red', name: 'Mutinynet' } + // : { color: 'purple', name: 'Signet' }, + regtest: { color: '#33C6EC', name: 'Regtest' }, + default: { color: 'gray', name: 'Unknown' }, + }; + + return networkDetails[network] || networkDetails['default']; +}; + +// const isMutinynet = (bitcoinRpcUrl: string) => { +// try { +// const url = new URL(bitcoinRpcUrl); +// return url.host === 'mutinynet.com'; +// } catch (e) { +// return false; +// } +// }; + +export const NetworkIndicator: FC = ({ + network, + // bitcoinRpcUrl, +}) => { + const { color, name } = getNetworkDetails( + network + // isMutinynet(bitcoinRpcUrl) + ); + + return ( +
+ Network: {name} +
+ ); +}; diff --git a/src/client/src/views/home/account/gateway/FedimintGatewayCard.tsx b/src/client/src/views/home/account/gateway/FedimintGatewayCard.tsx new file mode 100644 index 000000000..556dcff97 --- /dev/null +++ b/src/client/src/views/home/account/gateway/FedimintGatewayCard.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { + Card, + CardWithTitle, + LeftAlign, + Separation, + SingleLine, + SubTitle, +} from '../../../../components/generic/Styled'; +import { Sun } from 'react-feather'; +import { renderLine } from '../../../../components/generic/helpers'; +import { toast } from 'react-toastify'; +import { Input } from '../../../../components/input'; +import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; +import { Price } from '../../../../components/price/Price'; +import { useGatewayDispatch } from '../../../../context/GatewayContext'; +import { gatewayApi } from '../../../../api/GatewayApi'; +import { GatewayInfo } from '../../../../api/types'; +import { useGatewayEcashTotalSats } from '../../../../hooks/UseGatewayEcashTotal'; + +interface FedimintGatewayCardProps { + gatewayInfo: GatewayInfo; +} + +const sectionColor = '#FFD300'; + +export const FedimintGatewayCard = ({ + gatewayInfo, +}: FedimintGatewayCardProps) => { + const gatewayDispath = useGatewayDispatch(); + const [inviteCode, setInviteCode] = useState(''); + const totalFedimintEcash = useGatewayEcashTotalSats(); + + const handleEnter = () => { + gatewayApi.connectFederation(inviteCode).then(() => { + gatewayApi + .fetchInfo() + .then(info => gatewayDispath({ type: 'connected', state: info })) + .catch(({ error }) => { + toast.error(error.message); + }); + }); + }; + + return ( + + + + + Fedimint Ecash + + + {!gatewayInfo.federations || gatewayInfo.federations.length === 0 ? ( + <> + {/* TODO: Left Align the Text */} +
+ {'Connect to a Federation'} +
+ + setInviteCode(e.target.value)} + /> + + Connect + + + + ) : ( +
+ {renderLine('Total Amount', )} + {renderLine( + 'Connected Federations', + gatewayInfo.federations.length + )} +
+ )} +
+
+ ); +}; diff --git a/src/client/src/views/home/account/pegInEcash/PegInEcash.tsx b/src/client/src/views/home/account/pegInEcash/PegInEcash.tsx new file mode 100644 index 000000000..75e4a13f0 --- /dev/null +++ b/src/client/src/views/home/account/pegInEcash/PegInEcash.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { toast } from 'react-toastify'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { QRCodeSVG } from 'qrcode.react'; +import { Copy } from 'react-feather'; +import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; +import { mediaWidths } from '../../../../styles/Themes'; +import { SmallSelectWithValue } from '../../../../components/select'; +import { + ResponsiveLine, + SubTitle, +} from '../../../../components/generic/Styled'; +import { Federation } from '../../../../api/types'; +import { gatewayApi } from '../../../../api/GatewayApi'; +import { useGatewayFederations } from '../../../../hooks/UseGatewayFederations'; + +const S = { + row: styled.div` + display: grid; + align-items: center; + gap: 16px; + grid-template-columns: 1fr 2fr; + + @media (${mediaWidths.mobile}) { + width: 100%; + display: block; + } + `, +}; + +const Responsive = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + @media (${mediaWidths.mobile}) { + flex-direction: column; + } +`; + +const WrapRequest = styled.div` + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; + margin: 24px; + font-size: 14px; +`; + +const QRWrapper = styled.div` + width: 280px; + height: 280px; + margin: 16px; + background: white; + padding: 16px; +`; + +const Column = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const PegInEcashCard = () => { + const federations: Federation[] = useGatewayFederations(); + const [selectedFederation, setSelectedFederation] = useState(0); + const [address, setAddress] = useState(''); + + const options = federations.map(f => ({ + label: f.config.meta.federation_name || 'No connected Federations', + value: f.federation_id || 'No connected Federations', + })); + + const handleFetchPegInAddress = () => { + gatewayApi + .fetchAddress(federations[selectedFederation].federation_id) + .then(address => { + setAddress(address); + }) + .catch(e => { + toast.error('Error fetching peg out address', e); + }); + }; + + return ( + <> + {address !== '' ? ( + + + + + + {address} + toast.success('Address Copied')} + > + + + Copy + + + + + ) : ( + <> + + + Into Federation: + {federations.length > 0 && ( + + setSelectedFederation( + federations.findIndex(f => f.federation_id === e[0].value) + ) + } + options={options} + value={options[selectedFederation]} + isClearable={false} + /> + )} + + handleFetchPegInAddress()} + disabled={false} + withMargin={'0 0 0 16px'} + mobileMargin={'16px 0 0'} + arrow={true} + mobileFullWidth={true} + > + Create Peg In Address + + + + )} + + ); +}; diff --git a/src/client/src/views/home/account/pegOutEcash/PegOutEcash.tsx b/src/client/src/views/home/account/pegOutEcash/PegOutEcash.tsx new file mode 100644 index 000000000..a780760ec --- /dev/null +++ b/src/client/src/views/home/account/pegOutEcash/PegOutEcash.tsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import { toast } from 'react-toastify'; +import { InputWithDeco } from '../../../../components/input/InputWithDeco'; +import { + Separation, + SingleLine, + SubTitle, +} from '../../../../components/generic/Styled'; +import { + MultiButton, + SingleButton, +} from '../../../../components/buttons/multiButton/MultiButton'; +import { Price } from '../../../../components/price/Price'; +import Modal from '../../../../components/modal/ReactModal'; +import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; +import { renderLine } from '../../../../components/generic/helpers'; +import { Federation } from '../../../../api/types'; +import { SmallSelectWithValue } from '../../../../components/select'; +import { useGatewayFederations } from '../../../../hooks/UseGatewayFederations'; +import { gatewayApi } from '../../../../api/GatewayApi'; + +export const PegOutEcashCard = ({ setOpen }: { setOpen: () => void }) => { + const [modalOpen, setModalOpen] = useState(false); + + const [address, setAddress] = useState(''); + const [tokens, setTokens] = useState(0); + const [sendAll, setSendAll] = useState(false); + const [selectedFederation, setSelectedFederation] = useState(0); + const federations: Federation[] = useGatewayFederations(); + + const canSend = address !== '' && (sendAll || tokens > 0); + + const handlePegOut = ( + federationIdx: number, + tokenAmount: { sendAll?: boolean; tokens?: number }, + address: string + ) => { + const amountSat = sendAll ? 'all' : tokenAmount.tokens || 0; + gatewayApi + .requestWithdrawal( + federations[federationIdx].federation_id, + amountSat, + address + ) + .then(() => { + toast.success('Withdrawal request sent'); + setOpen(); + }) + .catch(e => { + toast.error('Error sending withdrawal request', e); + }); + }; + + const tokenAmount = sendAll ? { sendAll } : { tokens }; + + const renderButton = ( + onClick: () => void, + text: string, + selected: boolean + ) => ( + + {text} + + ); + + return ( + <> + setAddress(value)} + /> + + + setSelectedFederation(Number(e))} + options={federations.map(f => ({ + label: f.config.meta.federation_name || 'No connected Federations', + value: f.federation_id || 'No connected Federations', + }))} + value={{ + label: + federations[0].config.meta.federation_name || + 'No connected Federations', + value: federations[0].federation_id || 'No connected Federations', + }} + isClearable={false} + maxWidth={'500px'} + /> + + + + + {renderButton(() => setSendAll(true), 'Yes', sendAll)} + {renderButton(() => setSendAll(false), 'No', !sendAll)} + + + {!sendAll && ( + setTokens(Number(value))} + /> + )} + { + setModalOpen(true); + }} + > + Send + + setModalOpen(false)}> + + Send to Address + + {renderLine('Amount:', sendAll ? 'all' : )} + {renderLine('Address:', address)} + handlePegOut(selectedFederation, tokenAmount, address)} + disabled={!canSend} + withMargin={'16px 0 0'} + fullWidth={true} + arrow={true} + loading={false} + > + Send To Address + + + + ); +}; diff --git a/src/client/src/views/home/faucetActions/FaucetActions.tsx b/src/client/src/views/home/faucetActions/FaucetActions.tsx new file mode 100644 index 000000000..27401d24d --- /dev/null +++ b/src/client/src/views/home/faucetActions/FaucetActions.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + X, + Link, + CloudLightning, + FastForward, + GitPullRequest, +} from 'react-feather'; +import { + CardWithTitle, + SubTitle, + CardTitle, + SmallButton, +} from '../../../components/generic/Styled'; +import { + unSelectedNavButton, + cardColor, + cardBorderColor, + mediaWidths, +} from '../../../styles/Themes'; +import { Onchain } from './onchain'; +import { RequestChannel } from './request-channel'; +import { RefundFaucet } from './refund-faucet'; +import { PayInvoice } from './pay-invoice'; + +export const QuickCard = styled.div` + background: ${cardColor}; + box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1); + border-radius: 4px; + border: 1px solid ${cardBorderColor}; + height: 100px; + width: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 10px; + cursor: pointer; + color: #69c0ff; + + @media (${mediaWidths.mobile}) { + padding: 4px; + height: 80px; + width: 80px; + } + + &:hover { + border: 1px solid #69c0ff; + } +`; + +export const QuickTitle = styled.div` + font-size: 12px; + color: ${unSelectedNavButton}; + margin-top: 10px; + text-align: center; +`; + +const QuickRow = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 16px 0 32px; +`; + +export const FaucetActions = () => { + const [openCard, setOpenCard] = useState('none'); + + const getTitle = () => { + switch (openCard) { + case 'refund_faucet': + return 'Send your unused sats back to the Faucet'; + case 'pay_invoice': + return 'Pay an invoice with the Faucet'; + case 'request_channel': + return 'Request a channel from the Faucet'; + case 'onchain': + return 'Receive onchain sats from the Faucet'; + default: + return 'Mutinynet Faucet Actions'; + } + }; + + const renderContent = () => { + switch (openCard) { + case 'refund_faucet': + return ; + case 'request_channel': + return ; + case 'onchain': + return ; + case 'pay_invoice': + return ; + default: + return ( + + setOpenCard('onchain')}> + + Onchain + + setOpenCard('pay_invoice')}> + + Pay Invoice + + setOpenCard('refund_faucet')}> + + Refund Faucet + + setOpenCard('request_channel')}> + + Request Channel + + + ); + } + }; + + return ( + + + {getTitle()} + {openCard !== 'none' && ( + setOpenCard('none')}> + + + )} + + {renderContent()} + + ); +}; diff --git a/src/client/src/views/home/faucetActions/onchain.tsx b/src/client/src/views/home/faucetActions/onchain.tsx new file mode 100644 index 000000000..256b8c6c3 --- /dev/null +++ b/src/client/src/views/home/faucetActions/onchain.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { Card } from '../../../components/generic/Styled'; +import { InputWithDeco } from '../../../components/input/InputWithDeco'; +import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; +import { faucetApi } from '../../../api/FaucetApi'; + +export const Onchain = () => { + const [loading, setLoading] = useState(false); + const [amount, setAmount] = useState(50000); + const [address, setAddress] = useState(''); + + const handleOnchain = async () => { + setLoading(true); + + try { + await faucetApi.onchain({ sats: amount, address }); + toast.success('Successfully Paid to Onchain Address'); + } catch (err) { + toast.error((err as Error).message); + } + setLoading(false); + }; + + return ( + <> + + setAmount(Number(value))} + onEnter={() => handleOnchain()} + placeholder="Amount in sats" + title="Amount (sats)" + inputType="number" + /> + setAddress(value)} + onEnter={() => handleOnchain()} + /> + handleOnchain()} + > + Make it Rain + + + + ); +}; diff --git a/src/client/src/views/home/faucetActions/pay-invoice.tsx b/src/client/src/views/home/faucetActions/pay-invoice.tsx new file mode 100644 index 000000000..3bd0d2580 --- /dev/null +++ b/src/client/src/views/home/faucetActions/pay-invoice.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { Card } from '../../../components/generic/Styled'; +import { InputWithDeco } from '../../../components/input/InputWithDeco'; +import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; +import { faucetApi } from '../../../api/FaucetApi'; + +export const PayInvoice = () => { + const [loading, setLoading] = useState(false); + const [bolt11, setBolt11] = useState(''); + + const handlePayInvoice = async () => { + setLoading(true); + + try { + await faucetApi.payInvoice({ bolt11 }); + toast.success('Successfully Paid to Onchain Address'); + } catch (err) { + toast.error((err as Error).message); + } + setLoading(false); + }; + + return ( + <> + + setBolt11(value)} + onEnter={() => handlePayInvoice()} + placeholder="lnbt..." + title="Bolt11 Invoice" + /> + handlePayInvoice()} + > + Strike me now + + + + ); +}; diff --git a/src/client/src/views/home/faucetActions/refund-faucet.tsx b/src/client/src/views/home/faucetActions/refund-faucet.tsx new file mode 100644 index 000000000..2f3ec968d --- /dev/null +++ b/src/client/src/views/home/faucetActions/refund-faucet.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { Card } from '../../../components/generic/Styled'; +import { InputWithDeco } from '../../../components/input/InputWithDeco'; +import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; +import { faucetApi } from '../../../api/FaucetApi'; +import { usePayMutation } from '../../../graphql/mutations/__generated__/pay.generated'; + +export const RefundFaucet = () => { + const [loading, setLoading] = useState(false); + const [amount_sats, setAmount] = useState(50000); + + const [pay] = usePayMutation(); + + const handleRefundFaucet = async () => { + setLoading(true); + + try { + const request = await faucetApi.refundFaucet({ amount_sats }); + await pay({ + variables: { + request, + max_fee: 1000, + max_paths: 10, + }, + }); + } catch (err) { + toast.error((err as Error).message); + } + setLoading(false); + }; + + return ( + <> + + setAmount(Number(value))} + onEnter={() => handleRefundFaucet()} + placeholder="Amount in sats" + title="Amount (sats)" + inputType="number" + /> + handleRefundFaucet()} + > + Make it Rain + + + + ); +}; diff --git a/src/client/src/views/home/faucetActions/request-channel.tsx b/src/client/src/views/home/faucetActions/request-channel.tsx new file mode 100644 index 000000000..6bf81b0d7 --- /dev/null +++ b/src/client/src/views/home/faucetActions/request-channel.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { Card } from '../../../components/generic/Styled'; +import { InputWithDeco } from '../../../components/input/InputWithDeco'; +import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; +import { faucetApi } from '../../../api/FaucetApi'; +import { useGetNodeInfoQuery } from '../../../graphql/queries/__generated__/getNodeInfo.generated'; +import { Container, IconStyle, Item } from '../quickActions/openChannel'; +import { Hexagon } from 'react-feather'; + +const signetNodes = [ + { + name: '025698cc9ac623f5d1ba', + pubkey: + '025698cc9ac623f5d1baf56310f2f1b62dfffee43ffcdb2c20ccb541f70497d540', + host: '54.158.203.78', + connectionString: + '025698cc9ac623f5d1baf56310f2f1b62dfffee43ffcdb2c20ccb541f70497d540@54.158.203.78:9739', + }, + { + name: 'mutiny-net-lnd', + pubkey: + '02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b', + host: '45.79.52.207', + connectionString: + '02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b@45.79.52.207:9735', + }, + { + name: 'GREENFELONY', + pubkey: + '0366abc8eb4da61e31a8d2c4520d31cabdf58cc5250f855657397f3dd62493938a', + host: '45.33.17.66', + connectionString: + '0366abc8eb4da61e31a8d2c4520d31cabdf58cc5250f855657397f3dd62493938a@45.33.17.66:39735', + }, +]; + +export const RequestChannel = () => { + const [loading, setLoading] = useState(false); + + const [capacity, setCapacity] = useState(50000); + const [push_amount, setPushAmount] = useState(25000); + const [pubkey, setPubkey] = useState(''); + const [host, setHost] = useState(''); + const [port, setPort] = useState(9735); + + const { data } = useGetNodeInfoQuery(); + + const handleRequestChannel = async () => { + setLoading(true); + + try { + await faucetApi.requestChannel({ + capacity, + push_amount, + pubkey, + host: host + ':' + port, + }); + toast.success('Channel Opened'); + } catch (err) { + toast.error((err as Error).message); + } + setLoading(false); + }; + + useEffect(() => { + if ( + data?.getNodeInfo?.public_key && + typeof data.getNodeInfo.public_key === 'string' && + pubkey.length === 0 + ) { + setPubkey(data.getNodeInfo.public_key); + } + }, [data, pubkey]); + + return ( + <> + + + {signetNodes.map((item, index) => ( + { + const [pubkey, host, port] = item.connectionString.split(/@|:/); + setPubkey(pubkey); + setHost(host); + setPort(Number(port)); + }} + > + + + + {item.name} + + ))} + + setCapacity(Number(value))} + onEnter={() => handleRequestChannel()} + placeholder="50000" + title="Capacity (sats)" + inputType="number" + /> + setPushAmount(Number(value))} + onEnter={() => handleRequestChannel()} + placeholder="25000" + title="Push Amount (Sats)" + inputType="number" + /> + setHost(value)} + onEnter={() => handleRequestChannel()} + placeholder="127.0.0.1" + title="Host" + /> + + handleRequestChannel()} + > + Gimme a lightning channel + + + + ); +}; diff --git a/src/client/src/views/home/quickActions/QuickActions.tsx b/src/client/src/views/home/quickActions/QuickActions.tsx index d36b872e5..7138cd0ba 100644 --- a/src/client/src/views/home/quickActions/QuickActions.tsx +++ b/src/client/src/views/home/quickActions/QuickActions.tsx @@ -22,6 +22,7 @@ import { LnUrlCard } from './lnurl'; import { LnMarketsCard } from './lnmarkets'; import { AmbossCard } from './amboss/AmbossCard'; import { LightningAddressCard } from './lightningAddress/LightningAddress'; +import { GhostCard } from './ghost/GhostQuickAction'; export const QuickCard = styled.div` background: ${cardColor}; @@ -34,9 +35,7 @@ export const QuickCard = styled.div` flex-direction: column; justify-content: center; align-items: center; - margin-bottom: 25px; padding: 10px; - margin-right: 10px; cursor: pointer; color: #69c0ff; @@ -61,6 +60,8 @@ export const QuickTitle = styled.div` const QuickRow = styled.div` display: flex; flex-wrap: wrap; + gap: 8px; + margin: 16px 0 32px; `; export const QuickActions = () => { @@ -104,6 +105,7 @@ export const QuickActions = () => { default: return ( + setOpenCard('support')} /> setOpenCard('lightning_address')}> diff --git a/src/client/src/views/home/quickActions/ghost/GhostQuickAction.tsx b/src/client/src/views/home/quickActions/ghost/GhostQuickAction.tsx new file mode 100644 index 000000000..dd8ac457b --- /dev/null +++ b/src/client/src/views/home/quickActions/ghost/GhostQuickAction.tsx @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import { + cardBorderColor, + cardColor, + mediaWidths, + unSelectedNavButton, +} from '../../../../styles/Themes'; +import { GhostLogo } from '../../../../components/logo/GhostIcon'; +import { useRouter } from 'next/router'; + +const QuickTitle = styled.div` + font-size: 12px; + color: ${unSelectedNavButton}; + margin-top: 10px; +`; + +const QuickCard = styled.div` + background: ${cardColor}; + box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1); + border-radius: 4px; + border: 1px solid ${cardBorderColor}; + height: 100px; + width: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 10px; + cursor: pointer; + color: #69c0ff; + + @media (${mediaWidths.mobile}) { + padding: 4px; + height: 80px; + width: 80px; + } + + &:hover { + background-color: black; + color: white; + + & ${QuickTitle} { + color: white; + } + } +`; + +export const GhostCard = () => { + const { push } = useRouter(); + + return ( + push('/amboss')}> + + Ghost + + ); +}; diff --git a/src/client/src/views/home/quickActions/openChannel/index.tsx b/src/client/src/views/home/quickActions/openChannel/index.tsx index 3f39e6836..65cced865 100644 --- a/src/client/src/views/home/quickActions/openChannel/index.tsx +++ b/src/client/src/views/home/quickActions/openChannel/index.tsx @@ -31,12 +31,12 @@ import { BaseNode } from '../../../../graphql/types'; import { OpenChannelCard } from './OpenChannel'; import { OpenRecommended } from './OpenRecommended'; -const IconStyle = styled.div` +export const IconStyle = styled.div` margin-bottom: 8px; color: ${themeColors.blue2}; `; -const Item = styled.div` +export const Item = styled.div` font-size: 14px; display: flex; flex-direction: column; @@ -67,7 +67,7 @@ const Item = styled.div` } `; -const Container = styled.div` +export const Container = styled.div` display: flex; justify-content: center; flex-wrap: wrap; @@ -88,6 +88,7 @@ export const OpenChannel = ({ setOpenCard }: OpenChannelProps) => { if (!loading && data && data.getBaseNodes) { if (data.getBaseNodes.length > 0) { set(true); + console.log(data.getBaseNodes); } } }, [loading, data]); diff --git a/src/server/config/configuration.ts b/src/server/config/configuration.ts index 333193535..214d33422 100644 --- a/src/server/config/configuration.ts +++ b/src/server/config/configuration.ts @@ -50,6 +50,11 @@ type AmbossConfig = { disableBalancePushes: boolean; }; +type FedimintGatewayConfig = { + apiUrl: string; + password: string; +}; + type ConfigType = { basePath: string; isProduction: boolean; @@ -69,6 +74,7 @@ type ConfigType = { headers: Headers; subscriptions: SubscriptionsConfig; amboss: AmbossConfig; + fedimintGateway: FedimintGatewayConfig; }; export default (): ConfigType => { @@ -137,6 +143,11 @@ export default (): ConfigType => { disableBalancePushes: process.env.DISABLE_BALANCE_PUSHES === 'true', }; + const fedimintGateway = { + apiUrl: process.env.FM_GATEWAY_API || '', + password: process.env.FM_GATEWAY_PASSWORD || '', + }; + const config: ConfigType = { logJson: process.env.LOG_JSON === 'true', masterPasswordOverride: process.env.MASTER_PASSWORD_OVERRIDE || '', @@ -156,6 +167,7 @@ export default (): ConfigType => { yamlEnvs, subscriptions, amboss, + fedimintGateway, }; if (!isProduction) {