mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-26 15:42:30 +01:00
commit
75e2c9b2c0
116 changed files with 9128 additions and 25987 deletions
|
@ -6,6 +6,10 @@ tests
|
||||||
venv
|
venv
|
||||||
tools
|
tools
|
||||||
|
|
||||||
|
lnbits/static/css/*
|
||||||
|
lnbits/static/bundle.js
|
||||||
|
lnbits/static/bundle.css
|
||||||
|
|
||||||
*.md
|
*.md
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|
12
.env.example
12
.env.example
|
@ -34,19 +34,23 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
|
||||||
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
|
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
|
||||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||||
|
|
||||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
|
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet
|
||||||
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
|
# LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
|
||||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||||
# just so you can see the UI before dealing with this file.
|
# just so you can see the UI before dealing with this file.
|
||||||
|
|
||||||
# Set one of these blocks depending on the wallet kind you chose above:
|
# Set one of these blocks depending on the wallet kind you chose above:
|
||||||
|
|
||||||
|
# ClicheWallet
|
||||||
|
CLICHE_ENDPOINT=ws://127.0.0.1:12000
|
||||||
|
|
||||||
# SparkWallet
|
# SparkWallet
|
||||||
SPARK_URL=http://localhost:9737/rpc
|
SPARK_URL=http://localhost:9737/rpc
|
||||||
SPARK_TOKEN=myaccesstoken
|
SPARK_TOKEN=myaccesstoken
|
||||||
|
|
||||||
# CLightningWallet
|
# CoreLightningWallet
|
||||||
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||||
|
|
||||||
# LnbitsWallet
|
# LnbitsWallet
|
||||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||||
|
|
35
.github/workflows/formatting.yml
vendored
35
.github/workflows/formatting.yml
vendored
|
@ -7,30 +7,19 @@ on:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
black:
|
checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- run: sudo apt-get install python3-venv
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
- run: python3 -m venv venv
|
- name: Install packages
|
||||||
- run: ./venv/bin/pip install black
|
run: poetry install
|
||||||
- run: make checkblack
|
- name: Check black
|
||||||
isort:
|
run: make checkblack
|
||||||
runs-on: ubuntu-latest
|
- name: Check isort
|
||||||
steps:
|
run: make checkisort
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
- run: sudo apt-get install python3-venv
|
- name: Check prettier
|
||||||
- run: python3 -m venv venv
|
run: |
|
||||||
- run: ./venv/bin/pip install isort
|
npm install prettier
|
||||||
- run: make checkisort
|
make checkprettier
|
||||||
|
|
||||||
prettier:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
- run: sudo apt-get install python3-venv
|
|
||||||
- run: python3 -m venv venv
|
|
||||||
- run: npm install prettier
|
|
||||||
- run: ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
|
|
||||||
|
|
17
.github/workflows/migrations.yml
vendored
17
.github/workflows/migrations.yml
vendored
|
@ -22,22 +22,17 @@ jobs:
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8]
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
poetry install
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
sudo apt install unzip
|
sudo apt install unzip
|
||||||
- name: Run migrations
|
- name: Run migrations
|
||||||
run: |
|
run: |
|
||||||
|
@ -45,7 +40,7 @@ jobs:
|
||||||
mkdir -p ./data
|
mkdir -p ./data
|
||||||
export LNBITS_DATA_FOLDER="./data"
|
export LNBITS_DATA_FOLDER="./data"
|
||||||
unzip tests/data/mock_data.zip -d ./data
|
unzip tests/data/mock_data.zip -d ./data
|
||||||
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||||
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
|
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
|
||||||
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||||
./venv/bin/python tools/conv.py
|
poetry run python tools/conv.py
|
||||||
|
|
17
.github/workflows/mypy.yml
vendored
17
.github/workflows/mypy.yml
vendored
|
@ -5,9 +5,18 @@ on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
- uses: jpetrucciani/mypy-check@master
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
mypy_flags: '--install-types --non-interactive'
|
python-version: ${{ matrix.python-version }}
|
||||||
path: 'lnbits'
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
poetry install
|
||||||
|
- name: Run tests
|
||||||
|
run: poetry run mypy
|
||||||
|
|
77
.github/workflows/regtest.yml
vendored
77
.github/workflows/regtest.yml
vendored
|
@ -7,13 +7,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8]
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
|
@ -22,15 +23,8 @@ jobs:
|
||||||
./tests
|
./tests
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
poetry install
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pylightning
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
env:
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
|
@ -43,7 +37,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||||
make test-real-wallet
|
make test-real-wallet
|
||||||
CLightningWallet:
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
LndWallet:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -54,6 +52,7 @@ jobs:
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
|
@ -62,22 +61,60 @@ jobs:
|
||||||
./tests
|
./tests
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
poetry install
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
poetry add grpcio protobuf
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pylightning
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
env:
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
PORT: 5123
|
PORT: 5123
|
||||||
LNBITS_DATA_FOLDER: ./data
|
LNBITS_DATA_FOLDER: ./data
|
||||||
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
|
LNBITS_BACKEND_WALLET_CLASS: LndWallet
|
||||||
CLIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
LND_GRPC_ENDPOINT: localhost
|
||||||
|
LND_GRPC_PORT: 10009
|
||||||
|
LND_GRPC_CERT: docker/data/lnd-1/tls.cert
|
||||||
|
LND_GRPC_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
run: |
|
run: |
|
||||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||||
make test-real-wallet
|
make test-real-wallet
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
CoreLightningWallet:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.9]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
|
- name: Setup Regtest
|
||||||
|
run: |
|
||||||
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
|
cd docker
|
||||||
|
chmod +x ./tests
|
||||||
|
./tests
|
||||||
|
sudo chmod -R a+rwx .
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
poetry install
|
||||||
|
poetry add pyln-client
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
PYTHONUNBUFFERED: 1
|
||||||
|
PORT: 5123
|
||||||
|
LNBITS_DATA_FOLDER: ./data
|
||||||
|
LNBITS_BACKEND_WALLET_CLASS: CoreLightningWallet
|
||||||
|
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
||||||
|
run: |
|
||||||
|
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||||
|
make test-real-wallet
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
|
96
.github/workflows/tests.yml
vendored
96
.github/workflows/tests.yml
vendored
|
@ -4,71 +4,6 @@ on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
venv-sqlite:
|
venv-sqlite:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: [3.7, 3.8, 3.9]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
- name: Run tests
|
|
||||||
run: make test
|
|
||||||
venv-postgres:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:latest
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: postgres
|
|
||||||
ports:
|
|
||||||
# maps tcp port 5432 on service container to the host
|
|
||||||
- 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: [3.8]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
- name: Run tests
|
|
||||||
env:
|
|
||||||
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
|
|
||||||
run: make test
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
file: ./coverage.xml
|
|
||||||
poetry-sqlite:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -88,9 +23,29 @@ jobs:
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
./venv/bin/python -m pip install --upgrade pip
|
||||||
./venv/bin/pip install -r requirements.txt
|
./venv/bin/pip install -r requirements.txt
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
||||||
|
- name: Run tests
|
||||||
|
run: make test-venv
|
||||||
|
sqlite:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.9]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
VIRTUAL_ENV: ./venv
|
||||||
|
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
||||||
|
run: |
|
||||||
|
poetry install
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
poetry-postgres:
|
postgres:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -116,15 +71,10 @@ jobs:
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: abatilo/actions-poetry@v2.1.3
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
|
||||||
VIRTUAL_ENV: ./venv
|
|
||||||
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m venv ${{ env.VIRTUAL_ENV }}
|
poetry install
|
||||||
./venv/bin/python -m pip install --upgrade pip
|
|
||||||
./venv/bin/pip install -r requirements.txt
|
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
env:
|
||||||
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
|
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
|
||||||
|
|
51
Dockerfile
51
Dockerfile
|
@ -1,45 +1,12 @@
|
||||||
# Build image
|
FROM python:3.9-slim
|
||||||
FROM python:3.7-slim as builder
|
|
||||||
|
|
||||||
# Setup virtualenv
|
|
||||||
ENV VIRTUAL_ENV=/opt/venv
|
|
||||||
RUN python -m venv $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
# Install build deps
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev
|
RUN apt-get install -y curl
|
||||||
RUN python -m pip install --upgrade pip
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
RUN pip install wheel
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
# Install runtime deps
|
|
||||||
COPY requirements.txt /tmp/requirements.txt
|
|
||||||
RUN pip install -r /tmp/requirements.txt
|
|
||||||
|
|
||||||
# Install c-lightning specific deps
|
|
||||||
RUN pip install pylightning
|
|
||||||
|
|
||||||
# Install LND specific deps
|
|
||||||
RUN pip install lndgrpc
|
|
||||||
|
|
||||||
# Production image
|
|
||||||
FROM python:3.7-slim as lnbits
|
|
||||||
|
|
||||||
# Run as non-root
|
|
||||||
USER 1000:1000
|
|
||||||
|
|
||||||
# Copy over virtualenv
|
|
||||||
ENV VIRTUAL_ENV="/opt/venv"
|
|
||||||
COPY --from=builder --chown=1000:1000 $VIRTUAL_ENV $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
# Copy in app source
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --chown=1000:1000 lnbits /app/lnbits
|
COPY . .
|
||||||
|
RUN poetry config virtualenvs.create false
|
||||||
ENV LNBITS_PORT="5000"
|
RUN poetry install --no-dev --no-root
|
||||||
ENV LNBITS_HOST="0.0.0.0"
|
RUN poetry run python build.py
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
CMD ["poetry", "run", "lnbits", "--port", "5000", "--host", "0.0.0.0"]
|
||||||
CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"]
|
|
||||||
|
|
45
Makefile
45
Makefile
|
@ -4,58 +4,47 @@ all: format check requirements.txt
|
||||||
|
|
||||||
format: prettier isort black
|
format: prettier isort black
|
||||||
|
|
||||||
check: mypy checkprettier checkblack
|
check: mypy checkprettier checkisort checkblack
|
||||||
|
|
||||||
prettier: $(shell find lnbits -name "*.js" -name ".html")
|
prettier: $(shell find lnbits -name "*.js" -name ".html")
|
||||||
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
|
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
black: $(shell find lnbits -name "*.py")
|
black:
|
||||||
./venv/bin/black lnbits
|
poetry run black .
|
||||||
|
|
||||||
mypy: $(shell find lnbits -name "*.py")
|
mypy:
|
||||||
./venv/bin/mypy lnbits
|
poetry run mypy
|
||||||
./venv/bin/mypy lnbits/core
|
|
||||||
./venv/bin/mypy lnbits/extensions/*
|
|
||||||
|
|
||||||
isort: $(shell find lnbits -name "*.py")
|
isort:
|
||||||
./venv/bin/isort --profile black lnbits
|
poetry run isort .
|
||||||
|
|
||||||
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
|
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
|
||||||
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
|
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
checkblack: $(shell find lnbits -name "*.py")
|
checkblack:
|
||||||
./venv/bin/black --check lnbits
|
poetry run black --check .
|
||||||
|
|
||||||
checkisort: $(shell find lnbits -name "*.py")
|
checkisort:
|
||||||
./venv/bin/isort --profile black --check-only lnbits
|
poetry run isort --check-only .
|
||||||
|
|
||||||
Pipfile.lock: Pipfile
|
|
||||||
./venv/bin/pipenv lock
|
|
||||||
|
|
||||||
requirements.txt: Pipfile.lock
|
|
||||||
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
mkdir -p ./tests/data
|
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
FAKE_WALLET_SECRET="ToTheMoon1" \
|
FAKE_WALLET_SECRET="ToTheMoon1" \
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
|
poetry run pytest
|
||||||
|
|
||||||
test-real-wallet:
|
test-real-wallet:
|
||||||
mkdir -p ./tests/data
|
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
|
poetry run pytest
|
||||||
|
|
||||||
test-pipenv:
|
test-venv:
|
||||||
mkdir -p ./tests/data
|
|
||||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||||
FAKE_WALLET_SECRET="ToTheMoon1" \
|
FAKE_WALLET_SECRET="ToTheMoon1" \
|
||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
pipenv run pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
|
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
|
||||||
|
|
||||||
bak:
|
bak:
|
||||||
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
|
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
|
||||||
|
|
|
@ -25,7 +25,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
|
||||||
|
|
||||||
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
|
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
|
||||||
|
|
||||||
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
See [legend.lnbits.org](https://legend.lnbits.org) for more detailed documentation.
|
||||||
|
|
||||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ LNURL has a fallback scheme, so if scanned by a regular QR code reader it can de
|
||||||

|

|
||||||
|
|
||||||
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
||||||
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
|
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will still be able to access the funds.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ Wallets can be easily generated and given out to people at events (one click mul
|
||||||
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||||
|
|
||||||
|
|
||||||
[docs]: https://lnbits.org/
|
[docs]: https://legend.lnbits.org/
|
||||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||||
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
|
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
|
||||||
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
|
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
|
||||||
|
|
21
build.py
21
build.py
|
@ -1,13 +1,14 @@
|
||||||
import warnings
|
|
||||||
import subprocess
|
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import warnings
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Any, List, NamedTuple, Optional
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, List, NamedTuple, Optional
|
||||||
|
|
||||||
LNBITS_PATH = path.dirname(path.realpath(__file__)) + "/lnbits"
|
LNBITS_PATH = path.dirname(path.realpath(__file__)) + "/lnbits"
|
||||||
|
|
||||||
|
|
||||||
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
|
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
|
||||||
paths = get_vendored(".js", prefer_minified)
|
paths = get_vendored(".js", prefer_minified)
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
|
||||||
def url_for_vendored(abspath: str) -> str:
|
def url_for_vendored(abspath: str) -> str:
|
||||||
return "/" + os.path.relpath(abspath, LNBITS_PATH)
|
return "/" + os.path.relpath(abspath, LNBITS_PATH)
|
||||||
|
|
||||||
|
|
||||||
def transpile_scss():
|
def transpile_scss():
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
|
@ -80,6 +82,7 @@ def transpile_scss():
|
||||||
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
|
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
|
||||||
css.write(compile_string(scss.read()))
|
css.write(compile_string(scss.read()))
|
||||||
|
|
||||||
|
|
||||||
def bundle_vendored():
|
def bundle_vendored():
|
||||||
for getfiles, outputpath in [
|
for getfiles, outputpath in [
|
||||||
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
|
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
|
||||||
|
@ -96,15 +99,7 @@ def bundle_vendored():
|
||||||
def build():
|
def build():
|
||||||
transpile_scss()
|
transpile_scss()
|
||||||
bundle_vendored()
|
bundle_vendored()
|
||||||
# root = Path("lnbits/static/foo")
|
|
||||||
# root.mkdir(parents=True)
|
|
||||||
# root.joinpath("example.css").write_text("")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
build()
|
build()
|
||||||
|
|
||||||
#def build(setup_kwargs):
|
|
||||||
# """Build """
|
|
||||||
# transpile_scss()
|
|
||||||
# bundle_vendored()
|
|
||||||
# subprocess.run(["ls", "-la", "./lnbits/static"])
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
lnbits.org
|
legend.lnbits.org
|
|
@ -3,7 +3,7 @@ title: "LNbits docs"
|
||||||
remote_theme: pmarsceill/just-the-docs
|
remote_theme: pmarsceill/just-the-docs
|
||||||
logo: "/logos/lnbits-full.png"
|
logo: "/logos/lnbits-full.png"
|
||||||
search_enabled: true
|
search_enabled: true
|
||||||
url: https://lnbits.org
|
url: https://legend.lnbits.org
|
||||||
aux_links:
|
aux_links:
|
||||||
"LNbits on GitHub":
|
"LNbits on GitHub":
|
||||||
- "//github.com/lnbits/lnbits"
|
- "//github.com/lnbits/lnbits"
|
||||||
|
|
|
@ -9,4 +9,4 @@ nav_order: 3
|
||||||
API reference
|
API reference
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Coming soon...
|
[Swagger Docs](https://legend.lnbits.org/devs/swagger.html)
|
||||||
|
|
|
@ -17,10 +17,26 @@ Tests
|
||||||
|
|
||||||
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
|
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
|
||||||
```bash
|
```bash
|
||||||
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
|
poetry install
|
||||||
|
npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
Then to run the tests:
|
Then to run the tests:
|
||||||
```bash
|
```bash
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run formatting:
|
||||||
|
```bash
|
||||||
|
make format
|
||||||
|
```
|
||||||
|
|
||||||
|
Run mypy checks:
|
||||||
|
```bash
|
||||||
|
poetry run mypy
|
||||||
|
```
|
||||||
|
|
||||||
|
Run everything:
|
||||||
|
```bash
|
||||||
|
make all
|
||||||
|
```
|
||||||
|
|
|
@ -28,17 +28,24 @@ Going over the example extension's structure:
|
||||||
Adding new dependencies
|
Adding new dependencies
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
If for some reason your extensions needs a new python package to work, you can add a new package using Pipenv:
|
If for some reason your extensions needs a new python package to work, you can add a new package using `venv`, or `poerty`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ pipenv install new_package_name
|
$ poetry add <package>
|
||||||
|
# or
|
||||||
|
$ ./venv/bin/pip install <package>
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create a new entry in the `Pipenv` file.
|
|
||||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
**But we need an extra step to make sure LNbits doesn't break in production.**
|
||||||
All tests and deployments should run against the `requirements.txt` file so every time a new package is added
|
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry`.
|
||||||
it is necessary to run the Pipenv `lock` command and manually update the requirements file:
|
`nix` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||||
|
|
||||||
```sh
|
|
||||||
$ pipenv lock -r
|
SQLite to PostgreSQL migration
|
||||||
```
|
-----------------------
|
||||||
|
|
||||||
|
LNbits currently supports SQLite and PostgreSQL databases. There is a migration script `tools/conv.py` that helps users migrate from SQLite to PostgreSQL. This script also copies all extension databases to the new backend.
|
||||||
|
|
||||||
|
### Adding mock data to `mock_data.zip`
|
||||||
|
|
||||||
|
`mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR.
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
layout: default
|
|
||||||
parent: For developers
|
|
||||||
title: Installation
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
This guide has been moved to the [installation guide](../guide/installation.md).
|
|
||||||
To install the developer packages for running tests etc before pr'ing, use `./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock black mypy isort`.
|
|
||||||
|
|
||||||
## Notes:
|
|
||||||
|
|
||||||
* We recommend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
|
|
||||||
* <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session.
|
|
29
docs/devs/swagger.html
Normal file
29
docs/devs/swagger.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Load the latest Swagger UI code and style from npm using unpkg.com -->
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css"/>
|
||||||
|
<title>My New API</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div> <!-- Div to hold the UI component -->
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
// Begin Swagger UI call region
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "https://legend.lnbits.com/openapi.json", //Location of Open API spec in the repo
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
})
|
||||||
|
window.ui = ui
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -4,8 +4,6 @@ title: Basic installation
|
||||||
nav_order: 2
|
nav_order: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Basic installation
|
# Basic installation
|
||||||
|
|
||||||
You can choose between four package managers, `poetry`, `nix` and `venv`.
|
You can choose between four package managers, `poetry`, `nix` and `venv`.
|
||||||
|
@ -18,12 +16,22 @@ By default, LNbits will use SQLite as its database. You can also use PostgreSQL
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
git clone https://github.com/lnbits/lnbits-legend.git
|
||||||
cd lnbits-legend/
|
cd lnbits-legend/
|
||||||
|
|
||||||
|
# for making sure python 3.9 is installed, skip if installed
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install software-properties-common
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt install python3.9
|
||||||
|
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
poetry install
|
export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal
|
||||||
|
poetry env use python3.9
|
||||||
|
poetry install --no-dev
|
||||||
|
|
||||||
|
mkdir data
|
||||||
|
cp .env.example .env
|
||||||
|
sudo nano .env # set funding source
|
||||||
|
|
||||||
# You may need to install python 3.9, update your python following this guide https://linuxize.com/post/how-to-install-python-3-9-on-ubuntu-20-04/
|
|
||||||
|
|
||||||
mkdir data && cp .env.example .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Running the server
|
#### Running the server
|
||||||
|
@ -76,6 +84,17 @@ mkdir data && cp .env.example .env
|
||||||
|
|
||||||
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
|
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
|
||||||
|
|
||||||
|
## Option 4: Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/lnbits/lnbits-legend.git
|
||||||
|
cd lnbits-legend
|
||||||
|
docker build -t lnbits-legend .
|
||||||
|
cp .env.example .env
|
||||||
|
mkdir data
|
||||||
|
docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
|
||||||
|
```
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
Problems installing? These commands have helped us install LNbits.
|
Problems installing? These commands have helped us install LNbits.
|
||||||
|
@ -213,16 +232,22 @@ chmod +x mkcert-v*-linux-amd64
|
||||||
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
|
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
|
||||||
```
|
```
|
||||||
#### Create certificate
|
#### Create certificate
|
||||||
To create a certificate, first `cd` into your lnbits folder and execute the following command ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/))
|
To create a certificate, first `cd` into your LNbits folder and execute the following command on Linux:
|
||||||
|
```sh
|
||||||
|
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.pem -keyout key.pem
|
||||||
|
```
|
||||||
|
This will create two new files (`key.pem` and `cert.pem `).
|
||||||
|
|
||||||
|
Alternatively, you can use mkcert ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/)):
|
||||||
```sh
|
```sh
|
||||||
# add your local IP (192.x.x.x) as well if you want to use it in your local network
|
# add your local IP (192.x.x.x) as well if you want to use it in your local network
|
||||||
mkcert localhost 127.0.0.1 ::1
|
mkcert localhost 127.0.0.1 ::1
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create two new files (`localhost-key.pem` and `localhost.pem `) which you can then pass to uvicorn when you start LNbits:
|
You can then pass the certificate files to uvicorn when you start LNbits:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./localhost-key.pem --ssl-certfile ./localhost.pem
|
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -235,9 +260,9 @@ If you want to run LNbits on your Umbrel but want it to be reached through clear
|
||||||
To install using docker you first need to build the docker image as:
|
To install using docker you first need to build the docker image as:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits-legend.git
|
||||||
cd lnbits/ # ${PWD} referred as <lnbits_repo>
|
cd lnbits-legend
|
||||||
docker build -t lnbits .
|
docker build -t lnbits-legend .
|
||||||
```
|
```
|
||||||
|
|
||||||
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
|
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
|
||||||
|
@ -248,17 +273,15 @@ cp <lnbits_repo>/.env.example .env
|
||||||
|
|
||||||
and change the configuration in `.env` as required.
|
and change the configuration in `.env` as required.
|
||||||
|
|
||||||
Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container.
|
Then create the data directory
|
||||||
|
|
||||||
```
|
```
|
||||||
mkdir data
|
mkdir data
|
||||||
sudo chown 1000:1000 ./data/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then the image can be run as:
|
Then the image can be run as:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits
|
docker run --detach --publish 5000:5000 --name lnbits-legend -e "LNBITS_BACKEND_WALLET_CLASS='FakeWallet'" --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally you can access your lnbits on your machine at port 5000.
|
Finally you can access your lnbits on your machine at port 5000.
|
||||||
|
|
|
@ -8,18 +8,17 @@ nav_order: 3
|
||||||
Backend wallets
|
Backend wallets
|
||||||
===============
|
===============
|
||||||
|
|
||||||
LNbits can run on top of many lightning-network funding sources. Currently there is support for
|
LNbits can run on top of many lightning-network funding sources. Currently there is support for CoreLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularly.
|
||||||
CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily.
|
|
||||||
|
|
||||||
A backend wallet can be configured using the following LNbits environment variables:
|
A backend wallet can be configured using the following LNbits environment variables:
|
||||||
|
|
||||||
|
|
||||||
### CLightning
|
### CoreLightning
|
||||||
|
|
||||||
Using this wallet requires the installation of the `pylightning` Python package.
|
Using this wallet requires the installation of the `pylightning` Python package.
|
||||||
|
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
||||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
|
||||||
|
|
||||||
### Spark (c-lightning)
|
### Spark (c-lightning)
|
||||||
|
|
||||||
|
@ -27,6 +26,17 @@ Using this wallet requires the installation of the `pylightning` Python package.
|
||||||
- `SPARK_URL`: http://10.147.17.230:9737/rpc
|
- `SPARK_URL`: http://10.147.17.230:9737/rpc
|
||||||
- `SPARK_TOKEN`: secret_access_key
|
- `SPARK_TOKEN`: secret_access_key
|
||||||
|
|
||||||
|
### LND (REST)
|
||||||
|
|
||||||
|
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
||||||
|
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
|
||||||
|
- `LND_REST_CERT`: /file/path/tls.cert
|
||||||
|
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||||
|
|
||||||
### LND (gRPC)
|
### LND (gRPC)
|
||||||
|
|
||||||
Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages.
|
Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages.
|
||||||
|
@ -43,17 +53,6 @@ You can also use an AES-encrypted macaroon (more info) instead by using
|
||||||
|
|
||||||
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
|
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
|
||||||
|
|
||||||
### LND (REST)
|
|
||||||
|
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
|
||||||
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
|
|
||||||
- `LND_REST_CERT`: /file/path/tls.cert
|
|
||||||
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
|
||||||
|
|
||||||
### LNbits
|
### LNbits
|
||||||
|
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
|
||||||
|
|
|
@ -4,6 +4,8 @@ from typing import Any, Dict, List, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import COCKROACH, POSTGRES, Connection
|
from lnbits.db import COCKROACH, POSTGRES, Connection
|
||||||
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
|
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
|
||||||
|
@ -334,7 +336,7 @@ async def delete_expired_invoices(
|
||||||
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
if expiration_date > datetime.datetime.utcnow():
|
if expiration_date > datetime.datetime.utcnow():
|
||||||
continue
|
continue
|
||||||
|
logger.debug(f"Deleting expired invoice: {invoice.payment_hash}")
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
DELETE FROM apipayments
|
DELETE FROM apipayments
|
||||||
|
|
|
@ -141,19 +141,25 @@ class Payment(BaseModel):
|
||||||
if self.is_uncheckable:
|
if self.is_uncheckable:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
|
||||||
|
)
|
||||||
|
|
||||||
if self.is_out:
|
if self.is_out:
|
||||||
status = await WALLET.get_payment_status(self.checking_id)
|
status = await WALLET.get_payment_status(self.checking_id)
|
||||||
else:
|
else:
|
||||||
status = await WALLET.get_invoice_status(self.checking_id)
|
status = await WALLET.get_invoice_status(self.checking_id)
|
||||||
|
|
||||||
|
logger.debug(f"Status: {status}")
|
||||||
|
|
||||||
if self.is_out and status.failed:
|
if self.is_out and status.failed:
|
||||||
logger.info(
|
logger.info(
|
||||||
f" - deleting outgoing failed payment {self.checking_id}: {status}"
|
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||||
)
|
)
|
||||||
await self.delete()
|
await self.delete()
|
||||||
elif not status.pending:
|
elif not status.pending:
|
||||||
logger.info(
|
logger.info(
|
||||||
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
||||||
)
|
)
|
||||||
await self.set_pending(status.pending)
|
await self.set_pending(status.pending)
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,7 @@ async def pay_invoice(
|
||||||
payment_request, fee_reserve_msat
|
payment_request, fee_reserve_msat
|
||||||
)
|
)
|
||||||
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
||||||
if payment.checking_id:
|
if payment.ok and payment.checking_id:
|
||||||
logger.debug(f"creating final payment {payment.checking_id}")
|
logger.debug(f"creating final payment {payment.checking_id}")
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
await create_payment(
|
await create_payment(
|
||||||
|
@ -196,7 +196,7 @@ async def pay_invoice(
|
||||||
logger.debug(f"deleting temporary payment {temp_id}")
|
logger.debug(f"deleting temporary payment {temp_id}")
|
||||||
await delete_payment(temp_id, conn=conn)
|
await delete_payment(temp_id, conn=conn)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"backend payment failed, no checking_id {temp_id}")
|
logger.debug(f"backend payment failed")
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
logger.debug(f"deleting temporary payment {temp_id}")
|
logger.debug(f"deleting temporary payment {temp_id}")
|
||||||
await delete_payment(temp_id, conn=conn)
|
await delete_payment(temp_id, conn=conn)
|
||||||
|
@ -337,13 +337,16 @@ async def perform_lnurlauth(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_invoice_status(
|
async def check_transaction_status(
|
||||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
||||||
if not payment:
|
if not payment:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
status = await WALLET.get_invoice_status(payment.checking_id)
|
if payment.is_out:
|
||||||
|
status = await WALLET.get_payment_status(payment.checking_id)
|
||||||
|
else:
|
||||||
|
status = await WALLET.get_invoice_status(payment.checking_id)
|
||||||
if not payment.pending:
|
if not payment.pending:
|
||||||
return status
|
return status
|
||||||
if payment.is_out and status.failed:
|
if payment.is_out and status.failed:
|
||||||
|
|
|
@ -689,7 +689,7 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
<q-tabs
|
<q-tabs
|
||||||
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
|
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
||||||
active-class="px-0"
|
active-class="px-0"
|
||||||
indicator-color="transparent"
|
indicator-color="transparent"
|
||||||
>
|
>
|
||||||
|
|
|
@ -48,7 +48,7 @@ from ..crud import (
|
||||||
from ..services import (
|
from ..services import (
|
||||||
InvoiceFailure,
|
InvoiceFailure,
|
||||||
PaymentFailure,
|
PaymentFailure,
|
||||||
check_invoice_status,
|
check_transaction_status,
|
||||||
create_invoice,
|
create_invoice,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
perform_lnurlauth,
|
perform_lnurlauth,
|
||||||
|
@ -123,7 +123,7 @@ async def api_payments(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
for payment in pendingPayments:
|
for payment in pendingPayments:
|
||||||
await check_invoice_status(
|
await check_transaction_status(
|
||||||
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
||||||
)
|
)
|
||||||
return await get_payments(
|
return await get_payments(
|
||||||
|
@ -184,10 +184,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
|
|
||||||
lnurl_response: Union[None, bool, str] = None
|
lnurl_response: Union[None, bool, str] = None
|
||||||
if data.lnurl_callback:
|
if data.lnurl_callback:
|
||||||
if "lnurl_balance_check" in data:
|
if data.lnurl_balance_check is not None:
|
||||||
assert (
|
|
||||||
data.lnurl_balance_check is not None
|
|
||||||
), "lnurl_balance_check is required"
|
|
||||||
await save_balance_check(wallet.id, data.lnurl_balance_check)
|
await save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
@ -245,8 +242,6 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||||
|
|
||||||
@core_app.post(
|
@core_app.post(
|
||||||
"/api/v1/payments",
|
"/api/v1/payments",
|
||||||
# deprecated=True,
|
|
||||||
# description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead",
|
|
||||||
status_code=HTTPStatus.CREATED,
|
status_code=HTTPStatus.CREATED,
|
||||||
)
|
)
|
||||||
async def api_payments_create(
|
async def api_payments_create(
|
||||||
|
@ -407,7 +402,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||||
)
|
)
|
||||||
await check_invoice_status(payment.wallet_id, payment_hash)
|
await check_transaction_status(payment.wallet_id, payment_hash)
|
||||||
payment = await get_standalone_payment(
|
payment = await get_standalone_payment(
|
||||||
payment_hash, wallet_id=wallet.id if wallet else None
|
payment_hash, wallet_id=wallet.id if wallet else None
|
||||||
)
|
)
|
||||||
|
|
|
@ -148,7 +148,9 @@ async def wallet(
|
||||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
|
logger.debug(
|
||||||
|
f"Access {'user '+ user.id + ' ' if user else ''} {'wallet ' + wallet_name if wallet_name else ''}"
|
||||||
|
)
|
||||||
userwallet = user.get_wallet(wallet_id) # type: ignore
|
userwallet = user.get_wallet(wallet_id) # type: ignore
|
||||||
if not userwallet:
|
if not userwallet:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
|
|
|
@ -73,11 +73,9 @@ async def lnurl_callback(
|
||||||
wallet_id=cp.wallet,
|
wallet_id=cp.wallet,
|
||||||
amount=int(amount_received / 1000),
|
amount=int(amount_received / 1000),
|
||||||
memo=cp.lnurl_title,
|
memo=cp.lnurl_title,
|
||||||
description_hash=hashlib.sha256(
|
description_hash=(
|
||||||
(
|
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
||||||
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
).encode("utf-8"),
|
||||||
).encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
|
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
|
||||||
)
|
)
|
||||||
payResponse = {"pr": payment_request, "routes": []}
|
payResponse = {"pr": payment_request, "routes": []}
|
||||||
|
|
|
@ -90,9 +90,7 @@ async def lnurl_callback(
|
||||||
wallet_id=ls.wallet,
|
wallet_id=ls.wallet,
|
||||||
amount=int(amount_received / 1000),
|
amount=int(amount_received / 1000),
|
||||||
memo=await track.fullname(),
|
memo=await track.fullname(),
|
||||||
description_hash=hashlib.sha256(
|
description_hash=(await track.lnurlpay_metadata()).encode("utf-8"),
|
||||||
(await track.lnurlpay_metadata()).encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={"tag": "livestream", "track": track.id, "comment": comment},
|
extra={"tag": "livestream", "track": track.id, "comment": comment},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -70,11 +70,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
|
||||||
json={
|
json={
|
||||||
"out": False,
|
"out": False,
|
||||||
"amount": int(amount_received / 1000),
|
"amount": int(amount_received / 1000),
|
||||||
"description_hash": hashlib.sha256(
|
"description_hash": (
|
||||||
(await address.lnurlpay_metadata(domain=domain.domain)).encode(
|
await address.lnurlpay_metadata(domain=domain.domain)
|
||||||
"utf-8"
|
).encode("utf-8"),
|
||||||
)
|
|
||||||
).hexdigest(),
|
|
||||||
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
|
||||||
},
|
},
|
||||||
timeout=40,
|
timeout=40,
|
||||||
|
|
|
@ -6,7 +6,7 @@ from fastapi.params import Depends, Query
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.services import check_invoice_status, create_invoice
|
from lnbits.core.services import check_transaction_status, create_invoice
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||||
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
|
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
|
||||||
|
|
||||||
|
@ -229,7 +229,7 @@ async def api_address_send_address(payment_hash):
|
||||||
address = await get_address(payment_hash)
|
address = await get_address(payment_hash)
|
||||||
domain = await get_domain(address.domain)
|
domain = await get_domain(address.domain)
|
||||||
try:
|
try:
|
||||||
status = await check_invoice_status(domain.wallet, payment_hash)
|
status = await check_transaction_status(domain.wallet, payment_hash)
|
||||||
is_paid = not status.pending
|
is_paid = not status.pending
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"paid": False, "error": str(e)}
|
return {"paid": False, "error": str(e)}
|
||||||
|
|
|
@ -281,7 +281,13 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="CLOSE" color="primary" v-close-popup />
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="CLOSE"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup
|
||||||
|
@click="resetForm"
|
||||||
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
@ -371,6 +377,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
resetForm() {
|
||||||
|
this.formDialog.data = {flatrate: false}
|
||||||
|
},
|
||||||
getTickets: function () {
|
getTickets: function () {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
|
@ -463,7 +472,7 @@
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.forms.push(mapLNTicket(response.data))
|
self.forms.push(mapLNTicket(response.data))
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
self.formDialog.data = {}
|
self.resetForm()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
@ -497,7 +506,7 @@
|
||||||
})
|
})
|
||||||
self.forms.push(mapLNTicket(response.data))
|
self.forms.push(mapLNTicket(response.data))
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
self.formDialog.data = {}
|
self.resetForm()
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
|
|
@ -205,9 +205,7 @@ async def lnurl_callback(
|
||||||
wallet_id=device.wallet,
|
wallet_id=device.wallet,
|
||||||
amount=lnurldevicepayment.sats / 1000,
|
amount=lnurldevicepayment.sats / 1000,
|
||||||
memo=device.title,
|
memo=device.title,
|
||||||
description_hash=hashlib.sha256(
|
description_hash=(await device.lnurlpay_metadata()).encode("utf-8"),
|
||||||
(await device.lnurlpay_metadata()).encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={"tag": "PoS"},
|
extra={"tag": "PoS"},
|
||||||
)
|
)
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
|
|
|
@ -87,9 +87,7 @@ async def api_lnurl_callback(request: Request, link_id):
|
||||||
wallet_id=link.wallet,
|
wallet_id=link.wallet,
|
||||||
amount=int(amount_received / 1000),
|
amount=int(amount_received / 1000),
|
||||||
memo=link.description,
|
memo=link.description,
|
||||||
description_hash=hashlib.sha256(
|
description_hash=link.lnurlpay_metadata.encode("utf-8"),
|
||||||
link.lnurlpay_metadata.encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={
|
extra={
|
||||||
"tag": "lnurlp",
|
"tag": "lnurlp",
|
||||||
"link": link.id,
|
"link": link.id,
|
||||||
|
|
|
@ -96,7 +96,7 @@ async def api_link_create_or_update(
|
||||||
data.min *= data.fiat_base_multiplier
|
data.min *= data.fiat_base_multiplier
|
||||||
data.max *= data.fiat_base_multiplier
|
data.max *= data.fiat_base_multiplier
|
||||||
|
|
||||||
if "success_url" in data and data.success_url[:8] != "https://":
|
if data.success_url is not None and data.success_url.startswith("https://"):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="Success URL must be secure https://...",
|
detail="Success URL must be secure https://...",
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
|
|
@ -73,9 +73,7 @@ async def lnurl_callback(request: Request, item_id: int):
|
||||||
wallet_id=shop.wallet,
|
wallet_id=shop.wallet,
|
||||||
amount=int(amount_received / 1000),
|
amount=int(amount_received / 1000),
|
||||||
memo=item.name,
|
memo=item.name,
|
||||||
description_hash=hashlib.sha256(
|
description_hash=(await item.lnurlpay_metadata()).encode("utf-8"),
|
||||||
(await item.lnurlpay_metadata()).encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={"tag": "offlineshop", "item": item.id},
|
extra={"tag": "offlineshop", "item": item.id},
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
@ -4,7 +4,7 @@ from fastapi import Depends, Query
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import get_user, get_wallet
|
from lnbits.core.crud import get_user, get_wallet
|
||||||
from lnbits.core.services import check_invoice_status, create_invoice
|
from lnbits.core.services import check_transaction_status, create_invoice
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||||
|
|
||||||
from . import paywall_ext
|
from . import paywall_ext
|
||||||
|
@ -87,7 +87,7 @@ async def api_paywal_check_invoice(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
status = await check_invoice_status(paywall.wallet, payment_hash)
|
status = await check_transaction_status(paywall.wallet, payment_hash)
|
||||||
is_paid = not status.pending
|
is_paid = not status.pending
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"paid": False}
|
return {"paid": False}
|
||||||
|
|
|
@ -77,9 +77,7 @@ async def api_lnurlp_callback(
|
||||||
wallet_id=link.wallet,
|
wallet_id=link.wallet,
|
||||||
amount=int(amount_received / 1000),
|
amount=int(amount_received / 1000),
|
||||||
memo="Satsdice bet",
|
memo="Satsdice bet",
|
||||||
description_hash=hashlib.sha256(
|
description_hash=link.lnurlpay_metadata.encode("utf-8"),
|
||||||
link.lnurlpay_metadata.encode("utf-8")
|
|
||||||
).digest(),
|
|
||||||
extra={"tag": "satsdice", "link": link.id, "comment": "comment"},
|
extra={"tag": "satsdice", "link": link.id, "comment": "comment"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -421,7 +421,13 @@
|
||||||
this.formDialog = {
|
this.formDialog = {
|
||||||
show: false,
|
show: false,
|
||||||
fixedAmount: true,
|
fixedAmount: true,
|
||||||
data: {}
|
data: {
|
||||||
|
haircut: 0,
|
||||||
|
min_bet: 10,
|
||||||
|
max_bet: 1000,
|
||||||
|
currency: 'satoshis',
|
||||||
|
comment_chars: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePayLink(wallet, data) {
|
updatePayLink(wallet, data) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
@ -11,6 +12,14 @@ db = Database("ext_satspay")
|
||||||
|
|
||||||
satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
|
satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
|
||||||
|
|
||||||
|
satspay_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/satspay/static",
|
||||||
|
"app": StaticFiles(directory="lnbits/extensions/satspay/static"),
|
||||||
|
"name": "satspay_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def satspay_renderer():
|
def satspay_renderer():
|
||||||
return template_renderer(["lnbits/extensions/satspay/templates"])
|
return template_renderer(["lnbits/extensions/satspay/templates"])
|
||||||
|
|
|
@ -6,7 +6,7 @@ from lnbits.core.services import create_invoice
|
||||||
from lnbits.core.views.api import api_payment
|
from lnbits.core.views.api import api_payment
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from ..watchonly.crud import get_fresh_address, get_mempool, get_watch_wallet
|
from ..watchonly.crud import get_config, get_fresh_address
|
||||||
|
|
||||||
# from lnbits.db import open_ext_db
|
# from lnbits.db import open_ext_db
|
||||||
from . import db
|
from . import db
|
||||||
|
@ -18,7 +18,6 @@ from .models import Charges, CreateCharge
|
||||||
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||||
charge_id = urlsafe_short_hash()
|
charge_id = urlsafe_short_hash()
|
||||||
if data.onchainwallet:
|
if data.onchainwallet:
|
||||||
wallet = await get_watch_wallet(data.onchainwallet)
|
|
||||||
onchain = await get_fresh_address(data.onchainwallet)
|
onchain = await get_fresh_address(data.onchainwallet)
|
||||||
onchainaddress = onchain.address
|
onchainaddress = onchain.address
|
||||||
else:
|
else:
|
||||||
|
@ -89,7 +88,8 @@ async def get_charge(charge_id: str) -> Charges:
|
||||||
|
|
||||||
async def get_charges(user: str) -> List[Charges]:
|
async def get_charges(user: str) -> List[Charges]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""SELECT * FROM satspay.charges WHERE "user" = ?""", (user,)
|
"""SELECT * FROM satspay.charges WHERE "user" = ? ORDER BY "timestamp" DESC """,
|
||||||
|
(user,),
|
||||||
)
|
)
|
||||||
return [Charges.from_row(row) for row in rows]
|
return [Charges.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
@ -102,14 +102,16 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
||||||
charge = await get_charge(charge_id)
|
charge = await get_charge(charge_id)
|
||||||
if not charge.paid:
|
if not charge.paid:
|
||||||
if charge.onchainaddress:
|
if charge.onchainaddress:
|
||||||
mempool = await get_mempool(charge.user)
|
config = await get_config(charge.user)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
mempool.endpoint + "/api/address/" + charge.onchainaddress
|
config.mempool_endpoint
|
||||||
|
+ "/api/address/"
|
||||||
|
+ charge.onchainaddress
|
||||||
)
|
)
|
||||||
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
||||||
if respAmount >= charge.balance:
|
if respAmount > charge.balance:
|
||||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
await update_charge(charge_id=charge_id, balance=respAmount)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import time
|
from datetime import datetime, timedelta
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -38,12 +38,16 @@ class Charges(BaseModel):
|
||||||
def from_row(cls, row: Row) -> "Charges":
|
def from_row(cls, row: Row) -> "Charges":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_left(self):
|
||||||
|
now = datetime.utcnow().timestamp()
|
||||||
|
start = datetime.fromtimestamp(self.timestamp)
|
||||||
|
expiration = (start + timedelta(minutes=self.time)).timestamp()
|
||||||
|
return (expiration - now) / 60
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_elapsed(self):
|
def time_elapsed(self):
|
||||||
if (self.timestamp + (self.time * 60)) >= time.time():
|
return self.time_left < 0
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def paid(self):
|
def paid(self):
|
||||||
|
|
31
lnbits/extensions/satspay/static/js/utils.js
Normal file
31
lnbits/extensions/satspay/static/js/utils.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||||
|
const retryWithDelay = async function (fn, retryCount = 0) {
|
||||||
|
try {
|
||||||
|
await sleep(25)
|
||||||
|
// Do not return the call directly, use result.
|
||||||
|
// Otherwise the error will not be cought in this try-catch block.
|
||||||
|
const result = await fn()
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
if (retryCount > 100) throw err
|
||||||
|
await sleep((retryCount + 1) * 1000)
|
||||||
|
return retryWithDelay(fn, retryCount + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapCharge = (obj, oldObj = {}) => {
|
||||||
|
const charge = _.clone(obj)
|
||||||
|
|
||||||
|
charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time
|
||||||
|
charge.time = minutesToTime(obj.time)
|
||||||
|
charge.timeLeft = minutesToTime(obj.time_left)
|
||||||
|
|
||||||
|
charge.expanded = false
|
||||||
|
charge.displayUrl = ['/satspay/', obj.id].join('')
|
||||||
|
charge.expanded = oldObj.expanded
|
||||||
|
charge.pendingBalance = oldObj.pendingBalance || 0
|
||||||
|
return charge
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutesToTime = min =>
|
||||||
|
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''
|
|
@ -8,172 +8,10 @@
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a target="_blank" href="/docs#/satspay" class="text-white"
|
||||||
|
>Swagger REST API Documentation</a
|
||||||
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="API info"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-btn flat label="Swagger API" type="a" href="../docs#/satspay"></q-btn>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Create charge">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">POST</span> /satspay/api/v1/charge</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Body (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<charge_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url }}satspay/api/v1/charge -d
|
|
||||||
'{"onchainwallet": <string, watchonly_wallet_id>,
|
|
||||||
"description": <string>, "webhook":<string>, "time":
|
|
||||||
<integer>, "amount": <integer>, "lnbitswallet":
|
|
||||||
<string, lnbits_wallet_id>}' -H "Content-type:
|
|
||||||
application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Update charge">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">PUT</span>
|
|
||||||
/satspay/api/v1/charge/<charge_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Body (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<charge_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url
|
|
||||||
}}satspay/api/v1/charge/<charge_id> -d '{"onchainwallet":
|
|
||||||
<string, watchonly_wallet_id>, "description": <string>,
|
|
||||||
"webhook":<string>, "time": <integer>, "amount":
|
|
||||||
<integer>, "lnbitswallet": <string, lnbits_wallet_id>}'
|
|
||||||
-H "Content-type: application/json" -H "X-Api-Key:
|
|
||||||
{{user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Get charge">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/satspay/api/v1/charge/<charge_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Body (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<charge_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url
|
|
||||||
}}satspay/api/v1/charge/<charge_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].inkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Get charges">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span> /satspay/api/v1/charges</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Body (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<charge_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url }}satspay/api/v1/charges -H
|
|
||||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Delete a pay link"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-pink">DELETE</span>
|
|
||||||
/satspay/api/v1/charge/<charge_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
|
||||||
<code></code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X DELETE {{ request.base_url
|
|
||||||
}}satspay/api/v1/charge/<charge_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Get balances"
|
|
||||||
class="q-pb-md"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/satspay/api/v1/charges/balance/<charge_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Body (application/json)
|
|
||||||
</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<charge_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url
|
|
||||||
}}satspay/api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].inkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -1,223 +1,299 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
{% extends "public.html" %} {% block page %}
|
||||||
<div class="q-pa-sm theCard">
|
<div class="row items-center q-mt-md">
|
||||||
<q-card class="my-card">
|
<div class="col-lg-4 col-md-3 col-sm-1"></div>
|
||||||
<div class="column">
|
<div class="col-lg-4 col-md-6 col-sm-10">
|
||||||
<center>
|
<q-card>
|
||||||
<div class="col theHeading">{{ charge.description }}</div>
|
<div class="row q-mb-md">
|
||||||
</center>
|
<div class="col text-center q-mt-md">
|
||||||
<div class="col">
|
<span class="text-h4" v-text="charge.description"></span>
|
||||||
<div
|
|
||||||
class="col"
|
|
||||||
color="white"
|
|
||||||
style="background-color: grey; height: 30px; padding: 5px"
|
|
||||||
v-if="timetoComplete < 1"
|
|
||||||
>
|
|
||||||
<center>Time elapsed</center>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col"
|
|
||||||
color="white"
|
|
||||||
style="background-color: grey; height: 30px; padding: 5px"
|
|
||||||
v-else-if="charge_paid == 'True'"
|
|
||||||
>
|
|
||||||
<center>Charge paid</center>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<q-linear-progress size="30px" :value="newProgress" color="grey">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item style="padding: 3px">
|
|
||||||
<q-spinner color="white" size="0.8em"></q-spinner
|
|
||||||
><span style="font-size: 15px; color: white"
|
|
||||||
><span class="q-pr-xl q-pl-md"> Awaiting payment...</span>
|
|
||||||
<span class="q-pl-xl" style="color: white">
|
|
||||||
{% raw %} {{ newTimeLeft }} {% endraw %}</span
|
|
||||||
></span
|
|
||||||
>
|
|
||||||
</q-item>
|
|
||||||
</q-item-section>
|
|
||||||
</q-linear-progress>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="margin: 2px 15px; max-height: 100px">
|
<div class="row">
|
||||||
<center>
|
<div class="col text-center">
|
||||||
<q-btn flat dense outline @click="copyText('{{ charge.id }}')"
|
<div
|
||||||
>Charge ID: {{ charge.id }}</q-btn
|
color="white"
|
||||||
|
style="background-color: grey; height: 30px; padding: 5px"
|
||||||
|
v-if="!charge.timeLeft"
|
||||||
>
|
>
|
||||||
</center>
|
Time elapsed
|
||||||
<span
|
|
||||||
><small
|
|
||||||
>{% raw %} Total to pay: {{ charge_amount }}sats<br />
|
|
||||||
Amount paid: {{ charge_balance }}</small
|
|
||||||
><br />
|
|
||||||
Amount due: {{ charge_amount - charge_balance }}sats {% endraw %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<div class="col">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
disable
|
|
||||||
v-if="'{{ charge.lnbitswallet }}' == 'None' || charge_time_elapsed == 'True'"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="lightning⚡"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
bitcoin lightning payment method not available
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
v-else
|
|
||||||
@click="payLN"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="lightning⚡"
|
|
||||||
>
|
|
||||||
<q-tooltip> pay with lightning </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div
|
||||||
<q-btn
|
color="white"
|
||||||
flat
|
style="background-color: grey; height: 30px; padding: 5px"
|
||||||
disable
|
v-else-if="charge.paid"
|
||||||
v-if="'{{ charge.onchainwallet }}' == 'None' || charge_time_elapsed == 'True'"
|
>
|
||||||
style="color: primary; width: 100%"
|
Charge paid
|
||||||
label="onchain⛓️"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
bitcoin onchain payment method not available
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
v-else
|
|
||||||
@click="payON"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="onchain⛓️"
|
|
||||||
>
|
|
||||||
<q-tooltip> pay onchain </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card class="q-pa-lg" v-if="lnbtc">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div class="text-center q-pt-md">
|
|
||||||
<div v-if="timetoComplete < 1 && charge_paid == 'False'">
|
|
||||||
<q-icon
|
|
||||||
name="block"
|
|
||||||
style="color: #ccc; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="charge_paid == 'True'">
|
|
||||||
<q-icon
|
|
||||||
name="check"
|
|
||||||
style="color: green; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
v-if="'{{ charge.webhook }}' != 'None'"
|
|
||||||
type="a"
|
|
||||||
href="{{ charge.completelink }}"
|
|
||||||
label="{{ charge.completelinktext }}"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<center>
|
<q-linear-progress
|
||||||
<span class="text-subtitle2"
|
size="30px"
|
||||||
>Pay this <br />
|
:value="charge.progress"
|
||||||
lightning-network invoice</span
|
color="secondary"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item style="padding: 3px">
|
||||||
|
<q-spinner color="white" size="0.8em"></q-spinner
|
||||||
|
><span style="font-size: 15px; color: white"
|
||||||
|
><span class="q-pr-xl q-pl-md"> Awaiting payment...</span>
|
||||||
|
<span class="q-pl-xl" style="color: white">
|
||||||
|
{% raw %} {{ charge.timeLeft }} {% endraw %}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
</q-item>
|
||||||
|
</q-item-section>
|
||||||
|
</q-linear-progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-ml-md q-mt-md q-mb-lg">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 q-pr-lg">Charge Id:</div>
|
||||||
|
<div class="col-8 q-pr-lg">
|
||||||
|
<q-btn flat dense outline @click="copyText(charge.id)"
|
||||||
|
><span v-text="charge.id"></span
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center">
|
||||||
|
<div class="col-4 q-pr-lg">Total to pay:</div>
|
||||||
|
<div class="col-8 q-pr-lg">
|
||||||
|
<q-badge color="blue">
|
||||||
|
<span v-text="charge.amount" class="text-subtitle2"></span> sat
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-sm">
|
||||||
|
<div class="col-4 q-pr-lg">Amount paid:</div>
|
||||||
|
<div class="col-8 q-pr-lg">
|
||||||
|
<q-badge color="orange">
|
||||||
|
<span v-text="charge.balance" class="text-subtitle2"></span>
|
||||||
|
sat</q-badge
|
||||||
>
|
>
|
||||||
</center>
|
</div>
|
||||||
<a href="lightning:{{ charge.payment_request }}">
|
</div>
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<div v-if="pendingFunds" class="row items-center q-mt-sm">
|
||||||
<qrcode
|
<div class="col-4 q-pr-lg">Amount pending:</div>
|
||||||
:value="'{{ charge.payment_request }}'"
|
<div class="col-8 q-pr-lg">
|
||||||
:options="{width: 800}"
|
<q-badge color="gray">
|
||||||
class="rounded-borders"
|
<span v-text="pendingFunds" class="text-subtitle2"></span> sat
|
||||||
></qrcode>
|
</q-badge>
|
||||||
</q-responsive>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row items-center q-mt-sm">
|
||||||
<q-btn
|
<div class="col-4 q-pr-lg">Amount due:</div>
|
||||||
outline
|
<div class="col-8 q-pr-lg">
|
||||||
color="grey"
|
<q-badge v-if="charge.amount - charge.balance > 0" color="green">
|
||||||
@click="copyText('{{ charge.payment_request }}')"
|
<span
|
||||||
>Copy invoice</q-btn
|
v-text="charge.amount - charge.balance"
|
||||||
|
class="text-subtitle2"
|
||||||
|
></span>
|
||||||
|
sat
|
||||||
|
</q-badge>
|
||||||
|
<q-badge
|
||||||
|
v-else="charge.amount - charge.balance <= 0"
|
||||||
|
color="green"
|
||||||
|
class="text-subtitle2"
|
||||||
|
>
|
||||||
|
none</q-badge
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
disable
|
||||||
|
v-if="!charge.lnbitswallet || charge.time_elapsed"
|
||||||
|
style="color: primary; width: 100%"
|
||||||
|
label="lightning⚡"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
bitcoin lightning payment method not available
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
v-else
|
||||||
|
@click="payInvoice"
|
||||||
|
style="color: primary; width: 100%"
|
||||||
|
label="lightning⚡"
|
||||||
|
>
|
||||||
|
<q-tooltip> pay with lightning </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
disable
|
||||||
|
v-if="!charge.onchainwallet || charge.time_elapsed"
|
||||||
|
style="color: primary; width: 100%"
|
||||||
|
label="onchain⛓️"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
bitcoin onchain payment method not available
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
v-else
|
||||||
|
@click="payOnchain"
|
||||||
|
style="color: primary; width: 100%"
|
||||||
|
label="onchain⛓️"
|
||||||
|
>
|
||||||
|
<q-tooltip> pay onchain </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-pa-lg" v-if="lnbtc">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<div class="row items-center q-mt-sm">
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
|
<div class="col-md-8 col-sm-12">
|
||||||
|
<div v-if="!charge.timeLeft && !charge.paid">
|
||||||
|
<q-icon
|
||||||
|
name="block"
|
||||||
|
style="color: #ccc; font-size: 21.4em"
|
||||||
|
></q-icon>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="charge.paid">
|
||||||
|
<q-icon
|
||||||
|
name="check"
|
||||||
|
style="color: green; font-size: 21.4em"
|
||||||
|
></q-icon>
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
v-if="charge.webhook"
|
||||||
|
type="a"
|
||||||
|
:href="charge.completelink"
|
||||||
|
:label="charge.completelinktext"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="row text-center q-mb-sm">
|
||||||
|
<div class="col text-center">
|
||||||
|
<span class="text-subtitle2"
|
||||||
|
>Pay this lightning-network invoice:</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a :href="'lightning:'+charge.payment_request">
|
||||||
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
|
<qrcode
|
||||||
|
:value="charge.payment_request"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
|
<div class="row text-center q-mt-lg">
|
||||||
|
<div class="col text-center">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="copyText(charge.payment_request)"
|
||||||
|
>Copy invoice</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card class="q-pa-lg" v-if="onbtc">
|
<q-card class="q-pa-lg" v-if="onbtc">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center q-pt-md">
|
<div v-if="charge.timeLeft && !charge.paid" class="row items-center">
|
||||||
<div v-if="timetoComplete < 1 && charge_paid == 'False'">
|
<div class="col text-center">
|
||||||
<q-icon
|
<a
|
||||||
name="block"
|
style="color: unset"
|
||||||
style="color: #ccc; font-size: 21.4em"
|
:href="mempool_endpoint + '/address/' + charge.onchainaddress"
|
||||||
></q-icon>
|
target="_blank"
|
||||||
</div>
|
><span
|
||||||
<div v-else-if="charge_paid == 'True'">
|
class="text-subtitle1"
|
||||||
<q-icon
|
v-text="charge.onchainaddress"
|
||||||
name="check"
|
></span>
|
||||||
style="color: green; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
v-if="'{{ charge.webhook }}' != None"
|
|
||||||
type="a"
|
|
||||||
href="{{ charge.completelink }}"
|
|
||||||
label="{{ charge.completelinktext }}"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<center>
|
|
||||||
<span class="text-subtitle2"
|
|
||||||
>Send {{ charge.amount }}sats<br />
|
|
||||||
to this onchain address</span
|
|
||||||
>
|
|
||||||
</center>
|
|
||||||
<a href="bitcoin:{{ charge.onchainaddress }}">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
|
||||||
<qrcode
|
|
||||||
:value="'{{ charge.onchainaddress }}'"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
</a>
|
||||||
<div class="row q-mt-lg">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-md">
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
|
<div class="col-md-8 col-sm-12 text-center">
|
||||||
|
<div v-if="!charge.timeLeft && !charge.paid">
|
||||||
|
<q-icon
|
||||||
|
name="block"
|
||||||
|
style="color: #ccc; font-size: 21.4em"
|
||||||
|
></q-icon>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="charge.paid">
|
||||||
|
<q-icon
|
||||||
|
name="check"
|
||||||
|
style="color: green; font-size: 21.4em"
|
||||||
|
></q-icon>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="grey"
|
v-if="charge.webhook"
|
||||||
@click="copyText('{{ charge.onchainaddress }}')"
|
type="a"
|
||||||
>Copy address</q-btn
|
:href="charge.completelink"
|
||||||
>
|
:label="charge.completelinktext"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="row items-center q-mb-sm">
|
||||||
|
<div class="col text-center">
|
||||||
|
<span class="text-subtitle2"
|
||||||
|
>Send
|
||||||
|
|
||||||
|
<span v-text="charge.amount"></span>
|
||||||
|
sats to this onchain address</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a :href="'bitcoin:'+charge.onchainaddress">
|
||||||
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
|
<qrcode
|
||||||
|
:value="charge.onchainaddress"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
|
<div class="row items-center q-mt-lg">
|
||||||
|
<div class="col text-center">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="copyText(charge.onchainaddress)"
|
||||||
|
>Copy address</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-card>
|
</div>
|
||||||
|
<div class="col-lg- 4 col-md-3 col-sm-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %} {% block scripts %}
|
{% endblock %} {% block scripts %}
|
||||||
|
|
||||||
<style>
|
<script src="https://mempool.space/mempool.js"></script>
|
||||||
.theCard {
|
<script src="{{ url_for('satspay_static', path='js/utils.js') }}"></script>
|
||||||
width: 360px;
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
.theHeading {
|
|
||||||
margin: 15px;
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
@ -226,16 +302,14 @@
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
charge: JSON.parse('{{charge_data | tojson}}'),
|
||||||
|
mempool_endpoint: '{{mempool_endpoint}}',
|
||||||
|
pendingFunds: 0,
|
||||||
|
ws: null,
|
||||||
newProgress: 0.4,
|
newProgress: 0.4,
|
||||||
counter: 1,
|
counter: 1,
|
||||||
newTimeLeft: '',
|
|
||||||
timetoComplete: 100,
|
|
||||||
lnbtc: true,
|
lnbtc: true,
|
||||||
onbtc: false,
|
onbtc: false,
|
||||||
charge_time_elapsed: '{{charge.time_elapsed}}',
|
|
||||||
charge_amount: '{{charge.amount}}',
|
|
||||||
charge_balance: '{{charge.balance}}',
|
|
||||||
charge_paid: '{{charge.paid}}',
|
|
||||||
wallet: {
|
wallet: {
|
||||||
inkey: ''
|
inkey: ''
|
||||||
},
|
},
|
||||||
|
@ -245,90 +319,141 @@
|
||||||
methods: {
|
methods: {
|
||||||
startPaymentNotifier() {
|
startPaymentNotifier() {
|
||||||
this.cancelListener()
|
this.cancelListener()
|
||||||
|
if (!this.lnbitswallet) return
|
||||||
this.cancelListener = LNbits.event.onInvoicePaid(
|
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||||
this.wallet,
|
this.wallet,
|
||||||
payment => {
|
payment => {
|
||||||
this.checkBalance()
|
this.checkInvoiceBalance()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
checkBalance: function () {
|
checkBalances: async function () {
|
||||||
var self = this
|
if (!this.charge.hasStaleBalance) await this.refreshCharge()
|
||||||
LNbits.api
|
try {
|
||||||
.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satspay/api/v1/charges/balance/{{ charge.id }}',
|
`/satspay/api/v1/charge/balance/${this.charge.id}`
|
||||||
'filla'
|
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
this.charge = mapCharge(data, this.charge)
|
||||||
self.charge_time_elapsed = response.data.time_elapsed
|
} catch (error) {
|
||||||
self.charge_amount = response.data.amount
|
LNbits.utils.notifyApiError(error)
|
||||||
self.charge_balance = response.data.balance
|
}
|
||||||
if (self.charge_balance >= self.charge_amount) {
|
|
||||||
self.charge_paid = 'True'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
payLN: function () {
|
refreshCharge: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/satspay/api/v1/charge/${this.charge.id}`
|
||||||
|
)
|
||||||
|
this.charge = mapCharge(data, this.charge)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkPendingOnchain: async function () {
|
||||||
|
const {
|
||||||
|
bitcoin: {addresses: addressesAPI}
|
||||||
|
} = mempoolJS({
|
||||||
|
hostname: new URL(this.mempool_endpoint).hostname
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const utxos = await addressesAPI.getAddressTxsUtxo({
|
||||||
|
address: this.charge.onchainaddress
|
||||||
|
})
|
||||||
|
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
||||||
|
this.charge.hasStaleBalance = this.charge.balance === newBalance
|
||||||
|
|
||||||
|
this.pendingFunds = utxos
|
||||||
|
.filter(u => !u.status.confirmed)
|
||||||
|
.reduce((t, u) => t + u.value, 0)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('cannot check pending funds')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
payInvoice: function () {
|
||||||
this.lnbtc = true
|
this.lnbtc = true
|
||||||
this.onbtc = false
|
this.onbtc = false
|
||||||
},
|
},
|
||||||
payON: function () {
|
payOnchain: function () {
|
||||||
this.lnbtc = false
|
this.lnbtc = false
|
||||||
this.onbtc = true
|
this.onbtc = true
|
||||||
},
|
},
|
||||||
getTheTime: function () {
|
|
||||||
var timeToComplete =
|
|
||||||
parseInt('{{ charge.time }}') * 60 -
|
|
||||||
(Date.now() / 1000 - parseInt('{{ charge.timestamp }}'))
|
|
||||||
this.timetoComplete = timeToComplete
|
|
||||||
var timeLeft = Quasar.utils.date.formatDate(
|
|
||||||
new Date((timeToComplete - 3600) * 1000),
|
|
||||||
'HH:mm:ss'
|
|
||||||
)
|
|
||||||
this.newTimeLeft = timeLeft
|
|
||||||
},
|
|
||||||
getThePercentage: function () {
|
|
||||||
var timeToComplete =
|
|
||||||
parseInt('{{ charge.time }}') * 60 -
|
|
||||||
(Date.now() / 1000 - parseInt('{{ charge.timestamp }}'))
|
|
||||||
this.newProgress =
|
|
||||||
1 - timeToComplete / (parseInt('{{ charge.time }}') * 60)
|
|
||||||
},
|
|
||||||
|
|
||||||
timerCount: function () {
|
loopRefresh: function () {
|
||||||
self = this
|
// invoice only
|
||||||
var refreshIntervalId = setInterval(function () {
|
const refreshIntervalId = setInterval(async () => {
|
||||||
if (self.charge_paid == 'True' || self.timetoComplete < 1) {
|
if (this.charge.paid || !this.charge.timeLeft) {
|
||||||
clearInterval(refreshIntervalId)
|
clearInterval(refreshIntervalId)
|
||||||
}
|
}
|
||||||
self.getTheTime()
|
if (this.counter % 10 === 0) {
|
||||||
self.getThePercentage()
|
await this.checkBalances()
|
||||||
self.counter++
|
await this.checkPendingOnchain()
|
||||||
if (self.counter % 10 === 0) {
|
|
||||||
self.checkBalance()
|
|
||||||
}
|
}
|
||||||
|
this.counter++
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
},
|
||||||
|
initWs: async function () {
|
||||||
|
const {
|
||||||
|
bitcoin: {websocket}
|
||||||
|
} = mempoolJS({
|
||||||
|
hostname: new URL(this.mempool_endpoint).hostname
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws = new WebSocket('wss://mempool.space/api/v1/ws')
|
||||||
|
this.ws.addEventListener('open', x => {
|
||||||
|
if (this.charge.onchainaddress) {
|
||||||
|
this.trackAddress(this.charge.onchainaddress)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener('message', async ({data}) => {
|
||||||
|
const res = JSON.parse(data.toString())
|
||||||
|
if (res['address-transactions']) {
|
||||||
|
await this.checkBalances()
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'New payment received!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loopPingWs: function () {
|
||||||
|
setInterval(() => {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) this.initWs()
|
||||||
|
this.ws.send(JSON.stringify({action: 'ping'}))
|
||||||
|
}, 30 * 1000)
|
||||||
|
},
|
||||||
|
trackAddress: async function (address, retry = 0) {
|
||||||
|
try {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) this.initWs()
|
||||||
|
this.ws.send(JSON.stringify({'track-address': address}))
|
||||||
|
} catch (error) {
|
||||||
|
await sleep(1000)
|
||||||
|
if (retry > 10) throw error
|
||||||
|
this.trackAddress(address, retry + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: async function () {
|
||||||
console.log('{{ charge.onchainaddress }}' == 'None')
|
if (this.charge.lnbitswallet) this.payInvoice()
|
||||||
if ('{{ charge.lnbitswallet }}' == 'None') {
|
else this.payOnchain()
|
||||||
this.lnbtc = false
|
await this.checkBalances()
|
||||||
this.onbtc = true
|
|
||||||
}
|
// empty for onchain
|
||||||
this.wallet.inkey = '{{ wallet_inkey }}'
|
this.wallet.inkey = '{{ wallet_inkey }}'
|
||||||
this.getTheTime()
|
|
||||||
this.getThePercentage()
|
|
||||||
var timerCount = this.timerCount
|
|
||||||
if ('{{ charge.paid }}' == 'False') {
|
|
||||||
timerCount()
|
|
||||||
}
|
|
||||||
this.startPaymentNotifier()
|
this.startPaymentNotifier()
|
||||||
|
|
||||||
|
if (!this.charge.paid) {
|
||||||
|
this.loopRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.charge.onchainaddress) {
|
||||||
|
this.loopPingWs()
|
||||||
|
this.checkPendingOnchain()
|
||||||
|
this.trackAddress(this.charge.onchainaddress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,46 +18,54 @@
|
||||||
<h5 class="text-subtitle1 q-my-none">Charges</h5>
|
<h5 class="text-subtitle1 q-my-none">Charges</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col q-pr-lg">
|
||||||
<q-input
|
<q-input
|
||||||
borderless
|
borderless
|
||||||
dense
|
dense
|
||||||
debounce="300"
|
debounce="300"
|
||||||
v-model="filter"
|
v-model="filter"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
class="float-right"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<q-icon name="search"></q-icon>
|
<q-icon name="search"></q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-btn flat color="grey" @click="exportchargeCSV"
|
</div>
|
||||||
>Export to CSV</q-btn
|
<div class="col-auto">
|
||||||
>
|
<q-btn outline color="grey" label="...">
|
||||||
|
<q-menu auto-close>
|
||||||
|
<q-list style="min-width: 100px">
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section @click="exportchargeCSV"
|
||||||
|
>Export to CSV</q-item-section
|
||||||
|
>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
:data="ChargeLinks"
|
:data="chargeLinks"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="ChargesTable.columns"
|
:columns="chargesTable.columns"
|
||||||
:pagination.sync="ChargesTable.pagination"
|
:pagination.sync="chargesTable.pagination"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
>
|
>
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width>Status </q-th>
|
||||||
|
<q-th auto-width>Title</q-th>
|
||||||
<q-th
|
<q-th auto-width>Time Left (hh:mm)</q-th>
|
||||||
v-for="col in props.cols"
|
<q-th auto-width>Time To Pay (hh:mm)</q-th>
|
||||||
:key="col.name"
|
<q-th auto-width>Amount To Pay</q-th>
|
||||||
:props="props"
|
<q-th auto-width>Balance</q-th>
|
||||||
auto-width
|
<q-th auto-width>Pending Balance</q-th>
|
||||||
>
|
<q-th auto-width>Onchain Address</q-th>
|
||||||
<div v-if="col.name == 'id'"></div>
|
|
||||||
<div v-else>{{ col.label }}</div>
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -66,73 +74,179 @@
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
dense
|
dense
|
||||||
size="xs"
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
icon="link"
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
/>
|
||||||
type="a"
|
</q-td>
|
||||||
|
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.time_elapsed && props.row.balance < props.row.amount"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>expired</a
|
||||||
|
>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-else-if="props.row.balance >= props.row.amount"
|
||||||
|
color="green"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>paid</a
|
||||||
|
>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<q-badge v-else color="blue"
|
||||||
|
><a
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>waiting</a
|
||||||
|
>
|
||||||
|
</q-badge>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="description" :props="props" :class="">
|
||||||
|
<a
|
||||||
:href="props.row.displayUrl"
|
:href="props.row.displayUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>{{props.row.description}}</a
|
||||||
>
|
>
|
||||||
<q-tooltip> Payment link </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width>
|
<q-td key="timeLeft" :props="props" :class="">
|
||||||
<q-btn
|
<div>{{props.row.timeLeft}}</div>
|
||||||
v-if="props.row.time_elapsed && props.row.balance < props.row.amount"
|
<q-linear-progress
|
||||||
unelevated
|
v-if="props.row.timeLeft"
|
||||||
flat
|
:value="props.row.progress"
|
||||||
dense
|
color="secondary"
|
||||||
size="xs"
|
|
||||||
icon="error"
|
|
||||||
:color="($q.dark.isActive) ? 'red' : 'red'"
|
|
||||||
>
|
>
|
||||||
<q-tooltip> Time elapsed </q-tooltip>
|
</q-linear-progress>
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-else-if="props.row.balance >= props.row.amount"
|
|
||||||
unelevated
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="check"
|
|
||||||
:color="($q.dark.isActive) ? 'green' : 'green'"
|
|
||||||
>
|
|
||||||
<q-tooltip> PAID! </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="cached"
|
|
||||||
flat
|
|
||||||
:color="($q.dark.isActive) ? 'blue' : 'blue'"
|
|
||||||
>
|
|
||||||
<q-tooltip> Processing </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteChargeLink(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
>
|
|
||||||
<q-tooltip> Delete charge </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td
|
<q-td key="time to pay" :props="props" :class="">
|
||||||
v-for="col in props.cols"
|
<div>{{props.row.time}}</div>
|
||||||
:key="col.name"
|
</q-td>
|
||||||
:props="props"
|
<q-td key="amount" :props="props" :class="">
|
||||||
auto-width
|
<div>{{props.row.amount}}</div>
|
||||||
>
|
</q-td>
|
||||||
<div v-if="col.name == 'id'"></div>
|
<q-td key="balance" :props="props" :class="">
|
||||||
<div v-else>{{ col.value }}</div>
|
<div>{{props.row.balance}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="pendingBalance" :props="props" :class="">
|
||||||
|
<div>
|
||||||
|
{{props.row.pendingBalance ? props.row.pendingBalance : ''}}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="onchain address" :props="props" :class="">
|
||||||
|
<a
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>{{props.row.onchainaddress}}</a
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-show="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div
|
||||||
|
v-if="props.row.onchainwallet"
|
||||||
|
class="row items-center q-mt-md q-mb-lg"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg">Onchain Wallet:</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
{{getOnchainWalletName(props.row.onchainwallet)}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.lnbitswallet"
|
||||||
|
class="row items-center q-mt-md q-mb-lg"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg">LNbits Wallet:</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
{{getLNbitsWalletName(props.row.lnbitswallet)}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.row.completelink || props.row.completelinktext"
|
||||||
|
class="row items-center q-mt-md q-mb-lg"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg">Completed Link:</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<a
|
||||||
|
:href="props.row.completelink"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>{{props.row.completelinktext ||
|
||||||
|
props.row.completelink}}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.webhook"
|
||||||
|
class="row items-center q-mt-md q-mb-lg"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg">Webhook:</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<a
|
||||||
|
:href="props.row.webhook"
|
||||||
|
target="_blank"
|
||||||
|
style="color: unset; text-decoration: none"
|
||||||
|
>{{props.row.webhook || props.row.webhook}}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
|
<div class="col-2 q-pr-lg">ID:</div>
|
||||||
|
<div class="col-4 q-pr-lg">{{props.row.id}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-6 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="gray"
|
||||||
|
outline
|
||||||
|
type="a"
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="float-left q-mr-lg"
|
||||||
|
>Details</q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="gray"
|
||||||
|
outline
|
||||||
|
type="a"
|
||||||
|
@click="refreshBalance(props.row)"
|
||||||
|
target="_blank"
|
||||||
|
class="float-left"
|
||||||
|
>Refresh Balance</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="pink"
|
||||||
|
icon="cancel"
|
||||||
|
@click="deleteChargeLink(props.row.id)"
|
||||||
|
>Delete</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -155,11 +269,7 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<q-dialog
|
<q-dialog v-model="formDialogCharge.show" position="top">
|
||||||
v-model="formDialogCharge.show"
|
|
||||||
position="top"
|
|
||||||
@hide="closeFormDialog"
|
|
||||||
>
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form @submit="sendFormDataCharge" class="q-gutter-md">
|
<q-form @submit="sendFormDataCharge" class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
|
@ -246,7 +356,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
v-model="formDialogCharge.data.onchainwallet"
|
v-model="onchainwallet"
|
||||||
:options="walletLinks"
|
:options="walletLinks"
|
||||||
label="Onchain Wallet"
|
label="Onchain Wallet"
|
||||||
/>
|
/>
|
||||||
|
@ -284,49 +394,28 @@
|
||||||
<!-- lnbits/static/vendor
|
<!-- lnbits/static/vendor
|
||||||
<script src="/vendor/vue-qrcode@1.0.2/vue-qrcode.min.js"></script> -->
|
<script src="/vendor/vue-qrcode@1.0.2/vue-qrcode.min.js"></script> -->
|
||||||
<style></style>
|
<style></style>
|
||||||
|
<!-- todo: use config mempool -->
|
||||||
|
<script src="https://mempool.space/mempool.js"></script>
|
||||||
|
<script src="{{ url_for('satspay_static', path='js/utils.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
var mapCharge = obj => {
|
|
||||||
obj._data = _.clone(obj)
|
|
||||||
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
|
||||||
obj.time = obj.time + 'mins'
|
|
||||||
|
|
||||||
if (obj.time_elapsed) {
|
|
||||||
obj.date = 'Time elapsed'
|
|
||||||
} else {
|
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
|
||||||
new Date((obj.theTime - 3600) * 1000),
|
|
||||||
'HH:mm:ss'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
obj.displayUrl = ['/satspay/', obj.id].join('')
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
filter: '',
|
filter: '',
|
||||||
watchonlyactive: false,
|
|
||||||
balance: null,
|
balance: null,
|
||||||
checker: null,
|
|
||||||
walletLinks: [],
|
walletLinks: [],
|
||||||
ChargeLinks: [],
|
chargeLinks: [],
|
||||||
ChargeLinksObj: [],
|
|
||||||
onchainwallet: '',
|
onchainwallet: '',
|
||||||
currentaddress: '',
|
rescanning: false,
|
||||||
Addresses: {
|
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
},
|
|
||||||
mempool: {
|
mempool: {
|
||||||
endpoint: ''
|
endpoint: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
ChargesTable: {
|
chargesTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'theId',
|
name: 'theId',
|
||||||
|
@ -341,10 +430,10 @@
|
||||||
field: 'description'
|
field: 'description'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeleft',
|
name: 'timeLeft',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Time left',
|
label: 'Time left',
|
||||||
field: 'date'
|
field: 'timeLeft'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'time to pay',
|
name: 'time to pay',
|
||||||
|
@ -364,6 +453,12 @@
|
||||||
label: 'Balance',
|
label: 'Balance',
|
||||||
field: 'balance'
|
field: 'balance'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'pendingBalance',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Pending Balance',
|
||||||
|
field: 'pendingBalance'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'onchain address',
|
name: 'onchain address',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
@ -393,172 +488,218 @@
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
formDialogCharge: {
|
formDialogCharge: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
|
onchain: false,
|
||||||
|
onchainwallet: '',
|
||||||
|
lnbits: false,
|
||||||
|
description: '',
|
||||||
|
time: null,
|
||||||
|
amount: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelCharge: function (data) {
|
||||||
|
this.formDialogCharge.data.description = ''
|
||||||
|
this.formDialogCharge.data.onchainwallet = ''
|
||||||
|
this.formDialogCharge.data.lnbitswallet = ''
|
||||||
|
this.formDialogCharge.data.time = null
|
||||||
|
this.formDialogCharge.data.amount = null
|
||||||
|
this.formDialogCharge.data.webhook = ''
|
||||||
|
this.formDialogCharge.data.completelink = ''
|
||||||
|
this.formDialogCharge.show = false
|
||||||
|
},
|
||||||
|
|
||||||
|
getWalletLinks: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/watchonly/api/v1/wallet',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
this.walletLinks = data.map(w => ({
|
||||||
|
id: w.id,
|
||||||
|
label: w.title + ' - ' + w.id
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getWalletConfig: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/watchonly/api/v1/config',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
this.mempool.endpoint = data.mempool_endpoint
|
||||||
|
const url = new URL(this.mempool.endpoint)
|
||||||
|
this.mempool.hostname = url.hostname
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getOnchainWalletName: function (walletId) {
|
||||||
|
const wallet = this.walletLinks.find(w => w.id === walletId)
|
||||||
|
if (!wallet) return 'unknown'
|
||||||
|
return wallet.label
|
||||||
|
},
|
||||||
|
getLNbitsWalletName: function (walletId) {
|
||||||
|
const wallet = this.g.user.walletOptions.find(w => w.value === walletId)
|
||||||
|
if (!wallet) return 'unknown'
|
||||||
|
return wallet.label
|
||||||
|
},
|
||||||
|
|
||||||
|
getCharges: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/satspay/api/v1/charges',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
this.chargeLinks = data.map(c =>
|
||||||
|
mapCharge(
|
||||||
|
c,
|
||||||
|
this.chargeLinks.find(old => old.id === c.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendFormDataCharge: function () {
|
||||||
|
const wallet = this.g.user.wallets[0].inkey
|
||||||
|
const data = this.formDialogCharge.data
|
||||||
|
data.amount = parseInt(data.amount)
|
||||||
|
data.time = parseInt(data.time)
|
||||||
|
data.onchainwallet = this.onchainwallet?.id
|
||||||
|
this.createCharge(wallet, data)
|
||||||
|
},
|
||||||
|
refreshActiveChargesBalance: async function () {
|
||||||
|
try {
|
||||||
|
const activeLinkIds = this.chargeLinks
|
||||||
|
.filter(c => !c.paid && !c.time_elapsed && !c.hasStaleBalance)
|
||||||
|
.map(c => c.id)
|
||||||
|
.join(',')
|
||||||
|
if (activeLinkIds) {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/satspay/api/v1/charges/balance/' + activeLinkIds,
|
||||||
|
'filla'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
await this.getCharges()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshBalance: async function (charge) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/satspay/api/v1/charge/balance/' + charge.id,
|
||||||
|
'filla'
|
||||||
|
)
|
||||||
|
charge.balance = data.balance
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
rescanOnchainAddresses: async function () {
|
||||||
|
if (this.rescanning) return
|
||||||
|
this.rescanning = true
|
||||||
|
|
||||||
|
const {
|
||||||
|
bitcoin: {addresses: addressesAPI}
|
||||||
|
} = mempoolJS({hostname: this.mempool.hostname})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const onchainActiveCharges = this.chargeLinks.filter(
|
||||||
|
c => c.onchainaddress && !c.paid && !c.time_elapsed
|
||||||
|
)
|
||||||
|
for (const charge of onchainActiveCharges) {
|
||||||
|
const fn = async () =>
|
||||||
|
addressesAPI.getAddressTxsUtxo({
|
||||||
|
address: charge.onchainaddress
|
||||||
|
})
|
||||||
|
|
||||||
|
const utxos = await retryWithDelay(fn)
|
||||||
|
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
||||||
|
|
||||||
|
charge.pendingBalance = utxos
|
||||||
|
.filter(u => !u.status.confirmed)
|
||||||
|
.reduce((t, u) => t + u.value, 0)
|
||||||
|
|
||||||
|
charge.hasStaleBalance = charge.balance === newBalance
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this.rescanning = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createCharge: async function (wallet, data) {
|
||||||
|
try {
|
||||||
|
const resp = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/satspay/api/v1/charge',
|
||||||
|
wallet,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
this.chargeLinks.unshift(mapCharge(resp.data))
|
||||||
|
this.formDialogCharge.show = false
|
||||||
|
this.formDialogCharge.data = {
|
||||||
onchain: false,
|
onchain: false,
|
||||||
lnbits: false,
|
lnbits: false,
|
||||||
description: '',
|
description: '',
|
||||||
time: null,
|
time: null,
|
||||||
amount: null
|
amount: null
|
||||||
}
|
}
|
||||||
},
|
} catch (error) {
|
||||||
qrCodeDialog: {
|
LNbits.utils.notifyApiError(error)
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
cancelCharge: function (data) {
|
|
||||||
var self = this
|
|
||||||
self.formDialogCharge.data.description = ''
|
|
||||||
self.formDialogCharge.data.onchainwallet = ''
|
|
||||||
self.formDialogCharge.data.lnbitswallet = ''
|
|
||||||
self.formDialogCharge.data.time = null
|
|
||||||
self.formDialogCharge.data.amount = null
|
|
||||||
self.formDialogCharge.data.webhook = ''
|
|
||||||
self.formDialogCharge.data.completelink = ''
|
|
||||||
self.formDialogCharge.show = false
|
|
||||||
},
|
|
||||||
|
|
||||||
getWalletLinks: function () {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/watchonly/api/v1/wallet',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
for (i = 0; i < response.data.length; i++) {
|
|
||||||
self.walletLinks.push(response.data[i].id)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeFormDialog: function () {
|
|
||||||
this.formDialog.data = {
|
|
||||||
is_unique: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openQrCodeDialog: function (linkId) {
|
|
||||||
var self = this
|
|
||||||
var getAddresses = this.getAddresses
|
|
||||||
getAddresses(linkId)
|
|
||||||
self.current = linkId
|
|
||||||
self.Addresses.show = true
|
|
||||||
},
|
|
||||||
getCharges: function () {
|
|
||||||
var self = this
|
|
||||||
var getAddressBalance = this.getAddressBalance
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/satspay/api/v1/charges',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.ChargeLinks = response.data.map(mapCharge)
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendFormDataCharge: function () {
|
|
||||||
var self = this
|
|
||||||
var wallet = this.g.user.wallets[0].inkey
|
|
||||||
var data = this.formDialogCharge.data
|
|
||||||
data.amount = parseInt(data.amount)
|
|
||||||
data.time = parseInt(data.time)
|
|
||||||
this.createCharge(wallet, data)
|
|
||||||
},
|
|
||||||
timerCount: function () {
|
|
||||||
self = this
|
|
||||||
var refreshIntervalId = setInterval(function () {
|
|
||||||
for (i = 0; i < self.ChargeLinks.length - 1; i++) {
|
|
||||||
if (self.ChargeLinks[i]['paid'] == 'True') {
|
|
||||||
setTimeout(function () {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/satspay/api/v1/charges/balance/' +
|
|
||||||
self.ChargeLinks[i]['id'],
|
|
||||||
'filla'
|
|
||||||
)
|
|
||||||
.then(function (response) {})
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.getCharges()
|
|
||||||
}, 20000)
|
|
||||||
},
|
|
||||||
createCharge: function (wallet, data) {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request('POST', '/satspay/api/v1/charge', wallet, data)
|
|
||||||
.then(function (response) {
|
|
||||||
self.ChargeLinks.push(mapCharge(response.data))
|
|
||||||
self.formDialogCharge.show = false
|
|
||||||
self.formDialogCharge.data = {
|
|
||||||
onchain: false,
|
|
||||||
lnbits: false,
|
|
||||||
description: '',
|
|
||||||
time: null,
|
|
||||||
amount: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteChargeLink: function (chargeId) {
|
deleteChargeLink: function (chargeId) {
|
||||||
var self = this
|
const link = _.findWhere(this.chargeLinks, {id: chargeId})
|
||||||
var link = _.findWhere(this.ChargeLinks, {id: chargeId})
|
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||||
.onOk(function () {
|
.onOk(async () => {
|
||||||
LNbits.api
|
try {
|
||||||
.request(
|
const response = await LNbits.api.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/satspay/api/v1/charge/' + chargeId,
|
'/satspay/api/v1/charge/' + chargeId,
|
||||||
self.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
|
||||||
self.ChargeLinks = _.reject(self.ChargeLinks, function (obj) {
|
this.chargeLinks = _.reject(this.chargeLinks, function (obj) {
|
||||||
return obj.id === chargeId
|
return obj.id === chargeId
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
exportchargeCSV: function () {
|
exportchargeCSV: function () {
|
||||||
var self = this
|
LNbits.utils.exportCSV(
|
||||||
LNbits.utils.exportCSV(self.ChargesTable.columns, this.ChargeLinks)
|
this.chargesTable.columns,
|
||||||
|
this.chargeLinks,
|
||||||
|
'charges'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: async function () {
|
||||||
console.log(this.g.user)
|
await this.getCharges()
|
||||||
var self = this
|
await this.getWalletLinks()
|
||||||
var getCharges = this.getCharges
|
await this.getWalletConfig()
|
||||||
getCharges()
|
setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000)
|
||||||
var getWalletLinks = this.getWalletLinks
|
await this.rescanOnchainAddresses()
|
||||||
getWalletLinks()
|
setInterval(() => this.rescanOnchainAddresses(), 10 * 1000)
|
||||||
var timerCount = this.timerCount
|
|
||||||
timerCount()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,6 +9,7 @@ from starlette.responses import HTMLResponse
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
from lnbits.extensions.watchonly.crud import get_config
|
||||||
|
|
||||||
from . import satspay_ext, satspay_renderer
|
from . import satspay_ext, satspay_renderer
|
||||||
from .crud import get_charge
|
from .crud import get_charge
|
||||||
|
@ -24,14 +25,24 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/{charge_id}", response_class=HTMLResponse)
|
@satspay_ext.get("/{charge_id}", response_class=HTMLResponse)
|
||||||
async def display(request: Request, charge_id):
|
async def display(request: Request, charge_id: str):
|
||||||
charge = await get_charge(charge_id)
|
charge = await get_charge(charge_id)
|
||||||
if not charge:
|
if not charge:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||||
)
|
)
|
||||||
wallet = await get_wallet(charge.lnbitswallet)
|
wallet = await get_wallet(charge.lnbitswallet)
|
||||||
|
onchainwallet_config = await get_config(charge.user)
|
||||||
|
inkey = wallet.inkey if wallet else None
|
||||||
|
mempool_endpoint = (
|
||||||
|
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||||
|
)
|
||||||
return satspay_renderer().TemplateResponse(
|
return satspay_renderer().TemplateResponse(
|
||||||
"satspay/display.html",
|
"satspay/display.html",
|
||||||
{"request": request, "charge": charge, "wallet_key": wallet.inkey},
|
{
|
||||||
|
"request": request,
|
||||||
|
"charge_data": charge.dict(),
|
||||||
|
"wallet_inkey": inkey,
|
||||||
|
"mempool_endpoint": mempool_endpoint,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import Query
|
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
@ -31,7 +30,12 @@ async def api_charge_create(
|
||||||
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
):
|
):
|
||||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||||
return charge.dict()
|
return {
|
||||||
|
**charge.dict(),
|
||||||
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
|
**{"time_left": charge.time_left},
|
||||||
|
**{"paid": charge.paid},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
||||||
|
@ -51,6 +55,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
{
|
{
|
||||||
**charge.dict(),
|
**charge.dict(),
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
|
**{"time_left": charge.time_left},
|
||||||
**{"paid": charge.paid},
|
**{"paid": charge.paid},
|
||||||
}
|
}
|
||||||
for charge in await get_charges(wallet.wallet.user)
|
for charge in await get_charges(wallet.wallet.user)
|
||||||
|
@ -73,6 +78,7 @@ async def api_charge_retrieve(
|
||||||
return {
|
return {
|
||||||
**charge.dict(),
|
**charge.dict(),
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
|
**{"time_left": charge.time_left},
|
||||||
**{"paid": charge.paid},
|
**{"paid": charge.paid},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,9 +99,18 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
#############################BALANCE##########################
|
#############################BALANCE##########################
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charges/balance/{charge_id}")
|
@satspay_ext.get("/api/v1/charges/balance/{charge_ids}")
|
||||||
async def api_charges_balance(charge_id):
|
async def api_charges_balance(charge_ids):
|
||||||
|
charge_id_list = charge_ids.split(",")
|
||||||
|
charges = []
|
||||||
|
for charge_id in charge_id_list:
|
||||||
|
charge = await api_charge_balance(charge_id)
|
||||||
|
charges.append(charge)
|
||||||
|
return charges
|
||||||
|
|
||||||
|
|
||||||
|
@satspay_ext.get("/api/v1/charge/balance/{charge_id}")
|
||||||
|
async def api_charge_balance(charge_id):
|
||||||
charge = await check_address_balance(charge_id)
|
charge = await check_address_balance(charge_id)
|
||||||
|
|
||||||
if not charge:
|
if not charge:
|
||||||
|
@ -125,23 +140,9 @@ async def api_charges_balance(charge_id):
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
charge.webhook = None
|
charge.webhook = None
|
||||||
return charge.dict()
|
return {
|
||||||
|
**charge.dict(),
|
||||||
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
#############################MEMPOOL##########################
|
**{"time_left": charge.time_left},
|
||||||
|
**{"paid": charge.paid},
|
||||||
|
}
|
||||||
@satspay_ext.put("/api/v1/mempool")
|
|
||||||
async def api_update_mempool(
|
|
||||||
endpoint: str = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
mempool = await update_mempool(endpoint, user=wallet.wallet.user)
|
|
||||||
return mempool.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.route("/api/v1/mempool")
|
|
||||||
async def api_get_mempool(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
mempool = await get_mempool(wallet.wallet.user)
|
|
||||||
if not mempool:
|
|
||||||
mempool = await create_mempool(user=wallet.wallet.user)
|
|
||||||
return mempool.dict()
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.services import check_invoice_status, create_invoice
|
from lnbits.core.services import check_transaction_status, create_invoice
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||||
from lnbits.extensions.subdomains.models import CreateDomain, CreateSubdomain
|
from lnbits.extensions.subdomains.models import CreateDomain, CreateSubdomain
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ async def api_subdomain_make_subdomain(domain_id, data: CreateSubdomain):
|
||||||
async def api_subdomain_send_subdomain(payment_hash):
|
async def api_subdomain_send_subdomain(payment_hash):
|
||||||
subdomain = await get_subdomain(payment_hash)
|
subdomain = await get_subdomain(payment_hash)
|
||||||
try:
|
try:
|
||||||
status = await check_invoice_status(subdomain.wallet, payment_hash)
|
status = await check_transaction_status(subdomain.wallet, payment_hash)
|
||||||
is_paid = not status.pending
|
is_paid = not status.pending
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"paid": False}
|
return {"paid": False}
|
||||||
|
|
|
@ -57,7 +57,7 @@ async def api_create_tip(data: createTips):
|
||||||
name = name.replace('"', "''")
|
name = name.replace('"', "''")
|
||||||
if not name:
|
if not name:
|
||||||
name = "Anonymous"
|
name = "Anonymous"
|
||||||
description = f'"{name}": {message}'
|
description = f"{name}: {message}"
|
||||||
charge = await create_charge(
|
charge = await create_charge(
|
||||||
user=charge_details["user"],
|
user=charge_details["user"],
|
||||||
data=CreateCharge(
|
data=CreateCharge(
|
||||||
|
|
|
@ -8,15 +8,16 @@ You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnb
|
||||||
|
|
||||||
### Wallet Account
|
### Wallet Account
|
||||||
- a user can add one or more `xPubs` or `descriptors`
|
- a user can add one or more `xPubs` or `descriptors`
|
||||||
- the `xPub` fingerprint must be unique per user
|
- the `xPub` must be unique per user
|
||||||
- such and entry is called an `Wallet Account`
|
- such and entry is called an `Wallet Account`
|
||||||
- the addresses in a `Wallet Account` are split into `Receive Addresses` and `Change Address`
|
- the addresses in a `Wallet Account` are split into `Receive Addresses` and `Change Address`
|
||||||
- the user interacts directly only with the `Receive Addresses` (by sharing them)
|
- the user interacts directly only with the `Receive Addresses` (by sharing them)
|
||||||
- see [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) for more details
|
- see [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) for more details
|
||||||
- same `xPub` will always generate the same addresses (deterministic)
|
- same `xPub` will always generate the same addresses (deterministic)
|
||||||
- when a `Wallet Account` is created, there are generated `20 Receive Addresses` and `5 Change Address`
|
- when a `Wallet Account` is created, there are generated `20 Receive Addresses` and `5 Change Address`
|
||||||
- the limits can be change from the `Config` page (see `screenshot 1`)
|
- the limits can be change from the `Config` page (see `screenshot 1`)
|
||||||
- regular wallets only scan up to `20` empty receive addresses. If the user generates addresses beyond this limit a warning is shown (see `screenshot 4`)
|
- regular wallets only scan up to `20` empty receive addresses. If the user generates addresses beyond this limit a warning is shown (see `screenshot 4`)
|
||||||
|
- an account can be added `From Hardware Device`
|
||||||
|
|
||||||
### Scan Blockchain
|
### Scan Blockchain
|
||||||
- when the user clicks `Scan Blockchain`, the wallet will loop over the all addresses (for each account)
|
- when the user clicks `Scan Blockchain`, the wallet will loop over the all addresses (for each account)
|
||||||
|
@ -48,33 +49,32 @@ You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnb
|
||||||
- shows the UTXOs for all wallets
|
- shows the UTXOs for all wallets
|
||||||
- there can be multiple UTXOs for the same address
|
- there can be multiple UTXOs for the same address
|
||||||
|
|
||||||
### Make Payment
|
### New Payment
|
||||||
- create a new `Partially Signed Bitcoin Transaction`
|
- create a new `Partially Signed Bitcoin Transaction`
|
||||||
- multiple `Send Addresses` can be added
|
- multiple `Send Addresses` can be added
|
||||||
- the `Max` button next to an address is for sending the remaining funds to this address (no change)
|
- the `Max` button next to an address is for sending the remaining funds to this address (no change)
|
||||||
- the user can select the inputs (UTXOs) manually, or it can use of the basic selection algorithms
|
- the user can select the inputs (UTXOs) manually, or it can use of the basic selection algorithms
|
||||||
- amounts have to be provided for the `Send Addresses` beforehand (so the algorithm knows the amount to be selected)
|
- amounts have to be provided for the `Send Addresses` beforehand (so the algorithm knows the amount to be selected)
|
||||||
- `Show Advanced` allows to (see `screenshot 2`):
|
- `Show Change` allows to select from which account the change address will be selected (defaults to the first one)
|
||||||
- select from which account the change address will be selected (defaults to the first one)
|
- `Show Custom Fee` allows to manually select the fee
|
||||||
- select the `Fee Rate`
|
- it defaults to the `Medium` value at the moment the `New Payment` button was clicked
|
||||||
- it defaults to the `Medium` value at the moment the `Make Payment` button was clicked
|
- it can be refreshed
|
||||||
- it can be refreshed
|
- warnings are shown if the fee is too Low or to High
|
||||||
- warnings are shown if the fee is too Low or to High
|
|
||||||
|
|
||||||
### Create PSBT
|
### Check & Send
|
||||||
- based on the Inputs & Outputs selected by the user a PSBT will be generated
|
- creates the PSBT and sends it to the Hardware Wallet
|
||||||
- this wallet is watch-only, therefore does not support signing
|
- a confirmation will be shown for each Output and for the Fee
|
||||||
- it is not mandatory for the `Selected Amount` to be grater than `Payed Amount`
|
- after the user confirms the addresses and amounts, the transaction will be signed on the Hardware Device
|
||||||
- the generated PSBT can be combined with other PSBTs that add more inputs.
|
|
||||||
- the generated PSBT can be imported for signing into different wallets like Electrum
|
### Share PSBT
|
||||||
- import the PSBT into Electrum and check the In/Outs/Fee (see `screenshot 3`)
|
- Show the PSBT without sending it to the Hardware Wallet
|
||||||
|
|
||||||
## Screensots
|
## Screensots
|
||||||
- screenshot 1:
|
- screenshot 1:
|
||||||

|

|
||||||
|
|
||||||
- screenshot 2:
|
- screenshot 2:
|
||||||

|

|
||||||
|
|
||||||
- screenshot 3:
|
- screenshot 3:
|
||||||

|

|
||||||
|
|
|
@ -4,8 +4,8 @@ from typing import List, Optional
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .helpers import derive_address, parse_key
|
from .helpers import derive_address
|
||||||
from .models import Address, Config, Mempool, WalletAccount
|
from .models import Address, Config, WalletAccount
|
||||||
|
|
||||||
##########################WALLETS####################
|
##########################WALLETS####################
|
||||||
|
|
||||||
|
@ -22,9 +22,10 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
address_no,
|
address_no,
|
||||||
balance
|
balance,
|
||||||
|
network
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
|
@ -35,6 +36,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
||||||
w.type,
|
w.type,
|
||||||
w.address_no,
|
w.address_no,
|
||||||
w.balance,
|
w.balance,
|
||||||
|
w.network,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,9 +50,10 @@ async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]:
|
||||||
return WalletAccount.from_row(row) if row else None
|
return WalletAccount.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_watch_wallets(user: str) -> List[WalletAccount]:
|
async def get_watch_wallets(user: str, network: str) -> List[WalletAccount]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""SELECT * FROM watchonly.wallets WHERE "user" = ?""", (user,)
|
"""SELECT * FROM watchonly.wallets WHERE "user" = ? AND network = ?""",
|
||||||
|
(user, network),
|
||||||
)
|
)
|
||||||
return [WalletAccount(**row) for row in rows]
|
return [WalletAccount(**row) for row in rows]
|
||||||
|
|
||||||
|
@ -238,41 +241,3 @@ async def get_config(user: str) -> Optional[Config]:
|
||||||
"""SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
|
"""SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
|
||||||
)
|
)
|
||||||
return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None
|
return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None
|
||||||
|
|
||||||
|
|
||||||
######################MEMPOOL#######################
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
async def create_mempool(user: str) -> Optional[Mempool]:
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO watchonly.mempool ("user",endpoint)
|
|
||||||
VALUES (?, ?)
|
|
||||||
""",
|
|
||||||
(user, "https://mempool.space"),
|
|
||||||
)
|
|
||||||
row = await db.fetchone(
|
|
||||||
"""SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
|
|
||||||
)
|
|
||||||
return Mempool.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
async def update_mempool(user: str, **kwargs) -> Optional[Mempool]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""UPDATE watchonly.mempool SET {q} WHERE "user" = ?""",
|
|
||||||
(*kwargs.values(), user),
|
|
||||||
)
|
|
||||||
row = await db.fetchone(
|
|
||||||
"""SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
|
|
||||||
)
|
|
||||||
return Mempool.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
async def get_mempool(user: str) -> Mempool:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"""SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
|
|
||||||
)
|
|
||||||
return Mempool.from_row(row) if row else None
|
|
||||||
|
|
|
@ -77,7 +77,19 @@ async def m004_create_config_table(db):
|
||||||
);"""
|
);"""
|
||||||
)
|
)
|
||||||
|
|
||||||
### TODO: fix statspay dependcy first
|
|
||||||
# await db.execute(
|
async def m005_add_network_column_to_wallets(db):
|
||||||
# "DROP TABLE watchonly.wallets;"
|
"""
|
||||||
# )
|
Add network' column to the 'wallets' table
|
||||||
|
"""
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE watchonly.wallets ADD COLUMN network TEXT DEFAULT 'Mainnet';"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m006_drop_mempool_table(db):
|
||||||
|
"""
|
||||||
|
Mempool data is now part of `config`
|
||||||
|
"""
|
||||||
|
await db.execute("DROP TABLE watchonly.mempool;")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -8,6 +8,7 @@ from pydantic import BaseModel
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
masterpub: str = Query("")
|
masterpub: str = Query("")
|
||||||
title: str = Query("")
|
title: str = Query("")
|
||||||
|
network: str = "Mainnet"
|
||||||
|
|
||||||
|
|
||||||
class WalletAccount(BaseModel):
|
class WalletAccount(BaseModel):
|
||||||
|
@ -19,22 +20,13 @@ class WalletAccount(BaseModel):
|
||||||
address_no: int
|
address_no: int
|
||||||
balance: int
|
balance: int
|
||||||
type: str = ""
|
type: str = ""
|
||||||
|
network: str = "Mainnet"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "WalletAccount":
|
def from_row(cls, row: Row) -> "WalletAccount":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
class Mempool(BaseModel):
|
|
||||||
user: str
|
|
||||||
endpoint: str
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Mempool":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class Address(BaseModel):
|
class Address(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
address: str
|
address: str
|
||||||
|
@ -57,7 +49,7 @@ class TransactionInput(BaseModel):
|
||||||
address: str
|
address: str
|
||||||
branch_index: int
|
branch_index: int
|
||||||
address_index: int
|
address_index: int
|
||||||
masterpub_fingerprint: str
|
wallet: str
|
||||||
tx_hex: str
|
tx_hex: str
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,10 +58,11 @@ class TransactionOutput(BaseModel):
|
||||||
address: str
|
address: str
|
||||||
branch_index: int = None
|
branch_index: int = None
|
||||||
address_index: int = None
|
address_index: int = None
|
||||||
masterpub_fingerprint: str = None
|
wallet: str = None
|
||||||
|
|
||||||
|
|
||||||
class MasterPublicKey(BaseModel):
|
class MasterPublicKey(BaseModel):
|
||||||
|
id: str
|
||||||
public_key: str
|
public_key: str
|
||||||
fingerprint: str
|
fingerprint: str
|
||||||
|
|
||||||
|
@ -82,8 +75,23 @@ class CreatePsbt(BaseModel):
|
||||||
tx_size: int
|
tx_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractPsbt(BaseModel):
|
||||||
|
psbtBase64 = "" # // todo snake case
|
||||||
|
inputs: List[TransactionInput]
|
||||||
|
|
||||||
|
|
||||||
|
class SignedTransaction(BaseModel):
|
||||||
|
tx_hex: Optional[str]
|
||||||
|
tx_json: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastTransaction(BaseModel):
|
||||||
|
tx_hex: str
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
mempool_endpoint = "https://mempool.space"
|
mempool_endpoint = "https://mempool.space"
|
||||||
receive_gap_limit = 20
|
receive_gap_limit = 20
|
||||||
change_gap_limit = 5
|
change_gap_limit = 5
|
||||||
sats_denominated = True
|
sats_denominated = True
|
||||||
|
network = "Mainnet"
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
<div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="selectedWallet"
|
||||||
|
:options="accounts"
|
||||||
|
label="Wallet Account"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
multiple
|
||||||
|
:options="filterOptions"
|
||||||
|
v-model="filterValues"
|
||||||
|
label="Filter"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="addressesTable.filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
style="height: 400px"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="getFilteredAddresses()"
|
||||||
|
row-key="id"
|
||||||
|
virtual-scroll
|
||||||
|
:columns="addressesTable.columns"
|
||||||
|
:pagination.sync="addressesTable.pagination"
|
||||||
|
:filter="addressesTable.filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="address" :props="props">
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
style="color: unset"
|
||||||
|
:href="'https://'+ mempoolEndpoint + '/address/' + props.row.address"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{props.row.address}}</a
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.branch_index === 1"
|
||||||
|
color="orange"
|
||||||
|
class="q-mr-md"
|
||||||
|
outline
|
||||||
|
>
|
||||||
|
change
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
v-if="props.row.gapLimitExceeded"
|
||||||
|
color="yellow"
|
||||||
|
icon="warning"
|
||||||
|
title="Gap Limit Exceeded"
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
outline
|
||||||
|
class="q-ml-md"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td
|
||||||
|
key="amount"
|
||||||
|
:props="props"
|
||||||
|
:class="props.row.amount > 0 ? 'text-green-13 text-weight-bold' : ''"
|
||||||
|
>
|
||||||
|
<div>{{satBtc(props.row.amount)}}</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="note" :props="props" :class="">
|
||||||
|
<div>{{props.row.note}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="wallet" :props="props" :class="">
|
||||||
|
<div>{{getWalletName(props.row.wallet)}}</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-show="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
icon="qr_code"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
@click="showAddressDetails(props.row)"
|
||||||
|
>
|
||||||
|
QR Code</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
icon="refresh"
|
||||||
|
color="grey"
|
||||||
|
@click="scanAddress(props.row)"
|
||||||
|
>
|
||||||
|
Rescan</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
icon="history"
|
||||||
|
color="grey"
|
||||||
|
@click="searchInTab('history', props.row.address)"
|
||||||
|
>History</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
color="grey"
|
||||||
|
@click="searchInTab('utxos', props.row.address)"
|
||||||
|
>View Coins</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Note:</div>
|
||||||
|
<div class="col-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="props.row.note"
|
||||||
|
type="text"
|
||||||
|
label="Note"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="updateNoteForAddress(props.row, props.row.note)"
|
||||||
|
>Update
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.row.error" class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-10 q-pr-lg">
|
||||||
|
<q-badge color="red">{{props.row.error}}</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.gapLimitExceeded"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-10 q-pr-lg">
|
||||||
|
<q-badge color="yellow" text-color="black"
|
||||||
|
>Gap limit of 20 addresses exceeded. Other wallets might not
|
||||||
|
detect funds at this address.</q-badge
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
|
@ -0,0 +1,121 @@
|
||||||
|
async function addressList(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('address-list', {
|
||||||
|
name: 'address-list',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'addresses',
|
||||||
|
'accounts',
|
||||||
|
'mempool-endpoint',
|
||||||
|
'inkey',
|
||||||
|
'sats-denominated'
|
||||||
|
],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
history: [],
|
||||||
|
selectedWallet: null,
|
||||||
|
note: '',
|
||||||
|
filterOptions: [
|
||||||
|
'Show Change Addresses',
|
||||||
|
'Show Gap Addresses',
|
||||||
|
'Only With Amount'
|
||||||
|
],
|
||||||
|
filterValues: [],
|
||||||
|
|
||||||
|
addressesTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'expand',
|
||||||
|
align: 'left',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Address',
|
||||||
|
field: 'address',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Amount',
|
||||||
|
field: 'amount',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'note',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Note',
|
||||||
|
field: 'note',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wallet',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Account',
|
||||||
|
field: 'wallet',
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 0,
|
||||||
|
sortBy: 'amount',
|
||||||
|
descending: true
|
||||||
|
},
|
||||||
|
filter: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
getWalletName: function (walletId) {
|
||||||
|
const wallet = (this.accounts || []).find(wl => wl.id === walletId)
|
||||||
|
return wallet ? wallet.title : 'unknown'
|
||||||
|
},
|
||||||
|
getFilteredAddresses: function () {
|
||||||
|
const selectedWalletId = this.selectedWallet?.id
|
||||||
|
const filter = this.filterValues || []
|
||||||
|
const includeChangeAddrs = filter.includes('Show Change Addresses')
|
||||||
|
const includeGapAddrs = filter.includes('Show Gap Addresses')
|
||||||
|
const excludeNoAmount = filter.includes('Only With Amount')
|
||||||
|
|
||||||
|
const walletsLimit = (this.accounts || []).reduce((r, w) => {
|
||||||
|
r[`_${w.id}`] = w.address_no
|
||||||
|
return r
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const fAddresses = this.addresses.filter(
|
||||||
|
a =>
|
||||||
|
(includeChangeAddrs || !a.isChange) &&
|
||||||
|
(includeGapAddrs ||
|
||||||
|
a.isChange ||
|
||||||
|
a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
|
||||||
|
!(excludeNoAmount && a.amount === 0) &&
|
||||||
|
(!selectedWalletId || a.wallet === selectedWalletId)
|
||||||
|
)
|
||||||
|
return fAddresses
|
||||||
|
},
|
||||||
|
|
||||||
|
scanAddress: async function (addressData) {
|
||||||
|
this.$emit('scan:address', addressData)
|
||||||
|
},
|
||||||
|
showAddressDetails: function (addressData) {
|
||||||
|
this.$emit('show-address-details', addressData)
|
||||||
|
},
|
||||||
|
searchInTab: function (tab, value) {
|
||||||
|
this.$emit('search:tab', {tab, value})
|
||||||
|
},
|
||||||
|
updateNoteForAddress: async function (addressData, note) {
|
||||||
|
this.$emit('update:note', {addressId: addressData.id, note})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Fee Rate:</div>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="feeRate"
|
||||||
|
:rules="[val => !!val || 'Field is required']"
|
||||||
|
type="number"
|
||||||
|
label="sats/vbyte"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-7">
|
||||||
|
<q-slider
|
||||||
|
v-model="feeRate"
|
||||||
|
color="orange"
|
||||||
|
markers
|
||||||
|
snap
|
||||||
|
label
|
||||||
|
label-always
|
||||||
|
:label-value="getFeeRateLabel(feeRate)"
|
||||||
|
:min="1"
|
||||||
|
:max="recommededFees.fastestFee"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="feeRate < recommededFees.hourFee || feeRate > recommededFees.fastestFee"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-10 q-pr-lg">
|
||||||
|
<q-badge v-if="feeRate < recommededFees.hourFee" color="pink" size="lg">
|
||||||
|
Warning! The fee is too low. The transaction might take a long time to
|
||||||
|
confirm.
|
||||||
|
</q-badge>
|
||||||
|
<q-badge v-if="feeRate > recommededFees.fastestFee" color="pink">
|
||||||
|
Warning! The fee is too high. You might be overpaying for this
|
||||||
|
transaction.
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Fee:</div>
|
||||||
|
<div class="col-3 q-pr-lg">{{feeValue}} sats</div>
|
||||||
|
<div class="col-7">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
icon="refresh"
|
||||||
|
color="grey"
|
||||||
|
class="float-right"
|
||||||
|
@click="refreshRecommendedFees()"
|
||||||
|
>Refresh Fee Rates</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,64 @@
|
||||||
|
async function feeRate(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('fee-rate', {
|
||||||
|
name: 'fee-rate',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: ['rate', 'fee-value', 'sats-denominated', 'mempool-endpoint'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
feeRate: {
|
||||||
|
get: function () {
|
||||||
|
return this['rate']
|
||||||
|
},
|
||||||
|
set: function (value) {
|
||||||
|
this.$emit('update:rate', +value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
recommededFees: {
|
||||||
|
fastestFee: 1,
|
||||||
|
halfHourFee: 1,
|
||||||
|
hourFee: 1,
|
||||||
|
economyFee: 1,
|
||||||
|
minimumFee: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshRecommendedFees: async function () {
|
||||||
|
const fn = async () => {
|
||||||
|
const {
|
||||||
|
bitcoin: {fees: feesAPI}
|
||||||
|
} = mempoolJS({
|
||||||
|
hostname: this.mempoolEndpoint
|
||||||
|
})
|
||||||
|
return feesAPI.getFeesRecommended()
|
||||||
|
}
|
||||||
|
this.recommededFees = await retryWithDelay(fn)
|
||||||
|
},
|
||||||
|
getFeeRateLabel: function (feeRate) {
|
||||||
|
const fees = this.recommededFees
|
||||||
|
if (feeRate >= fees.fastestFee)
|
||||||
|
return `High Priority (${feeRate} sat/vB)`
|
||||||
|
if (feeRate >= fees.halfHourFee)
|
||||||
|
return `Medium Priority (${feeRate} sat/vB)`
|
||||||
|
if (feeRate >= fees.hourFee) return `Low Priority (${feeRate} sat/vB)`
|
||||||
|
return `No Priority (${feeRate} sat/vB)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: async function () {
|
||||||
|
await this.refreshRecommendedFees()
|
||||||
|
this.feeRate = this.recommededFees.halfHourFee
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
<div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col q-pr-lg"></div>
|
||||||
|
<div class="col q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn outline color="grey" label="...">
|
||||||
|
<q-menu auto-close>
|
||||||
|
<q-list style="min-width: 100px">
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section @click="exportHistoryToCSV"
|
||||||
|
>Export to CSV</q-item-section
|
||||||
|
>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
style="height: 400px"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="getFilteredAddressesHistory()"
|
||||||
|
row-key="id"
|
||||||
|
virtual-scroll
|
||||||
|
:columns="historyTable.columns"
|
||||||
|
:pagination.sync="historyTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
:icon="props.row.expanded ? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="status" :props="props">
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.sent"
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
color="orange"
|
||||||
|
class="q-mr-md cursor-pointer"
|
||||||
|
>
|
||||||
|
{{props.row.confirmed ? 'Sent' : 'Sending...'}}
|
||||||
|
</q-badge>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.received"
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
color="green"
|
||||||
|
class="q-mr-md cursor-pointer"
|
||||||
|
>
|
||||||
|
{{props.row.confirmed ? 'Received' : 'Receiving...'}}
|
||||||
|
</q-badge>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
key="amount"
|
||||||
|
:props="props"
|
||||||
|
:class="props.row.amount && props.row.received > 0 ? 'text-green-13 text-weight-bold' : ''"
|
||||||
|
>
|
||||||
|
<div>{{satBtc(props.row.totalAmount || props.row.amount)}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="address" :props="props">
|
||||||
|
<a
|
||||||
|
v-if="!props.row.sameTxItems"
|
||||||
|
style="color: unset"
|
||||||
|
:href="'https://' + mempoolEndpoint + '/address/' + props.row.address"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{props.row.address}}</a
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.sameTxItems"
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
outline
|
||||||
|
color="blue"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</q-badge>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="date" :props="props"> {{ props.row.date }} </q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-show="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Transaction Id</div>
|
||||||
|
<div class="col-10 q-pr-lg">
|
||||||
|
<a
|
||||||
|
style="color: unset"
|
||||||
|
:href="'https://' +mempoolEndpoint + '/tx/' + props.row.txId"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{props.row.txId}}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.sameTxItems"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg">UTXOs</div>
|
||||||
|
<div class="col-4 q-pr-lg">{{satBtc(props.row.amount)}}</div>
|
||||||
|
<div class="col-6 q-pr-lg">{{props.row.address}}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="s in props.row.sameTxItems || []"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-4 q-pr-lg">{{satBtc(s.amount)}}</div>
|
||||||
|
<div class="col-6 q-pr-lg">{{s.address}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Fee</div>
|
||||||
|
<div class="col-4 q-pr-lg">{{satBtc(props.row.fee)}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Block Height</div>
|
||||||
|
<div class="col-4 q-pr-lg">{{props.row.height}}</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
|
@ -0,0 +1,94 @@
|
||||||
|
async function history(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('history', {
|
||||||
|
name: 'history',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: ['history', 'mempool-endpoint', 'sats-denominated', 'filter'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
historyTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'expand',
|
||||||
|
align: 'left',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Amount',
|
||||||
|
field: 'amount',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Address',
|
||||||
|
field: 'address',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Date',
|
||||||
|
field: 'date',
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
exportColums: [
|
||||||
|
{
|
||||||
|
label: 'Action',
|
||||||
|
field: 'action'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Date&Time',
|
||||||
|
field: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Amount',
|
||||||
|
field: 'amount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fee',
|
||||||
|
field: 'fee'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Transaction Id',
|
||||||
|
field: 'txId'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
getFilteredAddressesHistory: function () {
|
||||||
|
return this.history.filter(a => (!a.isChange || a.sent) && !a.isSubItem)
|
||||||
|
},
|
||||||
|
exportHistoryToCSV: function () {
|
||||||
|
const history = this.getFilteredAddressesHistory().map(a => ({
|
||||||
|
...a,
|
||||||
|
action: a.sent ? 'Sent' : 'Received'
|
||||||
|
}))
|
||||||
|
LNbits.utils.exportCSV(
|
||||||
|
this.historyTable.exportColums,
|
||||||
|
history,
|
||||||
|
'address-history'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="checkbox-wrapper" @click="check">
|
||||||
|
<div :class="{ checkbox: true, checked: checked }"></div>
|
||||||
|
<div class="title">{{ title }}</div>
|
||||||
|
<q-btn color="primary">XXX</q-btn>
|
||||||
|
</div>
|
|
@ -0,0 +1,16 @@
|
||||||
|
async function initMyCheckbox(path) {
|
||||||
|
const t = await loadTemplateAsync(path)
|
||||||
|
Vue.component('my-checkbox', {
|
||||||
|
name: 'my-checkbox',
|
||||||
|
template: t,
|
||||||
|
data() {
|
||||||
|
return {checked: false, title: 'Check me'}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
check() {
|
||||||
|
this.checked = !this.checked
|
||||||
|
console.log('### checked', this.checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,312 @@
|
||||||
|
<div>
|
||||||
|
<q-form @submit="checkAndSend" ref="paymentFormRef" class="q-gutter-md">
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<send-to
|
||||||
|
:data.sync="sendToList"
|
||||||
|
:fee-rate="feeRate"
|
||||||
|
:tx-size="txSizeNoChange"
|
||||||
|
:selected-amount="selectedAmount"
|
||||||
|
:sats-denominated="satsDenominated"
|
||||||
|
@update:outputs="handleOutputsChange"
|
||||||
|
></send-to>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-toggle
|
||||||
|
label="Show Custom Fee"
|
||||||
|
color="secodary"
|
||||||
|
class="float-left"
|
||||||
|
v-model="showCustomFee"
|
||||||
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="float-right">
|
||||||
|
<span>Fee Rate:</span>
|
||||||
|
<span class="text-subtitle2 q-ml-md">
|
||||||
|
{{feeRate}} sats/vbyte</span
|
||||||
|
>
|
||||||
|
<span class="q-ml-lg">Fee:</span>
|
||||||
|
<span class="text-subtitle2 q-ml-md"> {{satBtc(feeValue)}} </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="showCustomFee" class="row items-center no-wrap q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-separator class="q-mb-md"></q-separator>
|
||||||
|
<fee-rate
|
||||||
|
:fee-value="feeValue"
|
||||||
|
:rate.sync="feeRate"
|
||||||
|
:mempool-endpoint="mempoolEndpoint"
|
||||||
|
:sats-denominated="satsDenominated"
|
||||||
|
></fee-rate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-toggle
|
||||||
|
label="Show Coin Select"
|
||||||
|
color="secodary"
|
||||||
|
class="float-left"
|
||||||
|
v-model="showCoinSelect"
|
||||||
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="float-right">
|
||||||
|
<span>Balance:</span>
|
||||||
|
<span class="text-subtitle2 q-ml-md"> {{satBtc(balance)}} </span>
|
||||||
|
<span class="q-ml-lg">Selected:</span>
|
||||||
|
<span class="text-subtitle2 q-ml-md">
|
||||||
|
{{satBtc(selectedAmount)}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="showCoinSelect" class="row items-center no-wrap q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-separator class="q-mb-md"></q-separator>
|
||||||
|
<utxo-list
|
||||||
|
ref="utxoList"
|
||||||
|
:utxos="utxos"
|
||||||
|
:selectable="true"
|
||||||
|
:payed-amount="totalPayedAmount"
|
||||||
|
:mempool-endpoint="mempoolEndpoint"
|
||||||
|
:sats-denominated="satsDenominated"
|
||||||
|
:accounts="accounts"
|
||||||
|
></utxo-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-toggle
|
||||||
|
label="Show Change"
|
||||||
|
color="secodary"
|
||||||
|
class="float-left"
|
||||||
|
v-model="showChange"
|
||||||
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-4">
|
||||||
|
<q-badge
|
||||||
|
v-if="changeAmount > 0 && changeAmount < DUST_LIMIT"
|
||||||
|
class="text-subtitle2 float-right"
|
||||||
|
color="yellow"
|
||||||
|
text-color="black"
|
||||||
|
>
|
||||||
|
Below dust limit. Will be used as fee.
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="float-right">
|
||||||
|
<span>Change:</span>
|
||||||
|
<span v-if="changeAmount < 0" class="text-subtitle2 q-ml-md">
|
||||||
|
{{satBtc(0)}}
|
||||||
|
</span>
|
||||||
|
<span v-if="changeAmount >= 0" class="text-subtitle2 q-ml-md">
|
||||||
|
{{satBtc(changeAmount)}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="showChange" class="row items-center no-wrap q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-separator class="q-mb-md"></q-separator>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-2 q-pr-lg">Change Account:</div>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="changeWallet"
|
||||||
|
:options="accounts"
|
||||||
|
@input="selectChangeAddress"
|
||||||
|
:rules="[val => !!val || 'Field is required']"
|
||||||
|
label="Wallet Account"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-7">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
v-model.trim="changeAddress.address"
|
||||||
|
:rules="[val => !!val || 'Field is required']"
|
||||||
|
type="text"
|
||||||
|
label="Change Address"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md q-pt-lg">
|
||||||
|
<div class="col-3">
|
||||||
|
<q-btn-dropdown
|
||||||
|
split
|
||||||
|
unelevated
|
||||||
|
:disabled="changeAmount < 0 || showChecking"
|
||||||
|
label="Check & Send"
|
||||||
|
color="green"
|
||||||
|
type="submit"
|
||||||
|
class="btn-full"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item :disabled="changeAmount < 0" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Serial Port</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Sign using a Serial Port device</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item @click="showPsbtDialog" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Share PSBT</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Share the PSBT as text or Animated QR Code</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-9">
|
||||||
|
<q-spinner
|
||||||
|
v-if="showChecking"
|
||||||
|
size="2.55em"
|
||||||
|
color="primary"
|
||||||
|
></q-spinner>
|
||||||
|
<q-badge
|
||||||
|
v-if="changeAmount < 0"
|
||||||
|
class="text-subtitle2 float-right"
|
||||||
|
color="yellow"
|
||||||
|
text-color="black"
|
||||||
|
>
|
||||||
|
The payed amount is higher than the selected amount!
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<q-dialog v-model="showPsbt" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="psbtBase64"
|
||||||
|
type="textarea"
|
||||||
|
rows="25"
|
||||||
|
cols="200"
|
||||||
|
label="PSBT"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="showFinalTx" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl">
|
||||||
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
|
<div class="col-12">
|
||||||
|
<span class="text-subtitle1">Transaction Details</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-lg"></q-separator>
|
||||||
|
<div v-if="signedTx" class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
|
<div class="col-3 q-pr-lg">Version</div>
|
||||||
|
<div class="col-9">{{signedTx.version}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
|
<div class="col-3 q-pr-lg">Locktime</div>
|
||||||
|
<div class="col-9">{{signedTx.locktime}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-sm">
|
||||||
|
<div class="col-3 q-pr-lg">Fee</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<q-badge color="orange">{{satBtc(signedTx.fee)}} </q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-lg"></q-separator>
|
||||||
|
<span class="text-subtitle2">Outputs</span>
|
||||||
|
<q-separator class="q-mb-lg"></q-separator>
|
||||||
|
<div
|
||||||
|
v-for="out in signedTx.outputs"
|
||||||
|
class="row items-center no-wrap q-mb-sm"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-badge color="orange">{{satBtc(out.amount)}}</q-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-9">
|
||||||
|
<q-badge outline color="blue">{{out.address}}</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-lg"></q-separator>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="signedTxHex"
|
||||||
|
type="textarea"
|
||||||
|
cols="300"
|
||||||
|
rows="1"
|
||||||
|
label="Signed Tx Hex"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="psbtBase64Signed"
|
||||||
|
ype="textarea"
|
||||||
|
cols="300"
|
||||||
|
rows="1"
|
||||||
|
label="PSBT"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
class="float-left"
|
||||||
|
@click="broadcastTransaction"
|
||||||
|
>Send</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
336
lnbits/extensions/watchonly/static/components/payment/payment.js
Normal file
336
lnbits/extensions/watchonly/static/components/payment/payment.js
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
async function payment(path) {
|
||||||
|
const t = await loadTemplateAsync(path)
|
||||||
|
Vue.component('payment', {
|
||||||
|
name: 'payment',
|
||||||
|
template: t,
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'accounts',
|
||||||
|
'addresses',
|
||||||
|
'utxos',
|
||||||
|
'mempool-endpoint',
|
||||||
|
'sats-denominated',
|
||||||
|
'serial-signer-ref',
|
||||||
|
'adminkey'
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
immediate: true,
|
||||||
|
accounts() {
|
||||||
|
this.updateChangeAddress()
|
||||||
|
},
|
||||||
|
addresses() {
|
||||||
|
this.updateChangeAddress()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
DUST_LIMIT: 546,
|
||||||
|
tx: null,
|
||||||
|
psbtBase64: null,
|
||||||
|
psbtBase64Signed: null,
|
||||||
|
signedTx: null,
|
||||||
|
signedTxHex: null,
|
||||||
|
sentTxId: null,
|
||||||
|
signedTxId: null,
|
||||||
|
paymentTab: 'destination',
|
||||||
|
sendToList: [{address: '', amount: undefined}],
|
||||||
|
changeWallet: null,
|
||||||
|
changeAddress: {},
|
||||||
|
showCustomFee: false,
|
||||||
|
showCoinSelect: false,
|
||||||
|
showChecking: false,
|
||||||
|
showChange: false,
|
||||||
|
showPsbt: false,
|
||||||
|
showFinalTx: false,
|
||||||
|
feeRate: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
txSize: function () {
|
||||||
|
const tx = this.createTx()
|
||||||
|
return Math.round(txSize(tx))
|
||||||
|
},
|
||||||
|
txSizeNoChange: function () {
|
||||||
|
const tx = this.createTx(true)
|
||||||
|
return Math.round(txSize(tx))
|
||||||
|
},
|
||||||
|
feeValue: function () {
|
||||||
|
return this.feeRate * this.txSize
|
||||||
|
},
|
||||||
|
selectedAmount: function () {
|
||||||
|
return this.utxos
|
||||||
|
.filter(utxo => utxo.selected)
|
||||||
|
.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
},
|
||||||
|
changeAmount: function () {
|
||||||
|
return (
|
||||||
|
this.selectedAmount -
|
||||||
|
this.totalPayedAmount -
|
||||||
|
this.feeRate * this.txSize
|
||||||
|
)
|
||||||
|
},
|
||||||
|
balance: function () {
|
||||||
|
return this.utxos.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
},
|
||||||
|
totalPayedAmount: function () {
|
||||||
|
return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
checkAndSend: async function () {
|
||||||
|
this.showChecking = true
|
||||||
|
try {
|
||||||
|
if (!this.serialSignerRef.isConnected()) {
|
||||||
|
const portOpen = await this.serialSignerRef.openSerialPort()
|
||||||
|
if (!portOpen) return
|
||||||
|
}
|
||||||
|
if (!this.serialSignerRef.isAuthenticated()) {
|
||||||
|
await this.serialSignerRef.hwwShowPasswordDialog()
|
||||||
|
const authenticated = await this.serialSignerRef.isAuthenticating()
|
||||||
|
if (!authenticated) return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.createPsbt()
|
||||||
|
|
||||||
|
if (this.psbtBase64) {
|
||||||
|
const txData = {
|
||||||
|
outputs: this.tx.outputs,
|
||||||
|
feeRate: this.tx.fee_rate,
|
||||||
|
feeValue: this.feeValue
|
||||||
|
}
|
||||||
|
await this.serialSignerRef.hwwSendPsbt(this.psbtBase64, txData)
|
||||||
|
await this.serialSignerRef.isSendingPsbt()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot check and sign PSBT!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.showChecking = false
|
||||||
|
this.psbtBase64 = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showPsbtDialog: async function () {
|
||||||
|
try {
|
||||||
|
const valid = await this.$refs.paymentFormRef.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
const data = await this.createPsbt()
|
||||||
|
if (data) {
|
||||||
|
this.showPsbt = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to create PSBT!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPsbt: async function () {
|
||||||
|
try {
|
||||||
|
console.log('### this.createPsbt')
|
||||||
|
this.tx = this.createTx()
|
||||||
|
for (const input of this.tx.inputs) {
|
||||||
|
input.tx_hex = await this.fetchTxHex(input.tx_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeOutput = this.tx.outputs.find(o => o.branch_index === 1)
|
||||||
|
if (changeOutput) changeOutput.amount = this.changeAmount
|
||||||
|
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/watchonly/api/v1/psbt',
|
||||||
|
this.adminkey,
|
||||||
|
this.tx
|
||||||
|
)
|
||||||
|
|
||||||
|
this.psbtBase64 = data
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createTx: function (excludeChange = false) {
|
||||||
|
const tx = {
|
||||||
|
fee_rate: this.feeRate,
|
||||||
|
masterpubs: this.accounts.map(w => ({
|
||||||
|
id: w.id,
|
||||||
|
public_key: w.masterpub,
|
||||||
|
fingerprint: w.fingerprint
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
tx.inputs = this.utxos
|
||||||
|
.filter(utxo => utxo.selected)
|
||||||
|
.map(mapUtxoToPsbtInput)
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout
|
||||||
|
)
|
||||||
|
|
||||||
|
tx.outputs = this.sendToList.map(out => ({
|
||||||
|
address: out.address,
|
||||||
|
amount: out.amount
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!excludeChange) {
|
||||||
|
const change = this.createChangeOutput()
|
||||||
|
const diffAmount = this.selectedAmount - this.totalPayedAmount
|
||||||
|
if (diffAmount >= this.DUST_LIMIT) {
|
||||||
|
tx.outputs.push(change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.tx_size = Math.round(txSize(tx))
|
||||||
|
tx.inputs = _.shuffle(tx.inputs)
|
||||||
|
tx.outputs = _.shuffle(tx.outputs)
|
||||||
|
|
||||||
|
return tx
|
||||||
|
},
|
||||||
|
createChangeOutput: function () {
|
||||||
|
const change = this.changeAddress
|
||||||
|
const walletAcount =
|
||||||
|
this.accounts.find(w => w.id === change.wallet) || {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: change.address,
|
||||||
|
address_index: change.addressIndex,
|
||||||
|
branch_index: change.isChange ? 1 : 0,
|
||||||
|
wallet: walletAcount.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectChangeAddress: function (account) {
|
||||||
|
if (!account) this.changeAddress = ''
|
||||||
|
this.changeAddress =
|
||||||
|
this.addresses.find(
|
||||||
|
a => a.wallet === account.id && a.isChange && !a.hasActivity
|
||||||
|
) || {}
|
||||||
|
},
|
||||||
|
updateChangeAddress: function () {
|
||||||
|
if (this.changeWallet) {
|
||||||
|
const changeAccount = (this.accounts || []).find(
|
||||||
|
w => w.id === this.changeWallet.id
|
||||||
|
)
|
||||||
|
// change account deleted
|
||||||
|
if (!changeAccount) {
|
||||||
|
this.changeWallet = this.accounts[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.changeWallet = this.accounts[0]
|
||||||
|
}
|
||||||
|
this.selectChangeAddress(this.changeWallet)
|
||||||
|
},
|
||||||
|
updateSignedPsbt: async function (psbtBase64) {
|
||||||
|
try {
|
||||||
|
this.showChecking = true
|
||||||
|
this.psbtBase64Signed = psbtBase64
|
||||||
|
|
||||||
|
console.log('### payment updateSignedPsbt psbtBase64', psbtBase64)
|
||||||
|
|
||||||
|
const data = await this.extractTxFromPsbt(psbtBase64)
|
||||||
|
this.showFinalTx = true
|
||||||
|
if (data) {
|
||||||
|
this.signedTx = JSON.parse(data.tx_json)
|
||||||
|
this.signedTxHex = data.tx_hex
|
||||||
|
} else {
|
||||||
|
this.signedTx = null
|
||||||
|
this.signedTxHex = null
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.showChecking = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractTxFromPsbt: async function (psbtBase64) {
|
||||||
|
console.log('### extractTxFromPsbt psbtBase64', psbtBase64)
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/watchonly/api/v1/psbt/extract',
|
||||||
|
this.adminkey,
|
||||||
|
{
|
||||||
|
psbtBase64,
|
||||||
|
inputs: this.tx.inputs
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console.log('### extractTxFromPsbt data', data)
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.log('### error', error)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot finalize PSBT!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcastTransaction: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/watchonly/api/v1/tx',
|
||||||
|
this.adminkey,
|
||||||
|
{tx_hex: this.signedTxHex}
|
||||||
|
)
|
||||||
|
this.sentTxId = data
|
||||||
|
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Transaction broadcasted!',
|
||||||
|
caption: `${data}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// todo: event rescan with amount
|
||||||
|
// todo: display tx id
|
||||||
|
} catch (error) {
|
||||||
|
this.sentTxId = null
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to broadcast!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.showFinalTx = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchTxHex: async function (txId) {
|
||||||
|
const {
|
||||||
|
bitcoin: {transactions: transactionsAPI}
|
||||||
|
} = mempoolJS({
|
||||||
|
hostname: this.mempoolEndpoint
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await transactionsAPI.getTxHex({txid: txId})
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: `Failed to fetch transaction details for tx id: '${txId}'`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleOutputsChange: function () {
|
||||||
|
this.$refs.utxoList.refreshUtxoSelection(this.totalPayedAmount)
|
||||||
|
},
|
||||||
|
getTotalPaymentAmount: function () {
|
||||||
|
return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
hide-header
|
||||||
|
:data="data"
|
||||||
|
:columns="paymentTable.columns"
|
||||||
|
:pagination.sync="paymentTable.pagination"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<div class="row no-wrap">
|
||||||
|
<div class="col-1">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="l"
|
||||||
|
@click="deletePaymentAddress(props.row)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
class="q-mt-sm"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-7 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="props.row.address"
|
||||||
|
type="text"
|
||||||
|
label="Address"
|
||||||
|
:rules="[val => !!val || 'Field is required']"
|
||||||
|
@input="handleOutputsChange"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="props.row.amount"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
label="Amount (sats)"
|
||||||
|
:rules="[val => !!val || 'Field is required', val => +val > DUST_LIMIT || 'Amount to small (below dust limit)']"
|
||||||
|
@input="handleOutputsChange"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-1">
|
||||||
|
<q-btn outline color="grey" @click="sendMaxToAddress(props.row)"
|
||||||
|
>Max</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
@click="addPaymentAddress"
|
||||||
|
class="btn-full"
|
||||||
|
>Add</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<div class="float-right">
|
||||||
|
<span>Payed Amount: </span>
|
||||||
|
<span class="text-subtitle2 q-ml-lg">
|
||||||
|
{{satBtc(getTotalPaymentAmount())}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,81 @@
|
||||||
|
async function sendTo(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('send-to', {
|
||||||
|
name: 'send-to',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'data',
|
||||||
|
'tx-size',
|
||||||
|
'selected-amount',
|
||||||
|
'fee-rate',
|
||||||
|
'sats-denominated'
|
||||||
|
],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dataLocal: {
|
||||||
|
get: function () {
|
||||||
|
return this.data
|
||||||
|
},
|
||||||
|
set: function (value) {
|
||||||
|
console.log('### computed update data', value)
|
||||||
|
this.$emit('update:data', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
DUST_LIMIT: 546,
|
||||||
|
paymentTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
align: 'left'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
},
|
||||||
|
filter: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
addPaymentAddress: function () {
|
||||||
|
this.dataLocal.push({address: '', amount: undefined})
|
||||||
|
this.handleOutputsChange()
|
||||||
|
},
|
||||||
|
deletePaymentAddress: function (v) {
|
||||||
|
const index = this.dataLocal.indexOf(v)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.dataLocal.splice(index, 1)
|
||||||
|
}
|
||||||
|
this.handleOutputsChange()
|
||||||
|
},
|
||||||
|
|
||||||
|
sendMaxToAddress: function (paymentAddress = {}) {
|
||||||
|
const feeValue = this.feeRate * this.txSize
|
||||||
|
const inputAmount = this.selectedAmount
|
||||||
|
const currentAmount = Math.max(0, paymentAddress.amount || 0)
|
||||||
|
const payedAmount = this.getTotalPaymentAmount() - currentAmount
|
||||||
|
paymentAddress.amount = Math.max(
|
||||||
|
0,
|
||||||
|
inputAmount - payedAmount - feeValue
|
||||||
|
)
|
||||||
|
},
|
||||||
|
handleOutputsChange: function () {
|
||||||
|
this.$emit('update:outputs')
|
||||||
|
},
|
||||||
|
getTotalPaymentAmount: function () {
|
||||||
|
return this.dataLocal.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
<div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.baudRate"
|
||||||
|
type="number"
|
||||||
|
label="Baud Rate"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.bufferSize"
|
||||||
|
type="number"
|
||||||
|
label="Buffer Size"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.flowControl"
|
||||||
|
label="Flow Control"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.parity"
|
||||||
|
label="Parity"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.dataBits"
|
||||||
|
type="number"
|
||||||
|
label="Data Bits"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.stopBits"
|
||||||
|
type="number"
|
||||||
|
label="Stop Bits"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,24 @@
|
||||||
|
async function serialPortConfig(path) {
|
||||||
|
const t = await loadTemplateAsync(path)
|
||||||
|
Vue.component('serial-port-config', {
|
||||||
|
name: 'serial-port-config',
|
||||||
|
template: t,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
baudRate: 9600,
|
||||||
|
bufferSize: 255,
|
||||||
|
dataBits: 8,
|
||||||
|
flowControl: 'none',
|
||||||
|
parity: 'none',
|
||||||
|
stopBits: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getConfig: function () {
|
||||||
|
return this.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,451 @@
|
||||||
|
<div>
|
||||||
|
<q-btn-dropdown
|
||||||
|
split
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
icon="usb"
|
||||||
|
:text-color="selectedPort ? hww.authenticated ? 'green' : 'orange' : 'white'"
|
||||||
|
@click="openSerialPortDialog"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item
|
||||||
|
v-if="selectedPort && !hww.authenticated"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="hwwShowPasswordDialog()"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Login</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Enter password for Hardware Wallet.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
v-if="hww.authenticated"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="hwwLogout()"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Logout</q-item-label>
|
||||||
|
<q-item-label caption>Clear password for HWW.</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="!selectedPort"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="openSerialPortConfig"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Config & Connect</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Set the Serial Port communication parameters.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="selectedPort"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="closeSerialPort()"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Disconnect</q-item-label>
|
||||||
|
<q-item-label caption>Disconnect from Serial Port.</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
v-if="selectedPort"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="hwwShowRestoreDialog()"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Restore</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Restore wallet from existing word list.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="hww.authenticated"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="hwwShowSeed()"
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Show Seed</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Show seed on the Hardware Wallet display.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="selectedPort"
|
||||||
|
@click="hwwShowWipeDialog()"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Wipe</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Clean-up the wallet. New random seed.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="selectedPort" @click="hwwHelp()" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Help</q-item-label>
|
||||||
|
<q-item-label caption>View available comands.</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="selectedPort"
|
||||||
|
@click="showConsole = true"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Console</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Show the serial port communication messages</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showConfigDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="hwwConfigAndConnect" class="q-gutter-md">
|
||||||
|
<span>Enter Config</span>
|
||||||
|
|
||||||
|
<serial-port-config
|
||||||
|
ref="serialPortConfig"
|
||||||
|
:config="hww.config"
|
||||||
|
></serial-port-config>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn unelevated color="primary" type="submit">Connect</q-btn>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showPasswordDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="hwwLogin" class="q-gutter-md">
|
||||||
|
<span>Enter password for Hardware Wallet (8 numbers/letters)</span>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="hww.password"
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!selectedPort"
|
||||||
|
type="submit"
|
||||||
|
>Login</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showConfirmationDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="hwwSignPsbt" class="q-gutter-md">
|
||||||
|
<div v-if="tx">
|
||||||
|
<div v-if="!hww.confirm.showFee" class="row q-mt-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<span class="text-subtitle2"
|
||||||
|
>Output {{hww.confirm.outputIndex}}</span
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="tx.outputs[hww.confirm.outputIndex].branch_index === 1"
|
||||||
|
color="orange"
|
||||||
|
text-color="black"
|
||||||
|
>
|
||||||
|
<span>change</span>
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hww.confirm.showFee" class="row q-mt-lg">
|
||||||
|
<div class="col-3">
|
||||||
|
<span>Address:</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<span>{{tx.outputs[hww.confirm.outputIndex].address}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hww.confirm.showFee" class="row q-mt-lg">
|
||||||
|
<div class="col-3">
|
||||||
|
<span>Amount:</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<span
|
||||||
|
>{{satBtc(tx.outputs[hww.confirm.outputIndex].amount)}}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hww.confirm.showFee" class="row q-mt-lg">
|
||||||
|
<div class="col-3">
|
||||||
|
<span>Fee: </span>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<span>{{satBtc(tx.feeValue)}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hww.confirm.showFee" class="row q-mt-lg">
|
||||||
|
<div class="col-3">
|
||||||
|
<span>Fee Rate:</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<span>{{tx.feeRate}} sats/vbyte</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-badge class="text-subtitle2" color="yellow" text-color="black">
|
||||||
|
<span>Check data on the display of the hardware device.</span>
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
v-if="hww.confirm.showFee"
|
||||||
|
unelevated
|
||||||
|
color="green"
|
||||||
|
:disable="!selectedPort"
|
||||||
|
type="submit"
|
||||||
|
class="float-left"
|
||||||
|
label="Confirm"
|
||||||
|
>
|
||||||
|
<q-spinner v-if="hww.signingPsbt" color="primary"></q-spinner>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
label="Next"
|
||||||
|
class="float-left"
|
||||||
|
v-if="!hww.confirm.showFee"
|
||||||
|
@click="hwwConfirmNext"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<q-btn
|
||||||
|
@click="cancelOperation"
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="float-right"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showWipeDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="hwwWipe" class="q-gutter-md">
|
||||||
|
<q-badge color="pink" text-color="black">
|
||||||
|
This action will remove all data from the Hardware Wallet. Please
|
||||||
|
create a back-up for the seed!
|
||||||
|
</q-badge>
|
||||||
|
<span>Enter new password for Hardware Wallet (8 numbers/letters)</span>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="hww.password"
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="hww.confirmedPassword"
|
||||||
|
type="password"
|
||||||
|
label="Confirm Password"
|
||||||
|
></q-input>
|
||||||
|
<q-badge color="pink" text-color="black">
|
||||||
|
This action cannot be reversed!
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!hww.password || hww.password.length < 8 || (hww.password !== hww.confirmedPassword)"
|
||||||
|
type="submit"
|
||||||
|
>Wipe</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="showConsole" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
for="serial-port-console"
|
||||||
|
v-model.trim="receivedData"
|
||||||
|
type="textarea"
|
||||||
|
rows="25"
|
||||||
|
cols="200"
|
||||||
|
label="Console"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showSeedDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl">
|
||||||
|
<span>Check word at position {{hww.seedWordPosition}} on display</span>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-btn
|
||||||
|
v-if="hww.seedWordPosition!== 1"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
@click="showPrevSeedWord"
|
||||||
|
>Prev</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-btn
|
||||||
|
v-if="hww.seedWordPosition!== 24"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
@click="showNextSeedWord"
|
||||||
|
>Next</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="hww.showRestoreDialog" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="hwwRestore" class="q-gutter-md">
|
||||||
|
<q-badge
|
||||||
|
color="pink"
|
||||||
|
text-color="black"
|
||||||
|
class="text-subtitle2"
|
||||||
|
multi-line
|
||||||
|
>
|
||||||
|
For test purposes only. Do not enter word list with real funds!!!
|
||||||
|
</q-badge>
|
||||||
|
<br /><br /><br />
|
||||||
|
<span>Enter new word list separated by space</span>
|
||||||
|
<q-input
|
||||||
|
v-model.trim="hww.mnemonic"
|
||||||
|
filled
|
||||||
|
:type="hww.showMnemonic ? 'text' : 'password'"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Word List"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
:name="hww.showMnemonic ? 'visibility' : 'visibility_off'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="hww.showMnemonic = !hww.showMnemonic"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<br />
|
||||||
|
<span>Enter new password (8 numbers/letters)</span>
|
||||||
|
<q-input
|
||||||
|
v-model.trim="hww.password"
|
||||||
|
filled
|
||||||
|
:type="hww.showPassword ? 'text' : 'password'"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="New Password"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
:name="hww.showPassword ? 'visibility' : 'visibility_off'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="hww.showPassword = !hww.showPassword"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="hww.confirmedPassword"
|
||||||
|
type="password"
|
||||||
|
label="Confirm Password"
|
||||||
|
></q-input>
|
||||||
|
<br /><br />
|
||||||
|
<q-badge
|
||||||
|
color="pink"
|
||||||
|
text-color="black"
|
||||||
|
class="text-subtitle2"
|
||||||
|
multi-line
|
||||||
|
>
|
||||||
|
For test purposes only. Do not enter word list with real funds!!!
|
||||||
|
</q-badge>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-badge
|
||||||
|
color="pink"
|
||||||
|
text-color="black"
|
||||||
|
class="text-subtitle2"
|
||||||
|
multi-line
|
||||||
|
>
|
||||||
|
ALL existing data on the Hardware Device will be lost.
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!hww.mnemonic || !hww.password || hww.password.length < 8 || (hww.password !== hww.confirmedPassword)"
|
||||||
|
type="submit"
|
||||||
|
>Restore</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
|
@ -0,0 +1,601 @@
|
||||||
|
async function serialSigner(path) {
|
||||||
|
const t = await loadTemplateAsync(path)
|
||||||
|
Vue.component('serial-signer', {
|
||||||
|
name: 'serial-signer',
|
||||||
|
template: t,
|
||||||
|
|
||||||
|
props: ['sats-denominated', 'network'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
selectedPort: null,
|
||||||
|
writableStreamClosed: null,
|
||||||
|
writer: null,
|
||||||
|
readableStreamClosed: null,
|
||||||
|
reader: null,
|
||||||
|
receivedData: '',
|
||||||
|
config: {},
|
||||||
|
|
||||||
|
hww: {
|
||||||
|
password: null,
|
||||||
|
showPassword: false,
|
||||||
|
mnemonic: null,
|
||||||
|
showMnemonic: false,
|
||||||
|
authenticated: false,
|
||||||
|
showPasswordDialog: false,
|
||||||
|
showConfigDialog: false,
|
||||||
|
showWipeDialog: false,
|
||||||
|
showRestoreDialog: false,
|
||||||
|
showConfirmationDialog: false,
|
||||||
|
showSignedPsbt: false,
|
||||||
|
sendingPsbt: false,
|
||||||
|
signingPsbt: false,
|
||||||
|
loginResolve: null,
|
||||||
|
psbtSentResolve: null,
|
||||||
|
xpubResolve: null,
|
||||||
|
seedWordPosition: 1,
|
||||||
|
showSeedDialog: false,
|
||||||
|
confirm: {
|
||||||
|
outputIndex: 0,
|
||||||
|
showFee: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx: null, // todo: move to hww
|
||||||
|
|
||||||
|
showConsole: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
openSerialPortDialog: async function () {
|
||||||
|
await this.openSerialPort()
|
||||||
|
},
|
||||||
|
openSerialPort: async function (config = {baudRate: 9600}) {
|
||||||
|
if (!this.checkSerialPortSupported()) return false
|
||||||
|
if (this.selectedPort) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Already connected. Disconnect first!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigator.serial.addEventListener('connect', event => {
|
||||||
|
console.log('### navigator.serial event: connected!', event)
|
||||||
|
})
|
||||||
|
|
||||||
|
navigator.serial.addEventListener('disconnect', () => {
|
||||||
|
console.log('### navigator.serial event: disconnected!', event)
|
||||||
|
this.hww.authenticated = false
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Disconnected from Serial Port!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.selectedPort = await navigator.serial.requestPort()
|
||||||
|
// Wait for the serial port to open.
|
||||||
|
await this.selectedPort.open(config)
|
||||||
|
this.startSerialPortReading()
|
||||||
|
|
||||||
|
const textEncoder = new TextEncoderStream()
|
||||||
|
this.writableStreamClosed = textEncoder.readable.pipeTo(
|
||||||
|
this.selectedPort.writable
|
||||||
|
)
|
||||||
|
|
||||||
|
this.writer = textEncoder.writable.getWriter()
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
this.selectedPort = null
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot open serial port!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSerialPortConfig: async function () {
|
||||||
|
this.hww.showConfigDialog = true
|
||||||
|
},
|
||||||
|
closeSerialPort: async function () {
|
||||||
|
try {
|
||||||
|
if (this.writer) this.writer.close()
|
||||||
|
if (this.writableStreamClosed) await this.writableStreamClosed
|
||||||
|
if (this.reader) this.reader.cancel()
|
||||||
|
if (this.readableStreamClosed)
|
||||||
|
await this.readableStreamClosed.catch(() => {
|
||||||
|
/* Ignore the error */
|
||||||
|
})
|
||||||
|
if (this.selectedPort) await this.selectedPort.close()
|
||||||
|
this.selectedPort = null
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Serial port disconnected!',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.selectedPort = null
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot close serial port!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.hww.authenticated = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isConnected: function () {
|
||||||
|
return !!this.selectedPort
|
||||||
|
},
|
||||||
|
isAuthenticated: function () {
|
||||||
|
return this.hww.authenticated
|
||||||
|
},
|
||||||
|
isAuthenticating: function () {
|
||||||
|
if (this.isAuthenticated()) return false
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.loginResolve = resolve
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
isSendingPsbt: async function () {
|
||||||
|
if (!this.hww.sendingPsbt) return false
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.psbtSentResolve = resolve
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
isFetchingXpub: async function () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.xpubResolve = resolve
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
checkSerialPortSupported: function () {
|
||||||
|
if (!navigator.serial) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Serial port communication not supported!',
|
||||||
|
caption:
|
||||||
|
'Make sure your browser supports Serial Port and that you are using HTTPS.',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
startSerialPortReading: async function () {
|
||||||
|
const port = this.selectedPort
|
||||||
|
|
||||||
|
while (port && port.readable) {
|
||||||
|
const textDecoder = new TextDecoderStream()
|
||||||
|
this.readableStreamClosed = port.readable.pipeTo(textDecoder.writable)
|
||||||
|
this.reader = textDecoder.readable.getReader()
|
||||||
|
const readStringUntil = readFromSerialPort(this.reader)
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const {value, done} = await readStringUntil('\n')
|
||||||
|
if (value) {
|
||||||
|
this.handleSerialPortResponse(value)
|
||||||
|
this.updateSerialPortConsole(value)
|
||||||
|
}
|
||||||
|
if (done) return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Serial port communication error!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSerialPortResponse: function (value) {
|
||||||
|
const command = value.split(' ')[0]
|
||||||
|
const commandData = value.substring(command.length).trim()
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case COMMAND_SIGN_PSBT:
|
||||||
|
this.handleSignResponse(commandData)
|
||||||
|
break
|
||||||
|
case COMMAND_PASSWORD:
|
||||||
|
this.handleLoginResponse(commandData)
|
||||||
|
break
|
||||||
|
case COMMAND_PASSWORD_CLEAR:
|
||||||
|
this.handleLogoutResponse(commandData)
|
||||||
|
break
|
||||||
|
case COMMAND_SEND_PSBT:
|
||||||
|
this.handleSendPsbtResponse(commandData)
|
||||||
|
break
|
||||||
|
case COMMAND_WIPE:
|
||||||
|
this.handleWipeResponse(commandData)
|
||||||
|
break
|
||||||
|
case COMMAND_XPUB:
|
||||||
|
this.handleXpubResponse(commandData)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('### console', value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateSerialPortConsole: function (value) {
|
||||||
|
this.receivedData += value + '\n'
|
||||||
|
const textArea = document.getElementById('serial-port-console')
|
||||||
|
if (textArea) textArea.scrollTop = textArea.scrollHeight
|
||||||
|
},
|
||||||
|
hwwShowPasswordDialog: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showPasswordDialog = true
|
||||||
|
await this.writer.write(COMMAND_PASSWORD + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to connect to Hardware Wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwShowWipeDialog: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showWipeDialog = true
|
||||||
|
await this.writer.write(COMMAND_WIPE + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to connect to Hardware Wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwShowRestoreDialog: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showRestoreDialog = true
|
||||||
|
await this.writer.write(COMMAND_WIPE + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to connect to Hardware Wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwConfirmNext: async function () {
|
||||||
|
this.hww.confirm.outputIndex += 1
|
||||||
|
if (this.hww.confirm.outputIndex >= this.tx.outputs.length) {
|
||||||
|
this.hww.confirm.showFee = true
|
||||||
|
}
|
||||||
|
await this.writer.write(COMMAND_CONFIRM_NEXT + '\n')
|
||||||
|
},
|
||||||
|
cancelOperation: async function () {
|
||||||
|
try {
|
||||||
|
await this.writer.write(COMMAND_CANCEL + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to send cancel!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwConfigAndConnect: async function () {
|
||||||
|
this.hww.showConfigDialog = false
|
||||||
|
const config = this.$refs.serialPortConfig.getConfig()
|
||||||
|
await this.openSerialPort(config)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
hwwLogin: async function () {
|
||||||
|
try {
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to send password to Hardware Wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.hww.showPasswordDialog = false
|
||||||
|
this.hww.password = null
|
||||||
|
this.hww.showPassword = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLoginResponse: function (res = '') {
|
||||||
|
this.hww.authenticated = res.trim() === '1'
|
||||||
|
if (this.loginResolve) {
|
||||||
|
this.loginResolve(this.hww.authenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hww.authenticated) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Login successfull!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Wrong password, try again!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwLogout: async function () {
|
||||||
|
try {
|
||||||
|
await this.writer.write(COMMAND_PASSWORD_CLEAR + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to logout from Hardware Wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLogoutResponse: function (res = '') {
|
||||||
|
this.hww.authenticated = !(res.trim() === '1')
|
||||||
|
if (this.hww.authenticated) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to logout from Hardware Wallet',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwSendPsbt: async function (psbtBase64, tx) {
|
||||||
|
try {
|
||||||
|
this.tx = tx
|
||||||
|
this.hww.sendingPsbt = true
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_SEND_PSBT + ' ' + this.network + ' ' + psbtBase64 + '\n'
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Data sent to serial port device!',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.hww.sendingPsbt = false
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to send data to serial port!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSendPsbtResponse: function (res = '') {
|
||||||
|
try {
|
||||||
|
const psbtOK = res.trim() === '1'
|
||||||
|
if (!psbtOK) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to send PSBT!',
|
||||||
|
caption: `${res}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.hww.confirm.outputIndex = 0
|
||||||
|
this.hww.showConfirmationDialog = true
|
||||||
|
this.hww.confirm = {
|
||||||
|
outputIndex: 0,
|
||||||
|
showFee: false
|
||||||
|
}
|
||||||
|
this.hww.sendingPsbt = false
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to send PSBT!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.psbtSentResolve()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwSignPsbt: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showConfirmationDialog = false
|
||||||
|
this.hww.signingPsbt = true
|
||||||
|
await this.writer.write(COMMAND_SIGN_PSBT + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to sign PSBT!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSignResponse: function (res = '') {
|
||||||
|
this.hww.signingPsbt = false
|
||||||
|
const [count, psbt] = res.trim().split(' ')
|
||||||
|
if (!psbt || !count || count.trim() === '0') {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'No input signed!',
|
||||||
|
caption: 'Are you using the right seed?',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateSignedPsbt(psbt)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Transaction Signed',
|
||||||
|
message: `Inputs signed: ${count}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hwwHelp: async function () {
|
||||||
|
try {
|
||||||
|
await this.writer.write(COMMAND_HELP + '\n')
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Check display or console for details!',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to ask for help!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwWipe: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showWipeDialog = false
|
||||||
|
await this.writer.write(COMMAND_WIPE + ' ' + this.hww.password + '\n')
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to ask for help!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.hww.password = null
|
||||||
|
this.hww.confirmedPassword = null
|
||||||
|
this.hww.showPassword = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleWipeResponse: function (res = '') {
|
||||||
|
const wiped = res.trim() === '1'
|
||||||
|
if (wiped) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Wallet wiped!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to wipe wallet!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwXpub: async function (path) {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
'### hwwXpub',
|
||||||
|
COMMAND_XPUB + ' ' + this.network + ' ' + path
|
||||||
|
)
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_XPUB + ' ' + this.network + ' ' + path + '\n'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to fetch XPub!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleXpubResponse: function (res = '') {
|
||||||
|
const args = res.trim().split(' ')
|
||||||
|
if (args.length < 3 || args[0].trim() !== '1') {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to fetch XPub!',
|
||||||
|
caption: `${res}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
this.xpubResolve({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const xpub = args[1].trim()
|
||||||
|
const fingerprint = args[2].trim()
|
||||||
|
this.xpubResolve({xpub, fingerprint})
|
||||||
|
},
|
||||||
|
hwwShowSeed: async function () {
|
||||||
|
try {
|
||||||
|
this.hww.showSeedDialog = true
|
||||||
|
this.hww.seedWordPosition = 1
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to show seed!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showNextSeedWord: async function () {
|
||||||
|
this.hww.seedWordPosition++
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showPrevSeedWord: async function () {
|
||||||
|
this.hww.seedWordPosition = Math.max(1, this.hww.seedWordPosition - 1)
|
||||||
|
console.log('### this.hww.seedWordPosition', this.hww.seedWordPosition)
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
handleShowSeedResponse: function (res = '') {
|
||||||
|
const args = res.trim().split(' ')
|
||||||
|
if (args.length < 2 || args[0].trim() !== '1') {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to show seed!',
|
||||||
|
caption: `${res}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hwwRestore: async function () {
|
||||||
|
try {
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_RESTORE + ' ' + this.hww.mnemonic + '\n'
|
||||||
|
)
|
||||||
|
await this.writer.write(
|
||||||
|
COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to restore from seed!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.hww.showRestoreDialog = false
|
||||||
|
this.hww.mnemonic = null
|
||||||
|
this.hww.showMnemonic = false
|
||||||
|
this.hww.password = null
|
||||||
|
this.hww.confirmedPassword = null
|
||||||
|
this.hww.showPassword = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSignedPsbt: async function (value) {
|
||||||
|
this.$emit('signed:psbt', value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div v-if="selectable" class="col-3 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="utxoSelectionMode"
|
||||||
|
:options="utxoSelectionModes"
|
||||||
|
label="Selection Mode"
|
||||||
|
@input="updateUtxoSelection"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectable" class="col-1 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
icon="refresh"
|
||||||
|
color="grey"
|
||||||
|
@click="updateUtxoSelection"
|
||||||
|
class="q-ml-sm"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectable" class="col-5 q-pr-lg"></div>
|
||||||
|
<div v-if="!selectable" class="col-9 q-pr-lg"></div>
|
||||||
|
<div class="col-3 float-right">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="utxos"
|
||||||
|
row-key="id"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination.sync="utxosTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td v-if="selectable" key="selected" :props="props">
|
||||||
|
<div>
|
||||||
|
<q-checkbox v-model="props.row.selected"></q-checkbox>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="status" :props="props">
|
||||||
|
<div>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.confirmed"
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
color="green"
|
||||||
|
class="q-mr-md cursor-pointer"
|
||||||
|
>
|
||||||
|
Confirmed
|
||||||
|
</q-badge>
|
||||||
|
<q-badge
|
||||||
|
v-if="!props.row.confirmed"
|
||||||
|
@click="props.row.expanded = !props.row.expanded"
|
||||||
|
color="orange"
|
||||||
|
class="q-mr-md cursor-pointer"
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="address" :props="props">
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
style="color: unset"
|
||||||
|
:href="'https://' + mempoolEndpoint + '/address/' + props.row.address"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{props.row.address}}</a
|
||||||
|
>
|
||||||
|
<q-badge v-if="props.row.isChange" color="orange" class="q-mr-md">
|
||||||
|
change
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td
|
||||||
|
key="amount"
|
||||||
|
:props="props"
|
||||||
|
class="text-green-13 text-weight-bold"
|
||||||
|
>
|
||||||
|
<div>{{satBtc(props.row.amount)}}</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="date" :props="props"> {{ props.row.date }} </q-td>
|
||||||
|
<q-td key="wallet" :props="props" :class="">
|
||||||
|
<div>{{getWalletName(props.row.wallet)}}</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-show="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Transaction Id</div>
|
||||||
|
<div class="col-10 q-pr-lg">
|
||||||
|
<a
|
||||||
|
style="color: unset"
|
||||||
|
:href="'https://' + mempoolEndpoint + '/tx/' + props.row.txId"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{props.row.txId}}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section></q-card
|
||||||
|
>
|
|
@ -0,0 +1,148 @@
|
||||||
|
async function utxoList(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('utxo-list', {
|
||||||
|
name: 'utxo-list',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'utxos',
|
||||||
|
'accounts',
|
||||||
|
'selectable',
|
||||||
|
'payed-amount',
|
||||||
|
'sats-denominated',
|
||||||
|
'mempool-endpoint',
|
||||||
|
'filter'
|
||||||
|
],
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
utxosTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'expand',
|
||||||
|
align: 'left',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'selected',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
selectable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'Status',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Address',
|
||||||
|
field: 'address',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Amount',
|
||||||
|
field: 'amount',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Date',
|
||||||
|
field: 'date',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wallet',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Account',
|
||||||
|
field: 'wallet',
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
utxoSelectionModes: [
|
||||||
|
'Manual',
|
||||||
|
'Random',
|
||||||
|
'Select All',
|
||||||
|
'Smaller Inputs First',
|
||||||
|
'Larger Inputs First'
|
||||||
|
],
|
||||||
|
utxoSelectionMode: 'Random',
|
||||||
|
utxoSelectAmount: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
columns: function () {
|
||||||
|
return this.utxosTable.columns.filter(c =>
|
||||||
|
c.selectable ? this.selectable : true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
getWalletName: function (walletId) {
|
||||||
|
const wallet = (this.accounts || []).find(wl => wl.id === walletId)
|
||||||
|
return wallet ? wallet.title : 'unknown'
|
||||||
|
},
|
||||||
|
getTotalSelectedUtxoAmount: function () {
|
||||||
|
const total = (this.utxos || [])
|
||||||
|
.filter(u => u.selected)
|
||||||
|
.reduce((t, a) => t + (a.amount || 0), 0)
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
refreshUtxoSelection: function (totalPayedAmount) {
|
||||||
|
this.utxoSelectAmount = totalPayedAmount
|
||||||
|
this.applyUtxoSelectionMode()
|
||||||
|
},
|
||||||
|
updateUtxoSelection: function () {
|
||||||
|
this.utxoSelectAmount = this.payedAmount
|
||||||
|
this.applyUtxoSelectionMode()
|
||||||
|
},
|
||||||
|
applyUtxoSelectionMode: function () {
|
||||||
|
const mode = this.utxoSelectionMode
|
||||||
|
const isSelectAll = mode === 'Select All'
|
||||||
|
if (isSelectAll) {
|
||||||
|
this.utxos.forEach(u => (u.selected = true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isManual = mode === 'Manual'
|
||||||
|
if (isManual || !this.utxoSelectAmount) return
|
||||||
|
|
||||||
|
this.utxos.forEach(u => (u.selected = false))
|
||||||
|
|
||||||
|
const isSmallerFirst = mode === 'Smaller Inputs First'
|
||||||
|
const isLargerFirst = mode === 'Larger Inputs First'
|
||||||
|
let selectedUtxos = this.utxos.slice()
|
||||||
|
if (isSmallerFirst || isLargerFirst) {
|
||||||
|
const sortFn = isSmallerFirst
|
||||||
|
? (a, b) => a.amount - b.amount
|
||||||
|
: (a, b) => b.amount - a.amount
|
||||||
|
selectedUtxos.sort(sortFn)
|
||||||
|
} else {
|
||||||
|
// default to random order
|
||||||
|
selectedUtxos = _.shuffle(selectedUtxos)
|
||||||
|
}
|
||||||
|
selectedUtxos.reduce((total, utxo) => {
|
||||||
|
utxo.selected = total < this.utxoSelectAmount
|
||||||
|
total += utxo.amount
|
||||||
|
return total
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
<div>
|
||||||
|
<q-card>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-ml-lg">
|
||||||
|
<q-btn unelevated @click="show = true" color="primary" icon="settings">
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="row justify-center q-gutter-x-md items-center">
|
||||||
|
<div class="text-h3">{{satBtc(total)}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 float-right">
|
||||||
|
<slot name="serial"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-dialog v-model="show" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="updateConfig" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="config.mempool_endpoint"
|
||||||
|
type="text"
|
||||||
|
label="Mempool Endpoint"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="config.receive_gap_limit"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
label="Receive Gap Limit"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="config.change_gap_limit"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
label="Change Gap Limit"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="config.network"
|
||||||
|
:options="networOptions"
|
||||||
|
label="Network"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
:label="config.sats_denominated ? 'sats denominated' : 'BTC denominated'"
|
||||||
|
color="secodary"
|
||||||
|
v-model="config.sats_denominated"
|
||||||
|
></q-toggle>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="
|
||||||
|
!config.mempool_endpoint "
|
||||||
|
type="submit"
|
||||||
|
>Update</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
|
@ -0,0 +1,67 @@
|
||||||
|
async function walletConfig(path) {
|
||||||
|
const t = await loadTemplateAsync(path)
|
||||||
|
Vue.component('wallet-config', {
|
||||||
|
name: 'wallet-config',
|
||||||
|
template: t,
|
||||||
|
|
||||||
|
props: ['total', 'config-data', 'adminkey'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
networOptions: ['Mainnet', 'Testnet'],
|
||||||
|
internalConfig: {},
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
config: {
|
||||||
|
get() {
|
||||||
|
return this.internalConfig
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
value.isLoaded = true
|
||||||
|
this.internalConfig = JSON.parse(JSON.stringify(value))
|
||||||
|
this.$emit(
|
||||||
|
'update:config-data',
|
||||||
|
JSON.parse(JSON.stringify(this.internalConfig))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.config.sats_denominated)
|
||||||
|
},
|
||||||
|
updateConfig: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/watchonly/api/v1/config',
|
||||||
|
this.adminkey,
|
||||||
|
this.config
|
||||||
|
)
|
||||||
|
this.show = false
|
||||||
|
this.config = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getConfig: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/watchonly/api/v1/config',
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.config = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getConfig()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
<div>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn-dropdown
|
||||||
|
split
|
||||||
|
unelevated
|
||||||
|
label="Add Wallet Account"
|
||||||
|
color="primary"
|
||||||
|
@click="showAddAccountDialog"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item @click="showAddAccountDialog" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>New Account</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Enter account Xpub or Descriptor</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item @click="getXpubFromDevice" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>From Hardware Device</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Get Xpub from a Hardware Device</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto q-pr-lg"></div>
|
||||||
|
<div class="col-auto q-pl-lg">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="walletAccounts"
|
||||||
|
row-key="id"
|
||||||
|
:columns="walletsTable.columns"
|
||||||
|
:pagination.sync="walletsTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="new">
|
||||||
|
<q-badge
|
||||||
|
size="lg"
|
||||||
|
color="secondary"
|
||||||
|
class="q-mr-md cursor-pointer"
|
||||||
|
@click="openGetFreshAddressDialog(props.row.id)"
|
||||||
|
>
|
||||||
|
New Receive Address
|
||||||
|
</q-badge>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="title" :props="props" :class="">
|
||||||
|
<div>{{props.row.title}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="amount" :props="props" :class="">
|
||||||
|
<div>{{getAmmountForWallet(props.row.id)}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="type" :props="props" :class="">
|
||||||
|
<div>{{props.row.type}}</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="id" :props="props" :class="">
|
||||||
|
<div>{{props.row.id}}</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-show="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
@click="openGetFreshAddressDialog(props.row.id)"
|
||||||
|
>New Receive Address</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
{{getAccountDescription(props.row.type)}}
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Master Pubkey:</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
v-model="props.row.masterpub"
|
||||||
|
filled
|
||||||
|
readonly
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Last Address Index:</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<span v-if="props.row.address_no >= 0"
|
||||||
|
>{{props.row.address_no}}</span
|
||||||
|
>
|
||||||
|
<span v-if="props.row.address_no < 0">none</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-2 q-pr-lg">Fingerprint:</div>
|
||||||
|
<div class="col-8">{{props.row.fingerprint}}</div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="pink"
|
||||||
|
icon="cancel"
|
||||||
|
@click="deleteWalletAccount(props.row.id)"
|
||||||
|
>Delete</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
<div class="col-2 q-pr-lg"></div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="addWalletAccount" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.title"
|
||||||
|
type="text"
|
||||||
|
label="Title"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="!formDialog.useSerialPort"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
v-model="formDialog.data.masterpub"
|
||||||
|
height="50px"
|
||||||
|
autogrow
|
||||||
|
label="Account Extended Public Key; xpub, ypub, zpub; Bitcoin Descriptor"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.useSerialPort"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.addressType"
|
||||||
|
:options="addressTypeOptions"
|
||||||
|
label="Address Type"
|
||||||
|
@input="handleAddressTypeChanged"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-if="formDialog.useSerialPort"
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="accountPath"
|
||||||
|
height="50px"
|
||||||
|
autogrow
|
||||||
|
label="Account Path"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
label="Add Watch-Only Account"
|
||||||
|
:disable="
|
||||||
|
(formDialog.data.masterpub == null && accountPath == null)||
|
||||||
|
formDialog.data.title == null || showCreating"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-spinner v-if="showCreating" color="primary" size="2em"></q-spinner>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
|
@ -0,0 +1,290 @@
|
||||||
|
async function walletList(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('wallet-list', {
|
||||||
|
name: 'wallet-list',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'adminkey',
|
||||||
|
'inkey',
|
||||||
|
'sats-denominated',
|
||||||
|
'addresses',
|
||||||
|
'network',
|
||||||
|
'serial-signer-ref'
|
||||||
|
],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
walletAccounts: [],
|
||||||
|
address: {},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
|
||||||
|
addressType: {
|
||||||
|
label: 'Segwit (P2WPKH)',
|
||||||
|
id: 'wpkh',
|
||||||
|
pathMainnet: "m/84'/0'/0'",
|
||||||
|
pathTestnet: "m/84'/1'/0'"
|
||||||
|
},
|
||||||
|
useSerialPort: false,
|
||||||
|
data: {
|
||||||
|
title: '',
|
||||||
|
masterpub: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountPath: '',
|
||||||
|
filter: '',
|
||||||
|
showCreating: false,
|
||||||
|
addressTypeOptions: [
|
||||||
|
{
|
||||||
|
label: 'Legacy (P2PKH)',
|
||||||
|
id: 'pkh',
|
||||||
|
pathMainnet: "m/44'/0'/0'",
|
||||||
|
pathTestnet: "m/44'/1'/0'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Segwit (P2WPKH)',
|
||||||
|
id: 'wpkh',
|
||||||
|
pathMainnet: "m/84'/0'/0'",
|
||||||
|
pathTestnet: "m/84'/1'/0'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wrapped Segwit (P2SH-P2WPKH)',
|
||||||
|
id: 'sh',
|
||||||
|
pathMainnet: "m/49'/0'/0'",
|
||||||
|
pathTestnet: "m/49'/1'/0'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Taproot (P2TR)',
|
||||||
|
id: 'tr',
|
||||||
|
pathMainnet: "m/86'/0'/0'",
|
||||||
|
pathTestnet: "m/86'/1'/0'"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
walletsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'new',
|
||||||
|
align: 'left',
|
||||||
|
label: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Title',
|
||||||
|
field: 'title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Amount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Type',
|
||||||
|
field: 'type'
|
||||||
|
},
|
||||||
|
{name: 'id', align: 'left', label: 'ID', field: 'id'}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
},
|
||||||
|
filter: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
immediate: true,
|
||||||
|
async network(newNet, oldNet) {
|
||||||
|
if (newNet !== oldNet) {
|
||||||
|
await this.refreshWalletAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, this.satsDenominated)
|
||||||
|
},
|
||||||
|
|
||||||
|
addWalletAccount: async function () {
|
||||||
|
this.showCreating = true
|
||||||
|
const data = _.omit(this.formDialog.data, 'wallet')
|
||||||
|
data.network = this.network
|
||||||
|
await this.createWalletAccount(data)
|
||||||
|
this.showCreating = false
|
||||||
|
},
|
||||||
|
createWalletAccount: async function (data) {
|
||||||
|
try {
|
||||||
|
if (this.formDialog.useSerialPort) {
|
||||||
|
const {xpub, fingerprint} = await this.fetchXpubFromHww()
|
||||||
|
if (!xpub) return
|
||||||
|
const path = this.accountPath.substring(2)
|
||||||
|
const outputType = this.formDialog.addressType.id
|
||||||
|
if (outputType === 'sh') {
|
||||||
|
data.masterpub = `${outputType}(wpkh([${fingerprint}/${path}]${xpub}/{0,1}/*))`
|
||||||
|
} else {
|
||||||
|
data.masterpub = `${outputType}([${fingerprint}/${path}]${xpub}/{0,1}/*)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/watchonly/api/v1/wallet',
|
||||||
|
this.adminkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
this.walletAccounts.push(mapWalletAccount(response.data))
|
||||||
|
this.formDialog.show = false
|
||||||
|
|
||||||
|
await this.refreshWalletAccounts()
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchXpubFromHww: async function () {
|
||||||
|
const error = findAccountPathIssues(this.accountPath)
|
||||||
|
if (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Invalid derivation path.',
|
||||||
|
caption: error,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.serialSignerRef.hwwXpub(this.accountPath)
|
||||||
|
return await this.serialSignerRef.isFetchingXpub()
|
||||||
|
},
|
||||||
|
deleteWalletAccount: function (walletAccountId) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
'Are you sure you want to delete this watch only wallet?'
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/watchonly/api/v1/wallet/' + walletAccountId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.walletAccounts = _.reject(this.walletAccounts, function (
|
||||||
|
obj
|
||||||
|
) {
|
||||||
|
return obj.id === walletAccountId
|
||||||
|
})
|
||||||
|
await this.refreshWalletAccounts()
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message:
|
||||||
|
'Error while deleting wallet account. Please try again.',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getWatchOnlyWallets: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/watchonly/api/v1/wallet?network=${this.network}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to fetch wallets.',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
refreshWalletAccounts: async function () {
|
||||||
|
this.walletAccounts = []
|
||||||
|
const wallets = await this.getWatchOnlyWallets()
|
||||||
|
this.walletAccounts = wallets.map(w => mapWalletAccount(w))
|
||||||
|
this.$emit('accounts-update', this.walletAccounts)
|
||||||
|
},
|
||||||
|
getAmmountForWallet: function (walletId) {
|
||||||
|
const amount = this.addresses
|
||||||
|
.filter(a => a.wallet === walletId)
|
||||||
|
.reduce((t, a) => t + a.amount || 0, 0)
|
||||||
|
return this.satBtc(amount)
|
||||||
|
},
|
||||||
|
closeFormDialog: function () {
|
||||||
|
this.formDialog.data = {
|
||||||
|
is_unique: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAccountDescription: function (accountType) {
|
||||||
|
return getAccountDescription(accountType)
|
||||||
|
},
|
||||||
|
openGetFreshAddressDialog: async function (walletId) {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/watchonly/api/v1/address/${walletId}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
const addressData = mapAddressesData(data)
|
||||||
|
|
||||||
|
addressData.note = `Shared on ${currentDateTime()}`
|
||||||
|
const lastAcctiveAddress =
|
||||||
|
this.addresses
|
||||||
|
.filter(
|
||||||
|
a =>
|
||||||
|
a.wallet === addressData.wallet && !a.isChange && a.hasActivity
|
||||||
|
)
|
||||||
|
.pop() || {}
|
||||||
|
addressData.gapLimitExceeded =
|
||||||
|
!addressData.isChange &&
|
||||||
|
addressData.addressIndex >
|
||||||
|
lastAcctiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT
|
||||||
|
|
||||||
|
const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
|
||||||
|
wallet.address_no = addressData.addressIndex
|
||||||
|
this.$emit('new-receive-address', addressData)
|
||||||
|
},
|
||||||
|
showAddAccountDialog: function () {
|
||||||
|
this.formDialog.show = true
|
||||||
|
this.formDialog.useSerialPort = false
|
||||||
|
},
|
||||||
|
getXpubFromDevice: async function () {
|
||||||
|
try {
|
||||||
|
if (!this.serialSignerRef.isConnected()) {
|
||||||
|
const portOpen = await this.serialSignerRef.openSerialPort()
|
||||||
|
if (!portOpen) return
|
||||||
|
}
|
||||||
|
if (!this.serialSignerRef.isAuthenticated()) {
|
||||||
|
await this.serialSignerRef.hwwShowPasswordDialog()
|
||||||
|
const authenticated = await this.serialSignerRef.isAuthenticating()
|
||||||
|
if (!authenticated) return
|
||||||
|
}
|
||||||
|
this.formDialog.show = true
|
||||||
|
this.formDialog.useSerialPort = true
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot fetch Xpub!',
|
||||||
|
caption: `${error}`,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAddressTypeChanged: function (value = {}) {
|
||||||
|
const addressType =
|
||||||
|
this.addressTypeOptions.find(t => t.id === value.id) || {}
|
||||||
|
this.accountPath = addressType[`path${this.network}`]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
if (this.inkey) {
|
||||||
|
await this.refreshWalletAccounts()
|
||||||
|
this.handleAddressTypeChanged(this.addressTypeOptions[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -43,7 +43,7 @@ const mapUtxoToPsbtInput = utxo => ({
|
||||||
address: utxo.address,
|
address: utxo.address,
|
||||||
branch_index: utxo.isChange ? 1 : 0,
|
branch_index: utxo.isChange ? 1 : 0,
|
||||||
address_index: utxo.addressIndex,
|
address_index: utxo.addressIndex,
|
||||||
masterpub_fingerprint: utxo.masterpubFingerprint,
|
wallet: utxo.wallet,
|
||||||
accountType: utxo.accountType,
|
accountType: utxo.accountType,
|
||||||
txHex: ''
|
txHex: ''
|
||||||
})
|
})
|
||||||
|
@ -66,15 +66,15 @@ const mapAddressDataToUtxo = (wallet, addressData, utxo) => ({
|
||||||
selected: false
|
selected: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapWalletAccount = function (obj) {
|
const mapWalletAccount = function (o) {
|
||||||
obj._data = _.clone(obj)
|
return Object.assign({}, o, {
|
||||||
obj.date = obj.time
|
date: o.time
|
||||||
? Quasar.utils.date.formatDate(
|
? Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(o.time * 1000),
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
: ''
|
: '',
|
||||||
obj.label = obj.title // for drop-downs
|
label: o.title,
|
||||||
obj.expanded = false
|
expanded: false
|
||||||
return obj
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,99 +1,4 @@
|
||||||
const tables = {
|
const tables = {
|
||||||
walletsTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'new',
|
|
||||||
align: 'left',
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Title',
|
|
||||||
field: 'title'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Amount'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Type',
|
|
||||||
field: 'type'
|
|
||||||
},
|
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
},
|
|
||||||
filter: ''
|
|
||||||
},
|
|
||||||
utxosTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'expand',
|
|
||||||
align: 'left',
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'selected',
|
|
||||||
align: 'left',
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
align: 'center',
|
|
||||||
label: 'Status',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'address',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Address',
|
|
||||||
field: 'address',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Amount',
|
|
||||||
field: 'amount',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Date',
|
|
||||||
field: 'date',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'wallet',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Account',
|
|
||||||
field: 'wallet',
|
|
||||||
sortable: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
},
|
|
||||||
filter: ''
|
|
||||||
},
|
|
||||||
paymentTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'data',
|
|
||||||
align: 'left'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
},
|
|
||||||
filter: ''
|
|
||||||
},
|
|
||||||
summaryTable: {
|
summaryTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
@ -117,157 +22,36 @@ const tables = {
|
||||||
label: 'Change'
|
label: 'Change'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
addressesTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'expand',
|
|
||||||
align: 'left',
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'address',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Address',
|
|
||||||
field: 'address',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Amount',
|
|
||||||
field: 'amount',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'note',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Note',
|
|
||||||
field: 'note',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'wallet',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Account',
|
|
||||||
field: 'wallet',
|
|
||||||
sortable: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 0,
|
|
||||||
sortBy: 'amount',
|
|
||||||
descending: true
|
|
||||||
},
|
|
||||||
filter: ''
|
|
||||||
},
|
|
||||||
historyTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'expand',
|
|
||||||
align: 'left',
|
|
||||||
label: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Status'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Amount',
|
|
||||||
field: 'amount',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'address',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Address',
|
|
||||||
field: 'address',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Date',
|
|
||||||
field: 'date',
|
|
||||||
sortable: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
exportColums: [
|
|
||||||
{
|
|
||||||
label: 'Action',
|
|
||||||
field: 'action'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Date&Time',
|
|
||||||
field: 'date'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Amount',
|
|
||||||
field: 'amount'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Fee',
|
|
||||||
field: 'fee'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Transaction Id',
|
|
||||||
field: 'txId'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 0
|
|
||||||
},
|
|
||||||
filter: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableData = {
|
const tableData = {
|
||||||
walletAccounts: [],
|
|
||||||
addresses: {
|
|
||||||
show: false,
|
|
||||||
data: [],
|
|
||||||
history: [],
|
|
||||||
selectedWallet: null,
|
|
||||||
note: '',
|
|
||||||
filterOptions: [
|
|
||||||
'Show Change Addresses',
|
|
||||||
'Show Gap Addresses',
|
|
||||||
'Only With Amount'
|
|
||||||
],
|
|
||||||
filterValues: []
|
|
||||||
},
|
|
||||||
utxos: {
|
utxos: {
|
||||||
data: [],
|
data: [],
|
||||||
total: 0
|
total: 0
|
||||||
},
|
},
|
||||||
payment: {
|
payment: {
|
||||||
data: [{address: '', amount: undefined}],
|
|
||||||
changeWallet: null,
|
|
||||||
changeAddress: {},
|
|
||||||
changeAmount: 0,
|
|
||||||
|
|
||||||
feeRate: 1,
|
|
||||||
recommededFees: {
|
|
||||||
fastestFee: 1,
|
|
||||||
halfHourFee: 1,
|
|
||||||
hourFee: 1,
|
|
||||||
economyFee: 1,
|
|
||||||
minimumFee: 1
|
|
||||||
},
|
|
||||||
fee: 0,
|
fee: 0,
|
||||||
txSize: 0,
|
txSize: 0,
|
||||||
|
tx: null,
|
||||||
psbtBase64: '',
|
psbtBase64: '',
|
||||||
utxoSelectionModes: [
|
psbtBase64Signed: '',
|
||||||
'Manual',
|
signedTx: null,
|
||||||
'Random',
|
signedTxHex: null,
|
||||||
'Select All',
|
sentTxId: null,
|
||||||
'Smaller Inputs First',
|
|
||||||
'Larger Inputs First'
|
signModes: [
|
||||||
|
{
|
||||||
|
label: 'Serial Port Device',
|
||||||
|
value: 'serial-port'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Animated QR',
|
||||||
|
value: 'animated-qr',
|
||||||
|
disable: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
utxoSelectionMode: 'Manual',
|
signMode: '',
|
||||||
show: false,
|
show: false,
|
||||||
showAdvanced: false
|
showAdvanced: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
const PSBT_BASE64_PREFIX = 'cHNidP8'
|
||||||
|
const COMMAND_PASSWORD = '/password'
|
||||||
|
const COMMAND_PASSWORD_CLEAR = '/password-clear'
|
||||||
|
const COMMAND_SEND_PSBT = '/psbt'
|
||||||
|
const COMMAND_SIGN_PSBT = '/sign'
|
||||||
|
const COMMAND_HELP = '/help'
|
||||||
|
const COMMAND_WIPE = '/wipe'
|
||||||
|
const COMMAND_SEED = '/seed'
|
||||||
|
const COMMAND_RESTORE = '/restore'
|
||||||
|
const COMMAND_CONFIRM_NEXT = '/confirm-next'
|
||||||
|
const COMMAND_CANCEL = '/cancel'
|
||||||
|
const COMMAND_XPUB = '/xpub'
|
||||||
|
|
||||||
|
const DEFAULT_RECEIVE_GAP_LIMIT = 20
|
||||||
|
|
||||||
const blockTimeToDate = blockTime =>
|
const blockTimeToDate = blockTime =>
|
||||||
blockTime ? moment(blockTime * 1000).format('LLL') : ''
|
blockTime ? moment(blockTime * 1000).format('LLL') : ''
|
||||||
|
|
||||||
|
@ -97,3 +112,72 @@ const ACCOUNT_TYPES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAccountDescription = type => ACCOUNT_TYPES[type] || 'nonstandard'
|
const getAccountDescription = type => ACCOUNT_TYPES[type] || 'nonstandard'
|
||||||
|
|
||||||
|
const readFromSerialPort = reader => {
|
||||||
|
let partialChunk
|
||||||
|
let fulliness = []
|
||||||
|
|
||||||
|
const readStringUntil = async (separator = '\n') => {
|
||||||
|
if (fulliness.length) return fulliness.shift().trim()
|
||||||
|
const chunks = []
|
||||||
|
if (partialChunk) {
|
||||||
|
// leftovers from previous read
|
||||||
|
chunks.push(partialChunk)
|
||||||
|
partialChunk = undefined
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
const {value, done} = await reader.read()
|
||||||
|
if (value) {
|
||||||
|
const values = value.split(separator)
|
||||||
|
// found one or more separators
|
||||||
|
if (values.length > 1) {
|
||||||
|
chunks.push(values.shift()) // first element
|
||||||
|
partialChunk = values.pop() // last element
|
||||||
|
fulliness = values // full lines
|
||||||
|
return {value: chunks.join('').trim(), done: false}
|
||||||
|
}
|
||||||
|
chunks.push(value)
|
||||||
|
}
|
||||||
|
if (done) return {value: chunks.join('').trim(), done: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readStringUntil
|
||||||
|
}
|
||||||
|
|
||||||
|
function satOrBtc(val, showUnit = true, showSats = false) {
|
||||||
|
const value = showSats
|
||||||
|
? LNbits.utils.formatSat(val)
|
||||||
|
: val == 0
|
||||||
|
? 0.0
|
||||||
|
: (val / 100000000).toFixed(8)
|
||||||
|
if (!showUnit) return value
|
||||||
|
return showSats ? value + ' sat' : value + ' BTC'
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTemplateAsync(path) {
|
||||||
|
const result = new Promise(resolve => {
|
||||||
|
const xhttp = new XMLHttpRequest()
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function () {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) resolve(this.responseText)
|
||||||
|
|
||||||
|
if (this.status == 404) resolve(`<div>Page not found: ${path}</div>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhttp.open('GET', path, true)
|
||||||
|
xhttp.send()
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAccountPathIssues(path = '') {
|
||||||
|
const p = path.split('/')
|
||||||
|
if (p[0] !== 'm') return "Path must start with 'm/'"
|
||||||
|
for (let i = 1; i < p.length; i++) {
|
||||||
|
if (p[i].endsWith('')) p[i] = p[i].substring(0, p[i].length - 1)
|
||||||
|
if (isNaN(p[i])) return `${p[i]} is not a valid value`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,8 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from embit import script
|
import httpx
|
||||||
from embit.descriptor import Descriptor, Key
|
from embit import finalizer, script
|
||||||
from embit.ec import PublicKey
|
from embit.ec import PublicKey
|
||||||
from embit.psbt import PSBT, DerivationPath
|
from embit.psbt import PSBT, DerivationPath
|
||||||
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
||||||
|
@ -15,34 +16,44 @@ from lnbits.extensions.watchonly import watchonly_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_config,
|
create_config,
|
||||||
create_fresh_addresses,
|
create_fresh_addresses,
|
||||||
create_mempool,
|
|
||||||
create_watch_wallet,
|
create_watch_wallet,
|
||||||
delete_addresses_for_wallet,
|
delete_addresses_for_wallet,
|
||||||
delete_watch_wallet,
|
delete_watch_wallet,
|
||||||
get_addresses,
|
get_addresses,
|
||||||
get_config,
|
get_config,
|
||||||
get_fresh_address,
|
get_fresh_address,
|
||||||
get_mempool,
|
|
||||||
get_watch_wallet,
|
get_watch_wallet,
|
||||||
get_watch_wallets,
|
get_watch_wallets,
|
||||||
update_address,
|
update_address,
|
||||||
update_config,
|
update_config,
|
||||||
update_mempool,
|
|
||||||
update_watch_wallet,
|
update_watch_wallet,
|
||||||
)
|
)
|
||||||
from .helpers import parse_key
|
from .helpers import parse_key
|
||||||
from .models import Config, CreatePsbt, CreateWallet, WalletAccount
|
from .models import (
|
||||||
|
BroadcastTransaction,
|
||||||
|
Config,
|
||||||
|
CreatePsbt,
|
||||||
|
CreateWallet,
|
||||||
|
ExtractPsbt,
|
||||||
|
SignedTransaction,
|
||||||
|
WalletAccount,
|
||||||
|
)
|
||||||
|
|
||||||
###################WALLETS#############################
|
###################WALLETS#############################
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.get("/api/v1/wallet")
|
@watchonly_ext.get("/api/v1/wallet")
|
||||||
async def api_wallets_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_wallets_retrieve(
|
||||||
|
network: str = Query("Mainnet"), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return [wallet.dict() for wallet in await get_watch_wallets(wallet.wallet.user)]
|
return [
|
||||||
|
wallet.dict()
|
||||||
|
for wallet in await get_watch_wallets(wallet.wallet.user, network)
|
||||||
|
]
|
||||||
except:
|
except:
|
||||||
return ""
|
return []
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.get("/api/v1/wallet/{wallet_id}")
|
@watchonly_ext.get("/api/v1/wallet/{wallet_id}")
|
||||||
|
@ -64,7 +75,13 @@ async def api_wallet_create_or_update(
|
||||||
data: CreateWallet, w: WalletTypeInfo = Depends(require_admin_key)
|
data: CreateWallet, w: WalletTypeInfo = Depends(require_admin_key)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
(descriptor, _) = parse_key(data.masterpub)
|
(descriptor, network) = parse_key(data.masterpub)
|
||||||
|
if data.network != network["name"]:
|
||||||
|
raise ValueError(
|
||||||
|
"Account network error. This account is for '{}'".format(
|
||||||
|
network["name"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
new_wallet = WalletAccount(
|
new_wallet = WalletAccount(
|
||||||
id="none",
|
id="none",
|
||||||
|
@ -75,11 +92,19 @@ async def api_wallet_create_or_update(
|
||||||
title=data.title,
|
title=data.title,
|
||||||
address_no=-1, # so fresh address on empty wallet can get address with index 0
|
address_no=-1, # so fresh address on empty wallet can get address with index 0
|
||||||
balance=0,
|
balance=0,
|
||||||
|
network=network["name"],
|
||||||
)
|
)
|
||||||
|
|
||||||
wallets = await get_watch_wallets(w.wallet.user)
|
wallets = await get_watch_wallets(w.wallet.user, network["name"])
|
||||||
existing_wallet = next(
|
existing_wallet = next(
|
||||||
(ew for ew in wallets if ew.fingerprint == new_wallet.fingerprint), None
|
(
|
||||||
|
ew
|
||||||
|
for ew in wallets
|
||||||
|
if ew.fingerprint == new_wallet.fingerprint
|
||||||
|
and ew.network == new_wallet.network
|
||||||
|
and ew.masterpub == new_wallet.masterpub
|
||||||
|
),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
if existing_wallet:
|
if existing_wallet:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -218,12 +243,13 @@ async def api_psbt_create(
|
||||||
|
|
||||||
descriptors = {}
|
descriptors = {}
|
||||||
for _, masterpub in enumerate(data.masterpubs):
|
for _, masterpub in enumerate(data.masterpubs):
|
||||||
descriptors[masterpub.fingerprint] = parse_key(masterpub.public_key)
|
descriptors[masterpub.id] = parse_key(masterpub.public_key)
|
||||||
|
|
||||||
inputs_extra = []
|
inputs_extra = []
|
||||||
bip32_derivations = {}
|
|
||||||
for i, inp in enumerate(data.inputs):
|
for i, inp in enumerate(data.inputs):
|
||||||
descriptor = descriptors[inp.masterpub_fingerprint][0]
|
bip32_derivations = {}
|
||||||
|
descriptor = descriptors[inp.wallet][0]
|
||||||
d = descriptor.derive(inp.address_index, inp.branch_index)
|
d = descriptor.derive(inp.address_index, inp.branch_index)
|
||||||
for k in d.keys:
|
for k in d.keys:
|
||||||
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
||||||
|
@ -242,12 +268,13 @@ async def api_psbt_create(
|
||||||
for i, inp in enumerate(inputs_extra):
|
for i, inp in enumerate(inputs_extra):
|
||||||
psbt.inputs[i].bip32_derivations = inp["bip32_derivations"]
|
psbt.inputs[i].bip32_derivations = inp["bip32_derivations"]
|
||||||
psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None)
|
psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None)
|
||||||
|
print("### ", inp.get("non_witness_utxo", None))
|
||||||
|
|
||||||
outputs_extra = []
|
outputs_extra = []
|
||||||
bip32_derivations = {}
|
bip32_derivations = {}
|
||||||
for i, out in enumerate(data.outputs):
|
for i, out in enumerate(data.outputs):
|
||||||
if out.branch_index == 1:
|
if out.branch_index == 1:
|
||||||
descriptor = descriptors[out.masterpub_fingerprint][0]
|
descriptor = descriptors[out.wallet][0]
|
||||||
d = descriptor.derive(out.address_index, out.branch_index)
|
d = descriptor.derive(out.address_index, out.branch_index)
|
||||||
for k in d.keys:
|
for k in d.keys:
|
||||||
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
||||||
|
@ -264,6 +291,66 @@ async def api_psbt_create(
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@watchonly_ext.put("/api/v1/psbt/extract")
|
||||||
|
async def api_psbt_extract_tx(
|
||||||
|
data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
res = SignedTransaction()
|
||||||
|
try:
|
||||||
|
psbt = PSBT.from_base64(data.psbtBase64)
|
||||||
|
for i, inp in enumerate(data.inputs):
|
||||||
|
psbt.inputs[i].non_witness_utxo = Transaction.from_string(inp.tx_hex)
|
||||||
|
|
||||||
|
final_psbt = finalizer.finalize_psbt(psbt)
|
||||||
|
if not final_psbt:
|
||||||
|
raise ValueError("PSBT cannot be finalized!")
|
||||||
|
res.tx_hex = final_psbt.to_string()
|
||||||
|
|
||||||
|
transaction = Transaction.from_string(res.tx_hex)
|
||||||
|
tx = {
|
||||||
|
"locktime": transaction.locktime,
|
||||||
|
"version": transaction.version,
|
||||||
|
"outputs": [],
|
||||||
|
"fee": psbt.fee(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for out in transaction.vout:
|
||||||
|
tx["outputs"].append(
|
||||||
|
{"amount": out.value, "address": out.script_pubkey.address()}
|
||||||
|
)
|
||||||
|
res.tx_json = json.dumps(tx)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||||
|
return res.dict()
|
||||||
|
|
||||||
|
|
||||||
|
@watchonly_ext.post("/api/v1/tx")
|
||||||
|
async def api_tx_broadcast(
|
||||||
|
data: BroadcastTransaction, w: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
config = await get_config(w.wallet.user)
|
||||||
|
if not config:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot broadcast transaction. Mempool endpoint not defined!"
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoint = (
|
||||||
|
config.mempool_endpoint
|
||||||
|
if config.network == "Mainnet"
|
||||||
|
else config.mempool_endpoint + "/testnet"
|
||||||
|
)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.post(endpoint + "/api/tx", data=data.tx_hex)
|
||||||
|
tx_id = r.text
|
||||||
|
print("### broadcast tx_id: ", tx_id)
|
||||||
|
return tx_id
|
||||||
|
# return "0f0f0f0f0f0f0f0f0f0f0f00f0f0f0f0f0f0f0f0f0f00f0f0f0f0f0f0.mock.transaction.id"
|
||||||
|
except Exception as e:
|
||||||
|
print("### broadcast error: ", str(e))
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
#############################CONFIG##########################
|
#############################CONFIG##########################
|
||||||
|
|
||||||
|
|
||||||
|
@ -281,23 +368,3 @@ async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)):
|
||||||
if not config:
|
if not config:
|
||||||
config = await create_config(user=w.wallet.user)
|
config = await create_config(user=w.wallet.user)
|
||||||
return config.dict()
|
return config.dict()
|
||||||
|
|
||||||
|
|
||||||
#############################MEMPOOL##########################
|
|
||||||
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
@watchonly_ext.put("/api/v1/mempool")
|
|
||||||
async def api_update_mempool(
|
|
||||||
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user)
|
|
||||||
return mempool.dict()
|
|
||||||
|
|
||||||
|
|
||||||
### TODO: fix statspay dependcy and remove
|
|
||||||
@watchonly_ext.get("/api/v1/mempool")
|
|
||||||
async def api_get_mempool(w: WalletTypeInfo = Depends(require_admin_key)):
|
|
||||||
mempool = await get_mempool(w.wallet.user)
|
|
||||||
if not mempool:
|
|
||||||
mempool = await create_mempool(user=w.wallet.user)
|
|
||||||
return mempool.dict()
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ new Vue({
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
is_unique: true,
|
is_unique: true,
|
||||||
use_custom: true,
|
use_custom: false,
|
||||||
title: 'Vouchers',
|
title: 'Vouchers',
|
||||||
min_withdrawable: 0,
|
min_withdrawable: 0,
|
||||||
wait_time: 1
|
wait_time: 1
|
||||||
|
@ -125,7 +125,6 @@ new Vue({
|
||||||
var link = _.findWhere(this.withdrawLinks, {id: linkId})
|
var link = _.findWhere(this.withdrawLinks, {id: linkId})
|
||||||
|
|
||||||
this.qrCodeDialog.data = _.clone(link)
|
this.qrCodeDialog.data = _.clone(link)
|
||||||
console.log(this.qrCodeDialog.data)
|
|
||||||
this.qrCodeDialog.data.url =
|
this.qrCodeDialog.data.url =
|
||||||
window.location.protocol + '//' + window.location.host
|
window.location.protocol + '//' + window.location.host
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
|
@ -140,6 +139,11 @@ new Vue({
|
||||||
id: this.formDialog.data.wallet
|
id: this.formDialog.data.wallet
|
||||||
})
|
})
|
||||||
var data = _.omit(this.formDialog.data, 'wallet')
|
var data = _.omit(this.formDialog.data, 'wallet')
|
||||||
|
|
||||||
|
if (!data.use_custom) {
|
||||||
|
data.custom_url = null
|
||||||
|
}
|
||||||
|
|
||||||
if (data.use_custom && !data?.custom_url) {
|
if (data.use_custom && !data?.custom_url) {
|
||||||
data.custom_url = CUSTOM_URL
|
data.custom_url = CUSTOM_URL
|
||||||
}
|
}
|
||||||
|
@ -168,6 +172,10 @@ new Vue({
|
||||||
data.title = 'vouchers'
|
data.title = 'vouchers'
|
||||||
data.is_unique = true
|
data.is_unique = true
|
||||||
|
|
||||||
|
if (!data.use_custom) {
|
||||||
|
data.custom_url = null
|
||||||
|
}
|
||||||
|
|
||||||
if (data.use_custom && !data?.custom_url) {
|
if (data.use_custom && !data?.custom_url) {
|
||||||
data.custom_url = '/static/images/default_voucher.png'
|
data.custom_url = '/static/images/default_voucher.png'
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,7 +241,7 @@
|
||||||
v-model="formDialog.data.custom_url"
|
v-model="formDialog.data.custom_url"
|
||||||
type="text"
|
type="text"
|
||||||
label="Custom design .png (optional)"
|
label="Custom design .png (optional)"
|
||||||
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
|
hint="Enter a URL if you want to use a custom design or leave blank for LNbits designed vouchers!"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item tag="label" class="rounded-borders">
|
<q-item tag="label" class="rounded-borders">
|
||||||
|
@ -353,7 +353,7 @@
|
||||||
v-model="simpleformDialog.data.custom_url"
|
v-model="simpleformDialog.data.custom_url"
|
||||||
type="text"
|
type="text"
|
||||||
label="Custom design .png (optional)"
|
label="Custom design .png (optional)"
|
||||||
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
|
hint="Enter a URL if you want to use a custom design or leave blank for LNbits designed vouchers!"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
|
|
|
@ -9,7 +9,10 @@
|
||||||
<img src="{{custom_url}}" alt="..." />
|
<img src="{{custom_url}}" alt="..." />
|
||||||
<span>{{ amt }} sats</span>
|
<span>{{ amt }} sats</span>
|
||||||
<div class="lnurlw">
|
<div class="lnurlw">
|
||||||
<qrcode :value="'{{one}}'" :options="{width: 95, margin: 1}"></qrcode>
|
<qrcode
|
||||||
|
:value="theurl + '/?lightning={{one}}'"
|
||||||
|
:options="{width: 95, margin: 1}"
|
||||||
|
></qrcode>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,18 +1,49 @@
|
||||||
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
from lnbits.settings import HOST, PORT
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--port", default="5000", help="Port to run LNBits on")
|
@click.command(
|
||||||
@click.option("--host", default="127.0.0.1", help="Host to run LNBits on")
|
context_settings=dict(
|
||||||
def main(port, host):
|
ignore_unknown_options=True,
|
||||||
|
allow_extra_args=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@click.option("--port", default=PORT, help="Port to listen on")
|
||||||
|
@click.option("--host", default=HOST, help="Host to run LNBits on")
|
||||||
|
@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile")
|
||||||
|
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str):
|
||||||
"""Launched with `poetry run lnbits` at root level"""
|
"""Launched with `poetry run lnbits` at root level"""
|
||||||
uvicorn.run("lnbits.__main__:app", port=port, host=host)
|
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||||
|
d = dict()
|
||||||
|
for a in ctx.args:
|
||||||
|
item = a.split("=")
|
||||||
|
if len(item) > 1: # argument like --key=value
|
||||||
|
print(a, item)
|
||||||
|
d[item[0].strip("--").replace("-", "_")] = (
|
||||||
|
int(item[1]) # need to convert to int if it's a number
|
||||||
|
if item[1].isdigit()
|
||||||
|
else item[1]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
d[a.strip("--")] = True # argument like --key
|
||||||
|
|
||||||
|
config = uvicorn.Config(
|
||||||
|
"lnbits.__main__:app",
|
||||||
|
port=port,
|
||||||
|
host=host,
|
||||||
|
ssl_keyfile=ssl_keyfile,
|
||||||
|
ssl_certfile=ssl_certfile,
|
||||||
|
**d
|
||||||
|
)
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
# def main():
|
|
||||||
# """Launched with `poetry run start` at root level"""
|
|
||||||
# uvicorn.run("lnbits.__main__:app")
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
|
|
||||||
from .clightning import CLightningWallet
|
from .cliche import ClicheWallet
|
||||||
|
from .cln import CoreLightningWallet # legacy .env support
|
||||||
|
from .cln import CoreLightningWallet as CLightningWallet
|
||||||
from .eclair import EclairWallet
|
from .eclair import EclairWallet
|
||||||
from .fake import FakeWallet
|
from .fake import FakeWallet
|
||||||
from .lnbits import LNbitsWallet
|
from .lnbits import LNbitsWallet
|
||||||
|
from .lndgrpc import LndWallet
|
||||||
from .lndrest import LndRestWallet
|
from .lndrest import LndRestWallet
|
||||||
from .lnpay import LNPayWallet
|
from .lnpay import LNPayWallet
|
||||||
from .lntxbot import LntxbotWallet
|
from .lntxbot import LntxbotWallet
|
||||||
|
|
143
lnbits/wallets/cliche.py
Normal file
143
lnbits/wallets/cliche.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from os import getenv
|
||||||
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
from websocket import create_connection
|
||||||
|
|
||||||
|
from .base import (
|
||||||
|
InvoiceResponse,
|
||||||
|
PaymentResponse,
|
||||||
|
PaymentStatus,
|
||||||
|
StatusResponse,
|
||||||
|
Wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClicheWallet(Wallet):
|
||||||
|
"""https://github.com/fiatjaf/cliche"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.endpoint = getenv("CLICHE_ENDPOINT")
|
||||||
|
|
||||||
|
async def status(self) -> StatusResponse:
|
||||||
|
try:
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send("get-info")
|
||||||
|
r = ws.recv()
|
||||||
|
except Exception as exc:
|
||||||
|
return StatusResponse(
|
||||||
|
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = json.loads(r)
|
||||||
|
except:
|
||||||
|
return StatusResponse(
|
||||||
|
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return StatusResponse(None, data["result"]["wallets"][0]["balance"])
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
if description_hash:
|
||||||
|
description_hash_hashed = hashlib.sha256(description_hash).hexdigest()
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send(
|
||||||
|
f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_hashed}"
|
||||||
|
)
|
||||||
|
r = ws.recv()
|
||||||
|
else:
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send(f"create-invoice --msatoshi {amount*1000} --description {memo}")
|
||||||
|
r = ws.recv()
|
||||||
|
data = json.loads(r)
|
||||||
|
checking_id = None
|
||||||
|
payment_request = None
|
||||||
|
error_message = None
|
||||||
|
|
||||||
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
|
logger.error(data["error"]["message"])
|
||||||
|
error_message = data["error"]["message"]
|
||||||
|
return InvoiceResponse(False, checking_id, payment_request, error_message)
|
||||||
|
|
||||||
|
if data.get("result") is not None:
|
||||||
|
checking_id, payment_request = (
|
||||||
|
data["result"]["payment_hash"],
|
||||||
|
data["result"]["invoice"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return InvoiceResponse(
|
||||||
|
False, checking_id, payment_request, "Could not get payment hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
||||||
|
|
||||||
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||||
|
r = ws.recv()
|
||||||
|
data = json.loads(r)
|
||||||
|
checking_id = None
|
||||||
|
error_message = None
|
||||||
|
|
||||||
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
|
logger.error(data["error"]["message"])
|
||||||
|
error_message = data["error"]["message"]
|
||||||
|
return PaymentResponse(False, None, 0, error_message)
|
||||||
|
|
||||||
|
if data.get("result") is not None and data["result"].get("payment_hash"):
|
||||||
|
checking_id = data["result"]["payment_hash"]
|
||||||
|
else:
|
||||||
|
return PaymentResponse(False, checking_id, 0, "Could not get payment hash")
|
||||||
|
|
||||||
|
return PaymentResponse(True, checking_id, 0, error_message)
|
||||||
|
|
||||||
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send(f"check-payment --hash {checking_id}")
|
||||||
|
r = ws.recv()
|
||||||
|
data = json.loads(r)
|
||||||
|
|
||||||
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
|
logger.error(data["error"]["message"])
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
statuses = {"pending": None, "complete": True, "failed": False}
|
||||||
|
return PaymentStatus(statuses[data["result"]["status"]])
|
||||||
|
|
||||||
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
ws = create_connection(self.endpoint)
|
||||||
|
ws.send(f"check-payment --hash {checking_id}")
|
||||||
|
r = ws.recv()
|
||||||
|
data = json.loads(r)
|
||||||
|
|
||||||
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
|
logger.error(data["error"]["message"])
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
statuses = {"pending": None, "complete": True, "failed": False}
|
||||||
|
return PaymentStatus(statuses[data["result"]["status"]])
|
||||||
|
|
||||||
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
try:
|
||||||
|
ws = await create_connection(self.endpoint)
|
||||||
|
while True:
|
||||||
|
r = await ws.recv()
|
||||||
|
data = json.loads(r)
|
||||||
|
try:
|
||||||
|
if data["result"]["status"]:
|
||||||
|
yield data["result"]["payment_hash"]
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
logger.error("lost connection to cliche's websocket, retrying in 5 seconds")
|
||||||
|
await asyncio.sleep(5)
|
|
@ -1,15 +1,18 @@
|
||||||
try:
|
try:
|
||||||
from lightning import LightningRpc, RpcError # type: ignore
|
from pyln.client import LightningRpc, RpcError # type: ignore
|
||||||
except ImportError: # pragma: nocover
|
except ImportError: # pragma: nocover
|
||||||
LightningRpc = None
|
LightningRpc = None
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11 as lnbits_bolt11
|
from lnbits import bolt11 as lnbits_bolt11
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
|
@ -41,26 +44,20 @@ def _paid_invoices_stream(ln, last_pay_index):
|
||||||
return ln.waitanyinvoice(last_pay_index)
|
return ln.waitanyinvoice(last_pay_index)
|
||||||
|
|
||||||
|
|
||||||
class CLightningWallet(Wallet):
|
class CoreLightningWallet(Wallet):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if LightningRpc is None: # pragma: nocover
|
if LightningRpc is None: # pragma: nocover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"The `pylightning` library must be installed to use `CLightningWallet`."
|
"The `pyln-client` library must be installed to use `CoreLightningWallet`."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.rpc = getenv("CLIGHTNING_RPC")
|
self.rpc = getenv("CORELIGHTNING_RPC") or getenv("CLIGHTNING_RPC")
|
||||||
self.ln = LightningRpc(self.rpc)
|
self.ln = LightningRpc(self.rpc)
|
||||||
|
|
||||||
# check description_hash support (could be provided by a plugin)
|
# check if description_hash is supported (from CLN>=v0.11.0)
|
||||||
self.supports_description_hash = False
|
self.supports_description_hash = (
|
||||||
try:
|
"deschashonly" in self.ln.help("invoice")["help"][0]["command"]
|
||||||
answer = self.ln.help("invoicewithdescriptionhash")
|
)
|
||||||
if answer["help"][0]["command"].startswith(
|
|
||||||
"invoicewithdescriptionhash msatoshi label description_hash"
|
|
||||||
):
|
|
||||||
self.supports_description_hash = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# check last payindex so we can listen from that point on
|
# check last payindex so we can listen from that point on
|
||||||
self.last_pay_index = 0
|
self.last_pay_index = 0
|
||||||
|
@ -87,22 +84,33 @@ class CLightningWallet(Wallet):
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = "lbl{}".format(random.random())
|
label = "lbl{}".format(random.random())
|
||||||
msat = amount * 1000
|
msat: int = int(amount * 1000)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if description_hash:
|
if description_hash and not self.supports_description_hash:
|
||||||
if not self.supports_description_hash:
|
raise Unsupported("description_hash")
|
||||||
raise Unsupported("description_hash")
|
r = self.ln.invoice(
|
||||||
|
msatoshi=msat,
|
||||||
|
label=label,
|
||||||
|
description=description_hash.decode("utf-8")
|
||||||
|
if description_hash
|
||||||
|
else memo,
|
||||||
|
exposeprivatechannels=True,
|
||||||
|
deschashonly=True
|
||||||
|
if description_hash
|
||||||
|
else False, # we can't pass None here
|
||||||
|
)
|
||||||
|
|
||||||
params = [msat, label, description_hash.hex()]
|
if r.get("code") and r.get("code") < 0:
|
||||||
r = self.ln.call("invoicewithdescriptionhash", params)
|
raise Exception(r.get("message"))
|
||||||
return InvoiceResponse(True, label, r["bolt11"], "")
|
|
||||||
else:
|
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
||||||
r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True)
|
|
||||||
return InvoiceResponse(True, label, r["bolt11"], "")
|
|
||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
||||||
return InvoiceResponse(False, label, None, error_message)
|
logger.error("RPC error:", error_message)
|
||||||
|
return InvoiceResponse(False, None, None, error_message)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("error:", e)
|
||||||
|
return InvoiceResponse(False, None, None, str(e))
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
invoice = lnbits_bolt11.decode(bolt11)
|
invoice = lnbits_bolt11.decode(bolt11)
|
||||||
|
@ -116,25 +124,32 @@ class CLightningWallet(Wallet):
|
||||||
try:
|
try:
|
||||||
wrapped = async_wrap(_pay_invoice)
|
wrapped = async_wrap(_pay_invoice)
|
||||||
r = await wrapped(self.ln, payload)
|
r = await wrapped(self.ln, payload)
|
||||||
except RpcError as exc:
|
except Exception as exc:
|
||||||
return PaymentResponse(False, None, 0, None, str(exc))
|
return PaymentResponse(False, None, 0, None, str(exc))
|
||||||
|
|
||||||
fee_msat = r["msatoshi_sent"] - r["msatoshi"]
|
fee_msat = r["msatoshi_sent"] - r["msatoshi"]
|
||||||
preimage = r["payment_preimage"]
|
return PaymentResponse(
|
||||||
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
|
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = self.ln.listinvoices(checking_id)
|
try:
|
||||||
|
r = self.ln.listinvoices(payment_hash=checking_id)
|
||||||
|
except:
|
||||||
|
return PaymentStatus(None)
|
||||||
if not r["invoices"]:
|
if not r["invoices"]:
|
||||||
return PaymentStatus(False)
|
return PaymentStatus(None)
|
||||||
if r["invoices"][0]["label"] == checking_id:
|
if r["invoices"][0]["payment_hash"] == checking_id:
|
||||||
return PaymentStatus(r["invoices"][0]["status"] == "paid")
|
return PaymentStatus(r["invoices"][0]["status"] == "paid")
|
||||||
raise KeyError("supplied an invalid checking_id")
|
raise KeyError("supplied an invalid checking_id")
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = self.ln.call("listpays", {"payment_hash": checking_id})
|
try:
|
||||||
|
r = self.ln.call("listpays", {"payment_hash": checking_id})
|
||||||
|
except:
|
||||||
|
return PaymentStatus(None)
|
||||||
if not r["pays"]:
|
if not r["pays"]:
|
||||||
return PaymentStatus(False)
|
return PaymentStatus(None)
|
||||||
if r["pays"][0]["payment_hash"] == checking_id:
|
if r["pays"][0]["payment_hash"] == checking_id:
|
||||||
status = r["pays"][0]["status"]
|
status = r["pays"][0]["status"]
|
||||||
if status == "complete":
|
if status == "complete":
|
||||||
|
@ -146,7 +161,13 @@ class CLightningWallet(Wallet):
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
wrapped = async_wrap(_paid_invoices_stream)
|
try:
|
||||||
paid = await wrapped(self.ln, self.last_pay_index)
|
wrapped = async_wrap(_paid_invoices_stream)
|
||||||
self.last_pay_index = paid["pay_index"]
|
paid = await wrapped(self.ln, self.last_pay_index)
|
||||||
yield paid["label"]
|
self.last_pay_index = paid["pay_index"]
|
||||||
|
yield paid["payment_hash"]
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(
|
||||||
|
f"lost connection to cln invoices stream: '{exc}', retrying in 5 seconds"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(5)
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -72,7 +73,7 @@ class EclairWallet(Wallet):
|
||||||
|
|
||||||
data: Dict = {"amountMsat": amount * 1000}
|
data: Dict = {"amountMsat": amount * 1000}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["description"] = memo or ""
|
data["description"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ class FakeWallet(Wallet):
|
||||||
data["timestamp"] = datetime.now().timestamp()
|
data["timestamp"] = datetime.now().timestamp()
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["tags_set"] = ["h"]
|
data["tags_set"] = ["h"]
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.decode("utf-8")
|
||||||
else:
|
else:
|
||||||
data["tags_set"] = ["d"]
|
data["tags_set"] = ["d"]
|
||||||
data["memo"] = memo
|
data["memo"] = memo
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
@ -59,7 +60,7 @@ class LNbitsWallet(Wallet):
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"out": False, "amount": amount}
|
data: Dict = {"out": False, "amount": amount}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
665
lnbits/wallets/lnd_grpc_files/router_pb2.py
Normal file
665
lnbits/wallets/lnd_grpc_files/router_pb2.py
Normal file
File diff suppressed because one or more lines are too long
871
lnbits/wallets/lnd_grpc_files/router_pb2_grpc.py
Normal file
871
lnbits/wallets/lnd_grpc_files/router_pb2_grpc.py
Normal file
|
@ -0,0 +1,871 @@
|
||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
|
||||||
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router__pb2
|
||||||
|
|
||||||
|
|
||||||
|
class RouterStub(object):
|
||||||
|
"""Router is a service that offers advanced interaction with the router
|
||||||
|
subsystem of the daemon.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.SendPaymentV2 = channel.unary_stream(
|
||||||
|
"/routerrpc.Router/SendPaymentV2",
|
||||||
|
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
|
||||||
|
response_deserializer=lightning__pb2.Payment.FromString,
|
||||||
|
)
|
||||||
|
self.TrackPaymentV2 = channel.unary_stream(
|
||||||
|
"/routerrpc.Router/TrackPaymentV2",
|
||||||
|
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
|
||||||
|
response_deserializer=lightning__pb2.Payment.FromString,
|
||||||
|
)
|
||||||
|
self.EstimateRouteFee = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/EstimateRouteFee",
|
||||||
|
request_serializer=router__pb2.RouteFeeRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.RouteFeeResponse.FromString,
|
||||||
|
)
|
||||||
|
self.SendToRoute = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/SendToRoute",
|
||||||
|
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.SendToRouteResponse.FromString,
|
||||||
|
)
|
||||||
|
self.SendToRouteV2 = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/SendToRouteV2",
|
||||||
|
request_serializer=router__pb2.SendToRouteRequest.SerializeToString,
|
||||||
|
response_deserializer=lightning__pb2.HTLCAttempt.FromString,
|
||||||
|
)
|
||||||
|
self.ResetMissionControl = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/ResetMissionControl",
|
||||||
|
request_serializer=router__pb2.ResetMissionControlRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.ResetMissionControlResponse.FromString,
|
||||||
|
)
|
||||||
|
self.QueryMissionControl = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/QueryMissionControl",
|
||||||
|
request_serializer=router__pb2.QueryMissionControlRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.QueryMissionControlResponse.FromString,
|
||||||
|
)
|
||||||
|
self.XImportMissionControl = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/XImportMissionControl",
|
||||||
|
request_serializer=router__pb2.XImportMissionControlRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.XImportMissionControlResponse.FromString,
|
||||||
|
)
|
||||||
|
self.GetMissionControlConfig = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/GetMissionControlConfig",
|
||||||
|
request_serializer=router__pb2.GetMissionControlConfigRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.GetMissionControlConfigResponse.FromString,
|
||||||
|
)
|
||||||
|
self.SetMissionControlConfig = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/SetMissionControlConfig",
|
||||||
|
request_serializer=router__pb2.SetMissionControlConfigRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.SetMissionControlConfigResponse.FromString,
|
||||||
|
)
|
||||||
|
self.QueryProbability = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/QueryProbability",
|
||||||
|
request_serializer=router__pb2.QueryProbabilityRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.QueryProbabilityResponse.FromString,
|
||||||
|
)
|
||||||
|
self.BuildRoute = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/BuildRoute",
|
||||||
|
request_serializer=router__pb2.BuildRouteRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.BuildRouteResponse.FromString,
|
||||||
|
)
|
||||||
|
self.SubscribeHtlcEvents = channel.unary_stream(
|
||||||
|
"/routerrpc.Router/SubscribeHtlcEvents",
|
||||||
|
request_serializer=router__pb2.SubscribeHtlcEventsRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.HtlcEvent.FromString,
|
||||||
|
)
|
||||||
|
self.SendPayment = channel.unary_stream(
|
||||||
|
"/routerrpc.Router/SendPayment",
|
||||||
|
request_serializer=router__pb2.SendPaymentRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.PaymentStatus.FromString,
|
||||||
|
)
|
||||||
|
self.TrackPayment = channel.unary_stream(
|
||||||
|
"/routerrpc.Router/TrackPayment",
|
||||||
|
request_serializer=router__pb2.TrackPaymentRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.PaymentStatus.FromString,
|
||||||
|
)
|
||||||
|
self.HtlcInterceptor = channel.stream_stream(
|
||||||
|
"/routerrpc.Router/HtlcInterceptor",
|
||||||
|
request_serializer=router__pb2.ForwardHtlcInterceptResponse.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.ForwardHtlcInterceptRequest.FromString,
|
||||||
|
)
|
||||||
|
self.UpdateChanStatus = channel.unary_unary(
|
||||||
|
"/routerrpc.Router/UpdateChanStatus",
|
||||||
|
request_serializer=router__pb2.UpdateChanStatusRequest.SerializeToString,
|
||||||
|
response_deserializer=router__pb2.UpdateChanStatusResponse.FromString,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterServicer(object):
|
||||||
|
"""Router is a service that offers advanced interaction with the router
|
||||||
|
subsystem of the daemon.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def SendPaymentV2(self, request, context):
|
||||||
|
"""
|
||||||
|
SendPaymentV2 attempts to route a payment described by the passed
|
||||||
|
PaymentRequest to the final destination. The call returns a stream of
|
||||||
|
payment updates.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def TrackPaymentV2(self, request, context):
|
||||||
|
"""
|
||||||
|
TrackPaymentV2 returns an update stream for the payment identified by the
|
||||||
|
payment hash.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def EstimateRouteFee(self, request, context):
|
||||||
|
"""
|
||||||
|
EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it
|
||||||
|
may cost to send an HTLC to the target end destination.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def SendToRoute(self, request, context):
|
||||||
|
"""
|
||||||
|
Deprecated, use SendToRouteV2. SendToRoute attempts to make a payment via
|
||||||
|
the specified route. This method differs from SendPayment in that it
|
||||||
|
allows users to specify a full route manually. This can be used for
|
||||||
|
things like rebalancing, and atomic swaps. It differs from the newer
|
||||||
|
SendToRouteV2 in that it doesn't return the full HTLC information.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def SendToRouteV2(self, request, context):
|
||||||
|
"""
|
||||||
|
SendToRouteV2 attempts to make a payment via the specified route. This
|
||||||
|
method differs from SendPayment in that it allows users to specify a full
|
||||||
|
route manually. This can be used for things like rebalancing, and atomic
|
||||||
|
swaps.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def ResetMissionControl(self, request, context):
|
||||||
|
"""
|
||||||
|
ResetMissionControl clears all mission control state and starts with a clean
|
||||||
|
slate.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def QueryMissionControl(self, request, context):
|
||||||
|
"""
|
||||||
|
QueryMissionControl exposes the internal mission control state to callers.
|
||||||
|
It is a development feature.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def XImportMissionControl(self, request, context):
|
||||||
|
"""
|
||||||
|
XImportMissionControl is an experimental API that imports the state provided
|
||||||
|
to the internal mission control's state, using all results which are more
|
||||||
|
recent than our existing values. These values will only be imported
|
||||||
|
in-memory, and will not be persisted across restarts.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def GetMissionControlConfig(self, request, context):
|
||||||
|
"""
|
||||||
|
GetMissionControlConfig returns mission control's current config.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def SetMissionControlConfig(self, request, context):
|
||||||
|
"""
|
||||||
|
SetMissionControlConfig will set mission control's config, if the config
|
||||||
|
provided is valid.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def QueryProbability(self, request, context):
|
||||||
|
"""
|
||||||
|
QueryProbability returns the current success probability estimate for a
|
||||||
|
given node pair and amount.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def BuildRoute(self, request, context):
|
||||||
|
"""
|
||||||
|
BuildRoute builds a fully specified route based on a list of hop public
|
||||||
|
keys. It retrieves the relevant channel policies from the graph in order to
|
||||||
|
calculate the correct fees and time locks.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def SubscribeHtlcEvents(self, request, context):
|
||||||
|
"""
|
||||||
|
SubscribeHtlcEvents creates a uni-directional stream from the server to
|
||||||
|
the client which delivers a stream of htlc events.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def SendPayment(self, request, context):
|
||||||
|
"""
|
||||||
|
Deprecated, use SendPaymentV2. SendPayment attempts to route a payment
|
||||||
|
described by the passed PaymentRequest to the final destination. The call
|
||||||
|
returns a stream of payment status updates.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def TrackPayment(self, request, context):
|
||||||
|
"""
|
||||||
|
Deprecated, use TrackPaymentV2. TrackPayment returns an update stream for
|
||||||
|
the payment identified by the payment hash.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def HtlcInterceptor(self, request_iterator, context):
|
||||||
|
"""*
|
||||||
|
HtlcInterceptor dispatches a bi-directional streaming RPC in which
|
||||||
|
Forwarded HTLC requests are sent to the client and the client responds with
|
||||||
|
a boolean that tells LND if this htlc should be intercepted.
|
||||||
|
In case of interception, the htlc can be either settled, cancelled or
|
||||||
|
resumed later by using the ResolveHoldForward endpoint.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
def UpdateChanStatus(self, request, context):
|
||||||
|
"""
|
||||||
|
UpdateChanStatus attempts to manually set the state of a channel
|
||||||
|
(enabled, disabled, or auto). A manual "disable" request will cause the
|
||||||
|
channel to stay disabled until a subsequent manual request of either
|
||||||
|
"enable" or "auto".
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details("Method not implemented!")
|
||||||
|
raise NotImplementedError("Method not implemented!")
|
||||||
|
|
||||||
|
|
||||||
|
def add_RouterServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
"SendPaymentV2": grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.SendPaymentV2,
|
||||||
|
request_deserializer=router__pb2.SendPaymentRequest.FromString,
|
||||||
|
response_serializer=lightning__pb2.Payment.SerializeToString,
|
||||||
|
),
|
||||||
|
"TrackPaymentV2": grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.TrackPaymentV2,
|
||||||
|
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
|
||||||
|
response_serializer=lightning__pb2.Payment.SerializeToString,
|
||||||
|
),
|
||||||
|
"EstimateRouteFee": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.EstimateRouteFee,
|
||||||
|
request_deserializer=router__pb2.RouteFeeRequest.FromString,
|
||||||
|
response_serializer=router__pb2.RouteFeeResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"SendToRoute": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.SendToRoute,
|
||||||
|
request_deserializer=router__pb2.SendToRouteRequest.FromString,
|
||||||
|
response_serializer=router__pb2.SendToRouteResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"SendToRouteV2": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.SendToRouteV2,
|
||||||
|
request_deserializer=router__pb2.SendToRouteRequest.FromString,
|
||||||
|
response_serializer=lightning__pb2.HTLCAttempt.SerializeToString,
|
||||||
|
),
|
||||||
|
"ResetMissionControl": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ResetMissionControl,
|
||||||
|
request_deserializer=router__pb2.ResetMissionControlRequest.FromString,
|
||||||
|
response_serializer=router__pb2.ResetMissionControlResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"QueryMissionControl": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.QueryMissionControl,
|
||||||
|
request_deserializer=router__pb2.QueryMissionControlRequest.FromString,
|
||||||
|
response_serializer=router__pb2.QueryMissionControlResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"XImportMissionControl": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.XImportMissionControl,
|
||||||
|
request_deserializer=router__pb2.XImportMissionControlRequest.FromString,
|
||||||
|
response_serializer=router__pb2.XImportMissionControlResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"GetMissionControlConfig": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.GetMissionControlConfig,
|
||||||
|
request_deserializer=router__pb2.GetMissionControlConfigRequest.FromString,
|
||||||
|
response_serializer=router__pb2.GetMissionControlConfigResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"SetMissionControlConfig": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.SetMissionControlConfig,
|
||||||
|
request_deserializer=router__pb2.SetMissionControlConfigRequest.FromString,
|
||||||
|
response_serializer=router__pb2.SetMissionControlConfigResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"QueryProbability": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.QueryProbability,
|
||||||
|
request_deserializer=router__pb2.QueryProbabilityRequest.FromString,
|
||||||
|
response_serializer=router__pb2.QueryProbabilityResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"BuildRoute": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.BuildRoute,
|
||||||
|
request_deserializer=router__pb2.BuildRouteRequest.FromString,
|
||||||
|
response_serializer=router__pb2.BuildRouteResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
"SubscribeHtlcEvents": grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.SubscribeHtlcEvents,
|
||||||
|
request_deserializer=router__pb2.SubscribeHtlcEventsRequest.FromString,
|
||||||
|
response_serializer=router__pb2.HtlcEvent.SerializeToString,
|
||||||
|
),
|
||||||
|
"SendPayment": grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.SendPayment,
|
||||||
|
request_deserializer=router__pb2.SendPaymentRequest.FromString,
|
||||||
|
response_serializer=router__pb2.PaymentStatus.SerializeToString,
|
||||||
|
),
|
||||||
|
"TrackPayment": grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.TrackPayment,
|
||||||
|
request_deserializer=router__pb2.TrackPaymentRequest.FromString,
|
||||||
|
response_serializer=router__pb2.PaymentStatus.SerializeToString,
|
||||||
|
),
|
||||||
|
"HtlcInterceptor": grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.HtlcInterceptor,
|
||||||
|
request_deserializer=router__pb2.ForwardHtlcInterceptResponse.FromString,
|
||||||
|
response_serializer=router__pb2.ForwardHtlcInterceptRequest.SerializeToString,
|
||||||
|
),
|
||||||
|
"UpdateChanStatus": grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.UpdateChanStatus,
|
||||||
|
request_deserializer=router__pb2.UpdateChanStatusRequest.FromString,
|
||||||
|
response_serializer=router__pb2.UpdateChanStatusResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
"routerrpc.Router", rpc_method_handlers
|
||||||
|
)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class Router(object):
|
||||||
|
"""Router is a service that offers advanced interaction with the router
|
||||||
|
subsystem of the daemon.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SendPaymentV2(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SendPaymentV2",
|
||||||
|
router__pb2.SendPaymentRequest.SerializeToString,
|
||||||
|
lightning__pb2.Payment.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def TrackPaymentV2(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/TrackPaymentV2",
|
||||||
|
router__pb2.TrackPaymentRequest.SerializeToString,
|
||||||
|
lightning__pb2.Payment.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def EstimateRouteFee(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/EstimateRouteFee",
|
||||||
|
router__pb2.RouteFeeRequest.SerializeToString,
|
||||||
|
router__pb2.RouteFeeResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SendToRoute(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SendToRoute",
|
||||||
|
router__pb2.SendToRouteRequest.SerializeToString,
|
||||||
|
router__pb2.SendToRouteResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SendToRouteV2(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SendToRouteV2",
|
||||||
|
router__pb2.SendToRouteRequest.SerializeToString,
|
||||||
|
lightning__pb2.HTLCAttempt.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ResetMissionControl(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/ResetMissionControl",
|
||||||
|
router__pb2.ResetMissionControlRequest.SerializeToString,
|
||||||
|
router__pb2.ResetMissionControlResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def QueryMissionControl(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/QueryMissionControl",
|
||||||
|
router__pb2.QueryMissionControlRequest.SerializeToString,
|
||||||
|
router__pb2.QueryMissionControlResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def XImportMissionControl(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/XImportMissionControl",
|
||||||
|
router__pb2.XImportMissionControlRequest.SerializeToString,
|
||||||
|
router__pb2.XImportMissionControlResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def GetMissionControlConfig(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/GetMissionControlConfig",
|
||||||
|
router__pb2.GetMissionControlConfigRequest.SerializeToString,
|
||||||
|
router__pb2.GetMissionControlConfigResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SetMissionControlConfig(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SetMissionControlConfig",
|
||||||
|
router__pb2.SetMissionControlConfigRequest.SerializeToString,
|
||||||
|
router__pb2.SetMissionControlConfigResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def QueryProbability(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/QueryProbability",
|
||||||
|
router__pb2.QueryProbabilityRequest.SerializeToString,
|
||||||
|
router__pb2.QueryProbabilityResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def BuildRoute(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/BuildRoute",
|
||||||
|
router__pb2.BuildRouteRequest.SerializeToString,
|
||||||
|
router__pb2.BuildRouteResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SubscribeHtlcEvents(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SubscribeHtlcEvents",
|
||||||
|
router__pb2.SubscribeHtlcEventsRequest.SerializeToString,
|
||||||
|
router__pb2.HtlcEvent.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SendPayment(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/SendPayment",
|
||||||
|
router__pb2.SendPaymentRequest.SerializeToString,
|
||||||
|
router__pb2.PaymentStatus.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def TrackPayment(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/TrackPayment",
|
||||||
|
router__pb2.TrackPaymentRequest.SerializeToString,
|
||||||
|
router__pb2.PaymentStatus.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def HtlcInterceptor(
|
||||||
|
request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.stream_stream(
|
||||||
|
request_iterator,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/HtlcInterceptor",
|
||||||
|
router__pb2.ForwardHtlcInterceptResponse.SerializeToString,
|
||||||
|
router__pb2.ForwardHtlcInterceptRequest.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def UpdateChanStatus(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None,
|
||||||
|
):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
"/routerrpc.Router/UpdateChanStatus",
|
||||||
|
router__pb2.UpdateChanStatusRequest.SerializeToString,
|
||||||
|
router__pb2.UpdateChanStatusResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
)
|
|
@ -2,10 +2,11 @@ imports_ok = True
|
||||||
try:
|
try:
|
||||||
import grpc
|
import grpc
|
||||||
from google import protobuf
|
from google import protobuf
|
||||||
|
from grpc import RpcError
|
||||||
except ImportError: # pragma: nocover
|
except ImportError: # pragma: nocover
|
||||||
imports_ok = False
|
imports_ok = False
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -19,6 +20,8 @@ from .macaroon import AESCipher, load_macaroon
|
||||||
if imports_ok:
|
if imports_ok:
|
||||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
||||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
||||||
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||||
|
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
@ -111,6 +114,7 @@ class LndWallet(Wallet):
|
||||||
f"{self.endpoint}:{self.port}", composite_creds
|
f"{self.endpoint}:{self.port}", composite_creds
|
||||||
)
|
)
|
||||||
self.rpc = lnrpc.LightningStub(channel)
|
self.rpc = lnrpc.LightningStub(channel)
|
||||||
|
self.routerpc = routerrpc.RouterStub(channel)
|
||||||
|
|
||||||
def metadata_callback(self, _, callback):
|
def metadata_callback(self, _, callback):
|
||||||
callback([("macaroon", self.macaroon)], None)
|
callback([("macaroon", self.macaroon)], None)
|
||||||
|
@ -118,6 +122,8 @@ class LndWallet(Wallet):
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
|
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
|
||||||
|
except RpcError as exc:
|
||||||
|
return StatusResponse(str(exc._details), 0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return StatusResponse(str(exc), 0)
|
return StatusResponse(str(exc), 0)
|
||||||
|
|
||||||
|
@ -132,7 +138,10 @@ class LndWallet(Wallet):
|
||||||
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
||||||
|
|
||||||
if description_hash:
|
if description_hash:
|
||||||
params["description_hash"] = description_hash # as bytes directly
|
params["description_hash"] = hashlib.sha256(
|
||||||
|
description_hash
|
||||||
|
).digest() # as bytes directly
|
||||||
|
|
||||||
else:
|
else:
|
||||||
params["memo"] = memo or ""
|
params["memo"] = memo or ""
|
||||||
|
|
||||||
|
@ -148,18 +157,39 @@ class LndWallet(Wallet):
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||||
req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
|
req = router.SendPaymentRequest(
|
||||||
resp = await self.rpc.SendPaymentSync(req)
|
payment_request=bolt11,
|
||||||
|
fee_limit_msat=fee_limit_msat,
|
||||||
|
timeout_seconds=30,
|
||||||
|
no_inflight_updates=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = await self.routerpc.SendPaymentV2(req).read()
|
||||||
|
except RpcError as exc:
|
||||||
|
return PaymentResponse(False, "", 0, None, exc._details)
|
||||||
|
except Exception as exc:
|
||||||
|
return PaymentResponse(False, "", 0, None, str(exc))
|
||||||
|
|
||||||
if resp.payment_error:
|
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
||||||
return PaymentResponse(False, "", 0, None, resp.payment_error)
|
statuses = {
|
||||||
|
0: None, # NON_EXISTENT
|
||||||
|
1: None, # IN_FLIGHT
|
||||||
|
2: True, # SUCCEEDED
|
||||||
|
3: False, # FAILED
|
||||||
|
}
|
||||||
|
|
||||||
r_hash = hashlib.sha256(resp.payment_preimage).digest()
|
if resp.status in [0, 1, 3]:
|
||||||
checking_id = stringify_checking_id(r_hash)
|
fee_msat = 0
|
||||||
fee_msat = resp.payment_route.total_fees_msat
|
preimage = ""
|
||||||
preimage = resp.payment_preimage.hex()
|
checking_id = ""
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
elif resp.status == 2: # SUCCEEDED
|
||||||
|
fee_msat = resp.htlcs[-1].route.total_fees_msat
|
||||||
|
preimage = resp.payment_preimage
|
||||||
|
checking_id = resp.payment_hash
|
||||||
|
return PaymentResponse(
|
||||||
|
statuses[resp.status], checking_id, fee_msat, preimage, None
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
@ -178,20 +208,55 @@ class LndWallet(Wallet):
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
return PaymentStatus(True)
|
"""
|
||||||
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
r_hash = parse_checking_id(checking_id)
|
||||||
|
if len(r_hash) != 32:
|
||||||
|
raise binascii.Error
|
||||||
|
except binascii.Error:
|
||||||
|
# this may happen if we switch between backend wallets
|
||||||
|
# that use different checking_id formats
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
# for some reason our checking_ids are in base64 but the payment hashes
|
||||||
|
# returned here are in hex, lnd is weird
|
||||||
|
checking_id = checking_id.replace("_", "/")
|
||||||
|
checking_id = base64.b64decode(checking_id).hex()
|
||||||
|
|
||||||
|
resp = self.routerpc.TrackPaymentV2(
|
||||||
|
router.TrackPaymentRequest(payment_hash=r_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
# HTLCAttempt.HTLCStatus:
|
||||||
|
# https://github.com/lightningnetwork/lnd/blob/master/lnrpc/lightning.proto#L3641
|
||||||
|
statuses = {
|
||||||
|
0: None, # IN_FLIGHT
|
||||||
|
1: True, # "SUCCEEDED"
|
||||||
|
2: False, # "FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for payment in resp:
|
||||||
|
return PaymentStatus(statuses[payment.htlcs[-1].status])
|
||||||
|
except: # most likely the payment wasn't found
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
return PaymentStatus(None)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
request = ln.InvoiceSubscription()
|
while True:
|
||||||
try:
|
request = ln.InvoiceSubscription()
|
||||||
async for i in self.rpc.SubscribeInvoices(request):
|
try:
|
||||||
if not i.settled:
|
async for i in self.rpc.SubscribeInvoices(request):
|
||||||
continue
|
if not i.settled:
|
||||||
|
continue
|
||||||
|
|
||||||
checking_id = stringify_checking_id(i.r_hash)
|
checking_id = stringify_checking_id(i.r_hash)
|
||||||
yield checking_id
|
yield checking_id
|
||||||
except error:
|
except Exception as exc:
|
||||||
logger.error(error)
|
logger.error(
|
||||||
|
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||||
logger.error(
|
)
|
||||||
"lost connection to lnd InvoiceSubscription, please restart lnbits."
|
await asyncio.sleep(5)
|
||||||
)
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from pydoc import describe
|
from pydoc import describe
|
||||||
|
@ -75,9 +76,9 @@ class LndRestWallet(Wallet):
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"value": amount, "private": True}
|
data: Dict = {"value": amount, "private": True}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = base64.b64encode(description_hash).decode(
|
data["description_hash"] = base64.b64encode(
|
||||||
"ascii"
|
hashlib.sha256(description_hash).digest()
|
||||||
)
|
).decode("ascii")
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
@ -141,15 +142,10 @@ class LndRestWallet(Wallet):
|
||||||
return PaymentStatus(True)
|
return PaymentStatus(True)
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
"""
|
||||||
r = await client.get(
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
||||||
url=f"{self.endpoint}/v1/payments",
|
"""
|
||||||
headers=self.auth,
|
url = f"{self.endpoint}/v2/router/track/{checking_id}"
|
||||||
params={"max_payments": "20", "reversed": True},
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.is_error:
|
|
||||||
return PaymentStatus(None)
|
|
||||||
|
|
||||||
# check payment.status:
|
# check payment.status:
|
||||||
# https://api.lightning.community/rest/index.html?python#peersynctype
|
# https://api.lightning.community/rest/index.html?python#peersynctype
|
||||||
|
@ -160,14 +156,27 @@ class LndRestWallet(Wallet):
|
||||||
"FAILED": False,
|
"FAILED": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# for some reason our checking_ids are in base64 but the payment hashes
|
async with httpx.AsyncClient(
|
||||||
# returned here are in hex, lnd is weird
|
timeout=None, headers=self.auth, verify=self.cert
|
||||||
checking_id = checking_id.replace("_", "/")
|
) as client:
|
||||||
checking_id = base64.b64decode(checking_id).hex()
|
async with client.stream("GET", url) as r:
|
||||||
|
async for l in r.aiter_lines():
|
||||||
for p in r.json()["payments"]:
|
try:
|
||||||
if p["payment_hash"] == checking_id:
|
line = json.loads(l)
|
||||||
return PaymentStatus(statuses[p["status"]])
|
if line.get("error"):
|
||||||
|
logger.error(
|
||||||
|
line["error"]["message"]
|
||||||
|
if "message" in line["error"]
|
||||||
|
else line["error"]
|
||||||
|
)
|
||||||
|
return PaymentStatus(None)
|
||||||
|
payment = line.get("result")
|
||||||
|
if payment is not None and payment.get("status"):
|
||||||
|
return PaymentStatus(statuses[payment["status"]])
|
||||||
|
else:
|
||||||
|
return PaymentStatus(None)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
|
@ -190,10 +199,8 @@ class LndRestWallet(Wallet):
|
||||||
|
|
||||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||||
yield payment_hash
|
yield payment_hash
|
||||||
except (OSError, httpx.ConnectError, httpx.ReadError):
|
except Exception as exc:
|
||||||
pass
|
logger.error(
|
||||||
|
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||||
logger.error(
|
)
|
||||||
"lost connection to lnd invoices stream, retrying in 5 seconds"
|
await asyncio.sleep(5)
|
||||||
)
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -54,7 +55,7 @@ class LNPayWallet(Wallet):
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"num_satoshis": f"{amount}"}
|
data: Dict = {"num_satoshis": f"{amount}"}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
@ -54,7 +55,7 @@ class LntxbotWallet(Wallet):
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"amt": str(amount)}
|
data: Dict = {"amt": str(amount)}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ class OpenNodeWallet(Wallet):
|
||||||
json={
|
json={
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"description": memo or "",
|
"description": memo or "",
|
||||||
"callback_url": url_for("/webhook_listener", _external=True),
|
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||||
},
|
},
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -101,7 +102,7 @@ class SparkWallet(Wallet):
|
||||||
r = await self.invoicewithdescriptionhash(
|
r = await self.invoicewithdescriptionhash(
|
||||||
msatoshi=amount * 1000,
|
msatoshi=amount * 1000,
|
||||||
label=label,
|
label=label,
|
||||||
description_hash=description_hash.hex(),
|
description_hash=hashlib.sha256(description_hash).hexdigest(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
r = await self.invoice(
|
r = await self.invoice(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue