Compare commits
18 Commits
master
...
jit-backup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4dea11bc6 | ||
|
|
f06cf66e81 | ||
|
|
e6ee0d8a2f | ||
|
|
53fe9def9d | ||
|
|
12ef00719e | ||
|
|
9b7a2ff7a2 | ||
|
|
b4896685cb | ||
|
|
9c09a440e2 | ||
|
|
ec215938df | ||
|
|
a05d039a55 | ||
|
|
f7fe33a730 | ||
|
|
e02ca2ba78 | ||
|
|
ee3a83c235 | ||
|
|
4526824fb4 | ||
|
|
9953264836 | ||
|
|
c1788faefe | ||
|
|
86c699d823 | ||
|
|
a75e6b9929 |
310
.github/workflows/build-test.yml
vendored
310
.github/workflows/build-test.yml
vendored
@ -1,310 +0,0 @@
|
||||
name: 'Test & Build the app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
test-desktop:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: true
|
||||
BTCPAY_SERVER_URL: http://localhost:14142
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# Setup infrastructure
|
||||
- name: Start containers
|
||||
run: |
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" build
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
./setup.sh
|
||||
cd submodules/btcpayserver
|
||||
# Start non-HTTPS to avoid certificate errors
|
||||
nohup dotnet run -c Debug --project BTCPayServer --launch-profile Bitcoin &
|
||||
while ! curl -s -k http://localhost:14142/api/v1/health > /dev/null; do
|
||||
echo "Waiting for BTCPay Server to start..."
|
||||
sleep 10
|
||||
done
|
||||
# Run tests
|
||||
- name: Run tests
|
||||
run: |
|
||||
dotnet test -c Release -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
|
||||
build-android:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui
|
||||
- name: Clean before build
|
||||
run: |
|
||||
dotnet clean BTCPayApp.Maui/BTCPayApp.Maui.csproj
|
||||
- name: Build
|
||||
# TODO: Use proper keystore once we switch to real releases
|
||||
# https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-8.0#code-try-4
|
||||
run: |
|
||||
dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-android -c Release -o publish/android
|
||||
env:
|
||||
ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: org.btcpayserver.BTCPayApp-Signed.apk
|
||||
path: publish/android/org.btcpayserver.BTCPayApp-Signed.apk
|
||||
- name: Create pre-release
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
automatic_release_tag: "latest"
|
||||
prerelease: true
|
||||
title: "Development Build"
|
||||
files: |
|
||||
publish/android/org.btcpayserver.BTCPayApp-Signed.apk
|
||||
|
||||
# build-desktop-win:
|
||||
# runs-on: windows-latest
|
||||
# steps:
|
||||
# # Setup code, .NET and Android
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Build win x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x64 -c Debug -o publish/win-x64
|
||||
# - name: Build win x86
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x86 -c Debug -o publish/win-x86
|
||||
# - name: Build win-arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-arm64 -c Debug -o publish/win-arm64
|
||||
# - name: Upload artifact win
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: windows build
|
||||
# path: |
|
||||
# publish/win-x64
|
||||
# publish/win-x86
|
||||
# publish/win-arm64
|
||||
|
||||
# build-desktop-linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# # Setup code, .NET and Android
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Build linux x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-x64 -c Debug -o publish/linux-x64
|
||||
# - name: Build linux arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-arm64 -c Debug -o publish/linux-arm64
|
||||
# - name: Upload artifact linux
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: linux build
|
||||
# path: |
|
||||
# publish/linux-x64
|
||||
# publish/linux-arm64
|
||||
|
||||
# build-desktop-mac:
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# # Checkout the code
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# # Import code-signing certificates
|
||||
# - name: Import Code-Signing Certificates
|
||||
# uses: apple-actions/import-codesign-certs@v3
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
|
||||
# # Verify imported certificates and extract Developer ID
|
||||
# - name: Find Code Signing Certificate
|
||||
# id: find-cert
|
||||
# run: |
|
||||
# CERT_NAME=$(security find-identity -v -p codesigning | grep "Apple Distribution" | awk -F '"Apple Distribution: ' '{print $2}' | awk -F '"' '{print $1}')
|
||||
# if [ -z "$CERT_NAME" ]; then
|
||||
# echo "No valid Apple Distribution certificate found!"
|
||||
# exit 1
|
||||
# fi
|
||||
# echo "Certificate Name: Apple Distribution: $CERT_NAME"
|
||||
# echo "CERT_NAME=Apple Distribution: $CERT_NAME" >> $GITHUB_ENV
|
||||
# # Setup .NET
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# # Build the app for macOS architectures
|
||||
# - name: Build mac x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-x64 -o publish/osx-x64
|
||||
# - name: Build mac arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-arm64 -o publish/osx-arm64
|
||||
# # Create the .app bundle
|
||||
# - name: Create .app bundle for x64
|
||||
# run: |
|
||||
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/MacOS
|
||||
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/Resources
|
||||
# ls -lA publish/osx-x64/
|
||||
# cp -R publish/osx-x64/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# #cp -R publish/osx-x64/* dist/osx-x64/BTCPayApp.app/Contents/MacOS/
|
||||
# #mv dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
# <plist version=\"1.0\">
|
||||
# <dict>
|
||||
# <key>CFBundleExecutable</key>
|
||||
# <string>BTCPayApp</string>
|
||||
# <key>CFBundleIdentifier</key>
|
||||
# <string>org.btcpayserver.app</string>
|
||||
# <key>CFBundleName</key>
|
||||
# <string>BTCPay App</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>1.0</string>
|
||||
# <key>CFBundlePackageType</key>
|
||||
# <string>APPL</string>
|
||||
# </dict>
|
||||
# </plist>" > dist/osx-x64/BTCPayApp.app/Contents/Info.plist
|
||||
# cat dist/osx-x64/BTCPayApp.app/Contents/Info.plist
|
||||
# ls -lA dist/osx-x64/BTCPayApp.app/Contents/**
|
||||
# - name: Create .app bundle for arm64
|
||||
# run: |
|
||||
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/MacOS
|
||||
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/Resources
|
||||
# ls -lA publish/osx-arm64/
|
||||
# cp -R publish/osx-arm64/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# #cp -R publish/osx-arm64/* dist/osx-arm64/BTCPayApp.app/Contents/MacOS/
|
||||
# #mv dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
# <plist version=\"1.0\">
|
||||
# <dict>
|
||||
# <key>CFBundleExecutable</key>
|
||||
# <string>BTCPayApp</string>
|
||||
# <key>CFBundleIdentifier</key>
|
||||
# <string>org.btcpayserver.app</string>
|
||||
# <key>CFBundleName</key>
|
||||
# <string>BTCPay App</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>1.0</string>
|
||||
# <key>CFBundlePackageType</key>
|
||||
# <string>APPL</string>
|
||||
# </dict>
|
||||
# </plist>" > dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
|
||||
# cat dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
|
||||
# ls -lA dist/osx-arm64/BTCPayApp.app/Contents/**
|
||||
# # Sign the .app bundles using the dynamic certificate name
|
||||
# - name: Sign x64 app bundle
|
||||
# run: |
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-x64/BTCPayApp.app
|
||||
# codesign --verify --deep --strict dist/osx-x64/BTCPayApp.app
|
||||
# - name: Sign arm64 app bundle
|
||||
# run: |
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-arm64/BTCPayApp.app
|
||||
# codesign --verify --deep --strict dist/osx-arm64/BTCPayApp.app
|
||||
# # Verify app bundle signing
|
||||
# - name: Verify x64 app bundle signing
|
||||
# run: spctl --assess --type execute dist/osx-x64/BTCPayApp.app
|
||||
# continue-on-error: true
|
||||
# - name: Verify arm64 app bundle signing
|
||||
# run: spctl --assess --type execute dist/osx-arm64/BTCPayApp.app
|
||||
# continue-on-error: true
|
||||
# # Create DMG files
|
||||
# - name: Create DMG for x64
|
||||
# run: |
|
||||
# mkdir -p dmg
|
||||
# hdiutil create -size 1gb -volname "BTCPayApp-osx-x64" -srcfolder "dist/osx-x64" -ov -format UDZO dmg/BTCPayApp-x64.dmg
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-x64.dmg
|
||||
# codesign --verify --deep --strict dmg/BTCPayApp-x64.dmg
|
||||
# - name: Create DMG for arm64
|
||||
# run: |
|
||||
# mkdir -p dmg
|
||||
# hdiutil create -size 1gb -volname "BTCPayApp-osx-arm64" -srcfolder "dist/osx-arm64" -ov -format UDZO dmg/BTCPayApp-arm64.dmg
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-arm64.dmg
|
||||
# codesign --verify --deep --strict dmg/BTCPayApp-arm64.dmg
|
||||
# # Verify DMG signing
|
||||
# - name: Verify x64 DMG signing
|
||||
# run: spctl --assess --type execute dmg/BTCPayApp-x64.dmg
|
||||
# continue-on-error: true
|
||||
# - name: Verify arm64 DMG signing
|
||||
# run: spctl --assess --type execute dmg/BTCPayApp-arm64.dmg
|
||||
# continue-on-error: true
|
||||
# # Upload artifacts
|
||||
# - name: Upload DMG artifacts
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: mac-dmg
|
||||
# path: dmg
|
||||
#
|
||||
# build-ios:
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup Xcode version
|
||||
# uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
# with:
|
||||
# xcode-version: 16.0
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Install workloads
|
||||
# run: dotnet workload install maui
|
||||
# - name: Import Code-Signing Certificates
|
||||
# uses: Apple-Actions/import-codesign-certs@v1
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
|
||||
# - name: Download Apple Provisioning Profiles
|
||||
# uses: Apple-Actions/download-provisioning-profiles@v1
|
||||
# with:
|
||||
# bundle-id: ${{ secrets.APPLE_BUNDLE_ID }}
|
||||
# issuer-id: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
# api-key-id: ${{ secrets.APPLE_KEY_ID }}
|
||||
# api-private-key: ${{ secrets.APPLE_KEY }}
|
||||
# - name: Build
|
||||
# run: dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-ios -c Debug -o publish/ios
|
||||
# - name: Upload artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: ios build
|
||||
# path: |
|
||||
# publish/ios
|
||||
51
.github/workflows/desktop.yml
vendored
Normal file
51
.github/workflows/desktop.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: 'Test the desktop app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# E2E tests
|
||||
- name: Start containers
|
||||
run: docker-compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
cd submodules/btcpayserver
|
||||
nohup dotnet run -c Release --project BTCPayServer &
|
||||
while ! curl -s http://localhost:14142/api/v1/health > /dev/null; do
|
||||
echo "Waiting for BTCPay Server to start..."
|
||||
sleep 10
|
||||
done
|
||||
# Unit and integration tests
|
||||
- name: Run fast tests
|
||||
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Fast=Fast" BTCPayApp.Tests
|
||||
- name: Run integration tests
|
||||
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Integration=Integration" BTCPayApp.Tests
|
||||
- name: Run Selenium tests
|
||||
run: dotnet test --filter "Selenium=Selenium" -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
51
.github/workflows/maui.yml
vendored
Normal file
51
.github/workflows/maui.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: 'Test the mobile app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '22'
|
||||
distribution: 'temurin'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
# Restore and build
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
# Unit and integration tests
|
||||
- name: Run fast tests
|
||||
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Fast=Fast"
|
||||
- name: Run integration tests
|
||||
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Integration=Integration"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
.idea
|
||||
**/bin
|
||||
**/obj
|
||||
**/tmp
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@ -39,8 +38,6 @@ bld/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="App-Server 2nd Instance" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https-second-instance" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="App-Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="mutinynet" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,18 +0,0 @@
|
||||
·<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="BTCPayApp.Maui" type="XamarinAndroidProject" factoryName="Xamarin.Android">
|
||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="DEPLOY_BEHAVIOUR_NAME" value="Default" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="false" name="BTCPayApp.Maui" type="XamarinIOSProject" factoryName="Xamarin.iOS">
|
||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="IS_PASS_PARENT_ENVS" value="false" />
|
||||
<option name="EXTRA_MLAUNCH_PARAMETERS" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,9 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL WITH SECOND APP" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="App-Server 2nd Instance" type="LaunchSettings" />
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,8 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL testnet" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="Server Testnet" type="LaunchSettings" />
|
||||
<toRun name="Docker compose testnet" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Server" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ANDROID" type="CompoundRunConfigurationType">
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Docker compose mutiny" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="services">
|
||||
<list>
|
||||
<option value="dev" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.mutinynet.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Docker compose testnet" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="services">
|
||||
<list>
|
||||
<option value="dev" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.testnet.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-MUTINYNET" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,19 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Regtest" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" default="false" projectName="BTCPayApp.Core" projectPath="$PROJECT_DIR$/BTCPayApp.Core/BTCPayApp.Core.csproj" />
|
||||
<option name="Build" default="false" projectName="BTCPayServer.Plugins.App" projectPath="$PROJECT_DIR$/BTCPayServer.Plugins.App/BTCPayServer.Plugins.App.csproj" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,17 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Testnet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-TESTNET" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,26 +0,0 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory) : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
|
||||
if (pendingMigrationsAsync.Length != 0)
|
||||
{
|
||||
logger.LogInformation("Applying {Length} migrations", pendingMigrationsAsync.Length);
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
logger.LogInformation("Migrations applied: {Migrations}", string.Join(", ", pendingMigrationsAsync));
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
45
BTCPayApp.Core/AspNetRip/AccessTokenResponse.cs
Normal file
45
BTCPayApp.Core/AspNetRip/AccessTokenResponse.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON data transfer object for the bearer token response typically found in "/login" and "/refresh" responses.
|
||||
/// </summary>
|
||||
public sealed class AccessTokenResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The value is always "Bearer" which indicates this response provides a "Bearer" token
|
||||
/// in the form of an opaque <see cref="AccessToken"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "tokenType": "Bearer" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public string TokenType { get; } = "Bearer";
|
||||
|
||||
/// <summary>
|
||||
/// The opaque bearer token to send as part of the Authorization request header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "accessToken": "{AccessToken}" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required string AccessToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds before the <see cref="AccessToken"/> expires.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "expiresIn": "{ExpiresInSeconds}" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required long ExpiresIn { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this provides the ability to get a new access_token after it expires using a refresh endpoint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "refreshToken": "{RefreshToken}" using using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required string RefreshToken { get; init; }
|
||||
}
|
||||
29
BTCPayApp.Core/AspNetRip/LoginRequest.cs
Normal file
29
BTCPayApp.Core/AspNetRip/LoginRequest.cs
Normal file
@ -0,0 +1,29 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The request type for the "/login" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// </summary>
|
||||
public sealed class LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's email address which acts as a user name.
|
||||
/// </summary>
|
||||
public required string Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's password.
|
||||
/// </summary>
|
||||
public required string Password { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The optional two-factor authenticator code. This may be required for users who have enabled two-factor authentication.
|
||||
/// This is not required if a <see cref="TwoFactorRecoveryCode"/> is sent.
|
||||
/// </summary>
|
||||
public string? TwoFactorCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional two-factor recovery code from <see cref="TwoFactorResponse.RecoveryCodes"/>.
|
||||
/// This is required for users who have enabled two-factor authentication but lost access to their <see cref="TwoFactorCode"/>.
|
||||
/// </summary>
|
||||
public string? TwoFactorRecoveryCode { get; init; }
|
||||
}
|
||||
71
BTCPayApp.Core/AspNetRip/ProblemDetails.cs
Normal file
71
BTCPayApp.Core/AspNetRip/ProblemDetails.cs
Normal file
@ -0,0 +1,71 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
|
||||
/// </summary>
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
|
||||
/// dereferenced, it provide human-readable documentation for the problem type
|
||||
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
|
||||
/// "about:blank".
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-5)]
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
|
||||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||
/// see[RFC7231], Section 3.4).
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-4)]
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-3)]
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable explanation specific to this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-2)]
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-1)]
|
||||
[JsonPropertyName("instance")]
|
||||
public string? Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
|
||||
/// <para>
|
||||
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
|
||||
/// other members of a problem type.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
|
||||
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
}
|
||||
13
BTCPayApp.Core/AspNetRip/RefreshRequest.cs
Normal file
13
BTCPayApp.Core/AspNetRip/RefreshRequest.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The request type for the "/refresh" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// </summary>
|
||||
public sealed class RefreshRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="AccessTokenResponse.RefreshToken"/> from the last "/login" or "/refresh" response used to get a new <see cref="AccessTokenResponse"/>
|
||||
/// with an extended expiration.
|
||||
/// </summary>
|
||||
public required string RefreshToken { get; init; }
|
||||
}
|
||||
23
BTCPayApp.Core/AspNetRip/ResetPasswordRequest.cs
Normal file
23
BTCPayApp.Core/AspNetRip/ResetPasswordRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The response type for the "/resetPassword" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// The "/resetPassword" endpoint requires the "/forgotPassword" endpoint to be called first to get the <see cref="ResetCode"/>.
|
||||
/// </summary>
|
||||
public sealed class ResetPasswordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The email address for the user requesting a password reset. This should match <see cref="ForgotPasswordRequest.Email"/>.
|
||||
/// </summary>
|
||||
public required string Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The code sent to the user's email to reset the password. To get the reset code, first make a "/forgotPassword" request.
|
||||
/// </summary>
|
||||
public required string ResetCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The new password the user with the given <see cref="Email"/> should login with. This will replace the previous password.
|
||||
/// </summary>
|
||||
public required string NewPassword { get; init; }
|
||||
}
|
||||
46
BTCPayApp.Core/Attempt2/AppToServerHelper.cs
Normal file
46
BTCPayApp.Core/Attempt2/AppToServerHelper.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public static class AppToServerHelper
|
||||
{
|
||||
|
||||
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = lightningPayment.PaymentHash.ToString(),
|
||||
Amount = lightningPayment.Value,
|
||||
PaymentHash = lightningPayment.PaymentHash.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
PaidAt = lightningPayment.Status == LightningPaymentStatus.Complete? DateTimeOffset.UtcNow: null, //TODO: store these in ln payment
|
||||
BOLT11 = lightningPayment.PaymentRequest.ToString(),
|
||||
Status = lightningPayment.Status == LightningPaymentStatus.Complete? LightningInvoiceStatus.Paid: lightningPayment.PaymentRequest.ExpiryDate < DateTimeOffset.UtcNow? LightningInvoiceStatus.Expired: LightningInvoiceStatus.Unpaid
|
||||
};
|
||||
}
|
||||
|
||||
public static LightningPayment ToPayment(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningPayment()
|
||||
{
|
||||
Id = lightningPayment.PaymentHash.ToString(),
|
||||
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
|
||||
PaymentHash = lightningPayment.PaymentHash.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
BOLT11 = lightningPayment.PaymentRequest.ToString(),
|
||||
Status = lightningPayment.Status
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<List<LightningPayment>> ToPayments(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToPayment).ToList();
|
||||
}
|
||||
public static async Task<List<LightningInvoice>> ToInvoices(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToInvoice).ToList();
|
||||
}
|
||||
}
|
||||
117
BTCPayApp.Core/Attempt2/BTCPayAppServerClient.cs
Normal file
117
BTCPayApp.Core/Attempt2/BTCPayAppServerClient.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System.Text;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServiceProvider _serviceProvider) : IBTCPayAppHubClient
|
||||
{
|
||||
public event AsyncEventHandler<string>? OnNewBlock;
|
||||
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
|
||||
public event AsyncEventHandler<string>? OnNotifyNetwork;
|
||||
public event AsyncEventHandler<string>? OnServerNodeInfo;
|
||||
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
|
||||
|
||||
public async Task NotifyServerEvent(ServerEvent ev)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerEvent: {ev}", ev);
|
||||
await OnNotifyServerEvent?.Invoke(this, ev);
|
||||
}
|
||||
|
||||
public async Task NotifyNetwork(string network)
|
||||
{
|
||||
_logger.LogInformation("NotifyNetwork: {network}", network);
|
||||
await OnNotifyNetwork?.Invoke(this, network);
|
||||
}
|
||||
|
||||
public async Task NotifyServerNode(string nodeInfo)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerNode: {nodeInfo}", nodeInfo);
|
||||
await OnServerNodeInfo?.Invoke(this, nodeInfo);
|
||||
}
|
||||
|
||||
public async Task TransactionDetected(TransactionDetectedRequest request)
|
||||
{
|
||||
_logger.LogInformation($"OnTransactionDetected: {request.TxId}");
|
||||
await OnTransactionDetected?.Invoke(this, request);
|
||||
}
|
||||
|
||||
public async Task NewBlock(string block)
|
||||
{
|
||||
_logger.LogInformation("NewBlock: {block}", block);
|
||||
await OnNewBlock?.Invoke(this, block);
|
||||
}
|
||||
|
||||
private PaymentsManager PaymentsManager =>
|
||||
_serviceProvider.GetRequiredService<LightningNodeManager>().Node.PaymentsManager;
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest)
|
||||
{
|
||||
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
|
||||
false);
|
||||
return (await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
|
||||
createLightningInvoiceRequest.Expiry, descHash)).ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice?> GetLightningInvoice(uint256 paymentHash)
|
||||
{
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invs.FirstOrDefault()?.ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningPayment(uint256 paymentHash)
|
||||
{
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invs.FirstOrDefault()?.ToPayment();
|
||||
}
|
||||
|
||||
public async Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request)
|
||||
{
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default).ToPayments();
|
||||
}
|
||||
|
||||
public async Task<List<LightningInvoice>> GetLightningInvoices(ListInvoicesParams request)
|
||||
{
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default).ToInvoices();
|
||||
}
|
||||
|
||||
public async Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi)
|
||||
{
|
||||
var network = _serviceProvider.GetRequiredService<OnChainWalletManager>().Network;
|
||||
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
|
||||
try
|
||||
{
|
||||
var result = await PaymentsManager.PayInvoice(bolt,
|
||||
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
|
||||
return new PayResponse()
|
||||
{
|
||||
Result = result.Status switch
|
||||
{
|
||||
LightningPaymentStatus.Unknown => PayResult.Unknown,
|
||||
LightningPaymentStatus.Pending => PayResult.Unknown,
|
||||
LightningPaymentStatus.Complete => PayResult.Ok,
|
||||
LightningPaymentStatus.Failed => PayResult.Error,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Details = new PayDetails()
|
||||
{
|
||||
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
|
||||
Status = result.Status
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error paying invoice");
|
||||
return new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
254
BTCPayApp.Core/Attempt2/BTCPayConnectionManager.cs
Normal file
254
BTCPayApp.Core/Attempt2/BTCPayConnectionManager.cs
Normal file
@ -0,0 +1,254 @@
|
||||
using System.Net;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
|
||||
{
|
||||
private const string ConfigDeviceIdentifierKey = "deviceIdentifier";
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly AuthenticationStateProvider _authStateProvider;
|
||||
private readonly ILogger<BTCPayConnectionManager> _logger;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly IBTCPayAppHubClient _btcPayAppServerClientInterface;
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly SyncService _syncService;
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public IBTCPayAppHubServer? HubProxy { get; private set; }
|
||||
private HubConnection? Connection { get; set; }
|
||||
public Network? ReportedNetwork { get; private set; }
|
||||
|
||||
public string ReportedNodeInfo { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
|
||||
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
|
||||
|
||||
public BTCPayConnectionState ConnectionState
|
||||
{
|
||||
get => _connectionState;
|
||||
private set
|
||||
{
|
||||
if (_connectionState == value)
|
||||
return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
_logger.LogInformation("Connection state changed: {State}", _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayConnectionManager(
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface,
|
||||
IConfigProvider configProvider,
|
||||
SyncService syncService)
|
||||
{
|
||||
_accountManager = accountManager;
|
||||
_authStateProvider = authStateProvider;
|
||||
_logger = logger;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_btcPayAppServerClientInterface = btcPayAppServerClientInterface;
|
||||
_configProvider = configProvider;
|
||||
_syncService = syncService;
|
||||
}
|
||||
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ConnectionChanged += OnConnectionChanged;
|
||||
_authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
_btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
|
||||
_btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
|
||||
await OnConnectionChanged(this, (BTCPayConnectionState.Init, BTCPayConnectionState.Init));
|
||||
}
|
||||
|
||||
private async Task<long> GetDeviceIdentifier()
|
||||
{
|
||||
return await _configProvider.GetOrSet(ConfigDeviceIdentifierKey,
|
||||
async () => RandomUtils.GetInt64(), false);
|
||||
}
|
||||
|
||||
|
||||
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
var account = _accountManager.GetAccount();
|
||||
switch (e.New)
|
||||
{
|
||||
case BTCPayConnectionState.Init:
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
case BTCPayConnectionState.WaitingForAuth:
|
||||
|
||||
await Kill();
|
||||
if (account is not null)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
|
||||
break;
|
||||
case BTCPayConnectionState.Connecting:
|
||||
if (account is null)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Connection is null)
|
||||
{
|
||||
Connection = new HubConnectionBuilder()
|
||||
.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(
|
||||
options.PayloadSerializerSettings);
|
||||
})
|
||||
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(),
|
||||
options =>
|
||||
{
|
||||
options.AccessTokenProvider = () =>
|
||||
Task.FromResult(_accountManager.GetAccount()?.AccessToken);
|
||||
})
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_subscription = Connection.Register(_btcPayAppServerClientInterface);
|
||||
HubProxy = Connection.CreateHubProxy<IBTCPayAppHubServer>();
|
||||
}
|
||||
|
||||
if (Connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Connection.StartAsync();
|
||||
if (Connection.State == HubConnectionState.Connected)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await _accountManager.RefreshAccess();
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case BTCPayConnectionState.Syncing:
|
||||
await _syncService.SyncToLocal();
|
||||
ConnectionState = BTCPayConnectionState.ConnectedFinishedInitialSync;
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedFinishedInitialSync:
|
||||
var deviceIdentifier = await GetDeviceIdentifier();
|
||||
var master = await HubProxy.DeviceMasterSignal(deviceIdentifier, true);
|
||||
ConnectionState =
|
||||
master ? BTCPayConnectionState.ConnectedAsMaster : BTCPayConnectionState.ConnectedAsSlave;
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsMaster:
|
||||
await _syncService.StartSync(false, await GetDeviceIdentifier());
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSlave:
|
||||
await _syncService.StartSync(true, await GetDeviceIdentifier());
|
||||
break;
|
||||
case BTCPayConnectionState.Disconnected:
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task OnServerNodeInfo(object? sender, string e)
|
||||
{
|
||||
ReportedNodeInfo = e;
|
||||
}
|
||||
|
||||
private async Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
_logger.LogInformation("OnNotifyServerEvent: {Type} - {Details}", e.Type, e.ToString());
|
||||
}
|
||||
|
||||
private async Task OnNotifyNetwork(object? sender, string e)
|
||||
{
|
||||
ReportedNetwork = Network.GetNetwork(e);
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
var authenticated = await _accountManager.CheckAuthenticated();
|
||||
await Kill();
|
||||
ConnectionState = !authenticated ? BTCPayConnectionState.WaitingForAuth : BTCPayConnectionState.Connecting;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while handling authentication state change");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task Kill()
|
||||
{
|
||||
var conn = Connection;
|
||||
Connection = null;
|
||||
if (conn is not null)
|
||||
await conn.StopAsync();
|
||||
_subscription?.Dispose();
|
||||
_subscription = null;
|
||||
HubProxy = null;
|
||||
await _syncService.StopSync();
|
||||
}
|
||||
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsMaster)
|
||||
{
|
||||
_logger.LogInformation("Sending device master signal to turn off");
|
||||
var deviceIdentifier = await GetDeviceIdentifier();
|
||||
await HubProxy.DeviceMasterSignal(deviceIdentifier, true);
|
||||
}
|
||||
|
||||
await Kill();
|
||||
_authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
ConnectionChanged -= OnConnectionChanged;
|
||||
}
|
||||
|
||||
public Task OnClosed(Exception? exception)
|
||||
{
|
||||
_logger.LogError(exception, "Hub connection closed");
|
||||
if (Connection?.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnReconnected(string? connectionId)
|
||||
{
|
||||
_logger.LogInformation("Hub connection reconnected");
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
|
||||
public async Task OnReconnecting(Exception? exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Hub connection reconnecting");
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
}
|
||||
13
BTCPayApp.Core/Attempt2/BTCPayConnectionState.cs
Normal file
13
BTCPayApp.Core/Attempt2/BTCPayConnectionState.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
WaitingForAuth,
|
||||
Connecting,
|
||||
Syncing,
|
||||
Disconnected,
|
||||
ConnectedAsMaster,
|
||||
ConnectedAsSlave,
|
||||
ConnectedFinishedInitialSync
|
||||
}
|
||||
42
BTCPayApp.Core/Attempt2/BTCPayPaymentsNotifier.cs
Normal file
42
BTCPayApp.Core/Attempt2/BTCPayPaymentsNotifier.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayPaymentsNotifier : IScopedHostedService
|
||||
{
|
||||
private readonly PaymentsManager _paymentsManager;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager, BTCPayConnectionManager connectionManager,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_paymentsManager = paymentsManager;
|
||||
_connectionManager = connectionManager;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
}
|
||||
|
||||
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
|
||||
{
|
||||
await _connectionManager.HubProxy
|
||||
.SendInvoiceUpdate(
|
||||
_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier, e.ToInvoice())
|
||||
.RunSync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
}
|
||||
}
|
||||
32
BTCPayApp.Core/Attempt2/ConfigHelpers.cs
Normal file
32
BTCPayApp.Core/Attempt2/ConfigHelpers.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public static class ConfigHelpers
|
||||
{
|
||||
public static async Task<T> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key,
|
||||
Func<Task<T>> factory)
|
||||
{
|
||||
var value = await secureConfigProvider.Get<T>(key);
|
||||
if (Equals(value, default(T)))
|
||||
{
|
||||
value = await factory();
|
||||
await secureConfigProvider.Set(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static async Task<T> GetOrSet<T>(this IConfigProvider configProvider, string key, Func<Task<T>> factory,
|
||||
bool backup)
|
||||
{
|
||||
var value = await configProvider.Get<T>(key);
|
||||
if (Equals(value, default(T)))
|
||||
{
|
||||
value = await factory();
|
||||
await configProvider.Set(key, value, backup);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
18
BTCPayApp.Core/Attempt2/LDKChangeDestinationSource.cs
Normal file
18
BTCPayApp.Core/Attempt2/LDKChangeDestinationSource.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LDKChangeDestinationSource:ChangeDestinationSourceInterface
|
||||
{
|
||||
private readonly LightningNodeManager _lightningNodeManager;
|
||||
|
||||
public LDKChangeDestinationSource( LightningNodeManager lightningNodeManager)
|
||||
{
|
||||
_lightningNodeManager = lightningNodeManager;
|
||||
}
|
||||
public Result_CVec_u8ZNoneZ get_change_destination_script()
|
||||
{
|
||||
var s = _lightningNodeManager.Node.DeriveScript().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_u8ZNoneZ.ok(s.ToBytes());
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,17 @@
|
||||
using org.ldk.enums;
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LDKKVStore(ConfigProvider configProvider) : KVStoreInterface
|
||||
public class LDKKVStore:KVStoreInterface
|
||||
{
|
||||
private readonly IConfigProvider _configProvider;
|
||||
|
||||
public LDKKVStore(IConfigProvider configProvider)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
private string CombineKey(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var str = "ln:";
|
||||
@ -28,28 +35,28 @@ public class LDKKVStore(ConfigProvider configProvider) : KVStoreInterface
|
||||
public Result_CVec_u8ZIOErrorZ read(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
var result = configProvider.Get<byte[]>(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var result = _configProvider.Get<byte[]>(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return result == null ? Result_CVec_u8ZIOErrorZ.err(IOError.LDKIOError_NotFound) : Result_CVec_u8ZIOErrorZ.ok(result);
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ write(string primary_namespace, string secondary_namespace, string key, byte[] buf)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
configProvider.Set(key1, buf, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_configProvider.Set(key1, buf, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ remove(string primary_namespace, string secondary_namespace, string key, bool lazy)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
configProvider.Set<byte[]>(key1, null, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_configProvider.Set<byte[]>(key1, null, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_CVec_StrZIOErrorZ list(string primary_namespace, string secondary_namespace)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, string.Empty);
|
||||
var result = configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var result = _configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_StrZIOErrorZ.ok(result.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
401
BTCPayApp.Core/Attempt2/LDKNode.cs
Normal file
401
BTCPayApp.Core/Attempt2/LDKNode.cs
Normal file
@ -0,0 +1,401 @@
|
||||
using System.Collections.Specialized;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.LSP.JIT;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using org.ldk.structs;
|
||||
using OutPoint = NBitcoin.OutPoint;
|
||||
using UInt128 = org.ldk.util.UInt128;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public partial class LDKNode:
|
||||
ILDKEventHandler<Event.Event_ChannelClosed>,
|
||||
ILDKEventHandler<Event.Event_ChannelPending>,
|
||||
ILDKEventHandler<Event.Event_ChannelReady>
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<ChannelDetails[]> GetChannels(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(nameof(GetChannels), async entry =>
|
||||
{
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
return ServiceProvider.GetRequiredService<ChannelManager>().list_channels();
|
||||
}).WithCancellation(cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task Handle(Event.Event_ChannelClosed evt)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelPending @event)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelReady @event)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task<PeerDetails[]> GetPeers(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
return await _memoryCache.GetOrCreateAsync(nameof(GetPeers), async entry =>
|
||||
{
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
return ServiceProvider.GetRequiredService<PeerManager>().list_peers();
|
||||
}).WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public void PeersChanged()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
}
|
||||
|
||||
private void InvalidateCache()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task<Result_ChannelIdAPIErrorZ> OpenChannel(Money amount, PubKey nodeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
|
||||
var channelManager = ServiceProvider.GetRequiredService<ChannelManager>();
|
||||
var entropySource = ServiceProvider.GetRequiredService<EntropySource>();
|
||||
var userConfig = ServiceProvider.GetRequiredService<UserConfig>();
|
||||
|
||||
|
||||
var temporaryChannelId = ChannelId.temporary_from_entropy_source(entropySource);
|
||||
|
||||
|
||||
var userChannelId = new UInt128(temporaryChannelId.get_a().Take(16).ToArray());
|
||||
try
|
||||
{
|
||||
return await Task.Run(() => channelManager.create_channel(nodeId.ToBytes(), amount.Satoshi, 0, userChannelId,
|
||||
temporaryChannelId, userConfig), cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
_logger.LogInformation("finished (trying to) opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<IJITService?> GetJITLSPService()
|
||||
{
|
||||
var config = await GetConfig();
|
||||
var lsp = config.JITLSP;
|
||||
if(lsp is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var jits = ServiceProvider.GetServices<IJITService>();
|
||||
return jits.FirstOrDefault(jit => jit.ProviderName == lsp);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKNode(
|
||||
IMemoryCache cache,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
BTCPayConnectionManager connectionManager,
|
||||
IServiceProvider serviceProvider,
|
||||
LDKWalletLogger logger,
|
||||
IConfigProvider configProvider,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_memoryCache = cache;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_connectionManager = connectionManager;
|
||||
_logger = logger;
|
||||
_configProvider = configProvider;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private TaskCompletionSource? _started;
|
||||
private readonly SemaphoreSlim _semaphore = new(1);
|
||||
|
||||
public Network Network => ServiceProvider.GetRequiredService<Network>();
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
bool exists;
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
exists = _started is not null;
|
||||
_started ??= new TaskCompletionSource();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
if (exists)
|
||||
{
|
||||
await _started.Task;
|
||||
return;
|
||||
}
|
||||
InvalidateCache();
|
||||
_config = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key)?? new LightningConfig();
|
||||
_configLoaded.SetResult();
|
||||
var keyPath = KeyPath.Parse(_config.LightningDerivationPath);
|
||||
Seed = new Mnemonic( _onChainWalletManager.WalletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
|
||||
_logger.LogInformation("Starting LDKNode services");
|
||||
var bb = await _onChainWalletManager.GetBestBlock();
|
||||
if (bb is null)
|
||||
{
|
||||
throw new InvalidOperationException("Best block could not be retrieved. Killing the startup");
|
||||
}
|
||||
foreach (var service in services)
|
||||
{
|
||||
_logger.LogInformation($"Starting {service.GetType().Name}");
|
||||
await service.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_started.SetResult();
|
||||
_logger.LogInformation("LDKNode started");
|
||||
}
|
||||
|
||||
private readonly TaskCompletionSource _configLoaded = new();
|
||||
|
||||
public async Task<LightningConfig> GetConfig()
|
||||
{
|
||||
await _configLoaded.Task;
|
||||
return _config!;
|
||||
}
|
||||
public async Task<string[]> GetJITLSPs()
|
||||
{
|
||||
return ServiceProvider.GetServices<IJITService>().Select(jit => jit.ProviderName).ToArray();
|
||||
}
|
||||
|
||||
public async Task UpdateConfig(LightningConfig config)
|
||||
{
|
||||
await _started.Task;
|
||||
await _configProvider.Set(LightningConfig.Key, config, true);
|
||||
_config = config;
|
||||
|
||||
ConfigUpdated?.Invoke(this, config);
|
||||
}
|
||||
|
||||
|
||||
public AsyncEventHandler<LightningConfig>? ConfigUpdated;
|
||||
|
||||
public byte[] Seed { get; private set; }
|
||||
|
||||
public PaymentsManager PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
|
||||
public LDKPeerHandler PeerHandler => ServiceProvider.GetRequiredService<LDKPeerHandler>();
|
||||
|
||||
public PubKey NodeId => new(ServiceProvider.GetRequiredService<ChannelManager>().get_our_node_id());
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
bool exists;
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
exists = _started is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
if (!exists)
|
||||
return;
|
||||
// var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
|
||||
|
||||
|
||||
_logger.LogInformation("Stopping LDKNode services");
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
var tasks = services.Select(async service =>
|
||||
{
|
||||
_logger.LogInformation($"Stopping {service.GetType().Name}");
|
||||
await service.StopAsync(cancellationToken);
|
||||
_logger.LogInformation($"Stopped {service.GetType().Name}");
|
||||
}).ToArray();
|
||||
await Task.WhenAll(tasks);
|
||||
// _ = _connectionManager.HubProxy.DeviceMasterSignal(identifier, false).RunSync();
|
||||
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeAsync().GetAwaiter().GetResult();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// await StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private readonly TaskCompletionSource<ChannelMonitor[]?> icm = new();
|
||||
private LightningConfig? _config;
|
||||
|
||||
public async Task<ChannelMonitor[]> GetInitialChannelMonitors()
|
||||
{
|
||||
return await icm.Task;
|
||||
}
|
||||
private async Task<ChannelMonitor[]> GetInitialChannelMonitors(EntropySource entropySource,
|
||||
SignerProvider signerProvider)
|
||||
{
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var data = await db.LightningChannels.Select(channel => channel.Data)
|
||||
.ToArrayAsync();
|
||||
|
||||
var channels = ChannelManagerHelper.GetInitialMonitors(data, entropySource, signerProvider);
|
||||
icm.SetResult(channels);
|
||||
return channels;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<byte[]?> GetRawChannelManager()
|
||||
{
|
||||
return await _configProvider.Get<byte[]>("ln:ChannelManager") ?? null;
|
||||
}
|
||||
|
||||
public async Task UpdateChannelManager(ChannelManager serializedChannelManager)
|
||||
{
|
||||
await _configProvider.Set("ln:ChannelManager", serializedChannelManager.write(), true);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
|
||||
{
|
||||
await _configProvider.Set("ln:NetworkGraph", networkGraph.write(), true);
|
||||
}
|
||||
|
||||
public async Task UpdateScore(WriteableScore score)
|
||||
{
|
||||
await _configProvider.Set("ln:Score", score.write(), true);
|
||||
}
|
||||
|
||||
|
||||
public async Task<(byte[] serializedChannelManager, ChannelMonitor[] channelMonitors)?> GetSerializedChannelManager(
|
||||
EntropySource entropySource, SignerProvider signerProvider)
|
||||
{
|
||||
|
||||
var data = await GetRawChannelManager();
|
||||
if (data is null)
|
||||
{
|
||||
icm.SetResult(Array.Empty<ChannelMonitor>());
|
||||
return null;
|
||||
}
|
||||
|
||||
var channels = await GetInitialChannelMonitors(entropySource, signerProvider);
|
||||
return (data, channels);
|
||||
}
|
||||
|
||||
public async Task<Script> DeriveScript()
|
||||
{
|
||||
var derivationKey = (await GetConfig()).ScriptDerivationKey;
|
||||
return await _onChainWalletManager.DeriveScript(derivationKey);
|
||||
}
|
||||
|
||||
|
||||
public async Task TrackScripts(Script[] scripts, string derivation = WalletDerivation.LightningScripts)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
_logger.LogDebug("Tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
var identifier = _onChainWalletManager.WalletConfig.Derivations[derivation].Identifier;
|
||||
|
||||
await _connectionManager.HubProxy.TrackScripts(identifier,
|
||||
scripts.Select(script => script.ToHex()).ToArray()).RunSync();
|
||||
_logger.LogDebug("Tracked scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateChannel(List<ChannelAlias> identifiers, byte[] write)
|
||||
{
|
||||
var ids = identifiers.Select(alias => alias.Id).ToArray();
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var channel = (await context.ChannelAliases.Include(alias => alias.Channel)
|
||||
.ThenInclude(channel1 => channel1.Aliases).FirstOrDefaultAsync(alias => ids.Contains(alias.Id)))?.Channel;
|
||||
|
||||
if (channel is not null)
|
||||
{
|
||||
foreach (var alias in identifiers)
|
||||
{
|
||||
if (channel.Aliases.All(a => a.Id != alias.Id))
|
||||
{
|
||||
channel.Aliases.Add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
channel.Data = write;
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.LightningChannels.AddAsync(new Channel()
|
||||
{
|
||||
Id = identifiers.First().ChannelId,
|
||||
Data = write,
|
||||
Aliases = identifiers.ToList()
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task Peer(PubKey key, PeerInfo? value)
|
||||
{
|
||||
var toString = key.ToString().ToLowerInvariant();
|
||||
var config = await GetConfig();
|
||||
if (value is null)
|
||||
{
|
||||
if (config.Peers.Remove(toString))
|
||||
{
|
||||
await UpdateConfig(config);
|
||||
return;
|
||||
}
|
||||
}
|
||||
config.Peers.AddOrReplace(toString, value);
|
||||
await UpdateConfig(config);
|
||||
}
|
||||
}
|
||||
257
BTCPayApp.Core/Attempt2/LightningNodeService.cs
Normal file
257
BTCPayApp.Core/Attempt2/LightningNodeService.cs
Normal file
@ -0,0 +1,257 @@
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LightningNodeManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-LN";
|
||||
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<LightningNodeManager> _logger;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
private IServiceScope? _nodeScope;
|
||||
public LDKNode? Node => _nodeScope?.ServiceProvider.GetService<LDKNode>();
|
||||
private LightningNodeState _state = LightningNodeState.Init;
|
||||
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsMaster;
|
||||
private bool IsOnchainConfigured => _onChainWalletManager.WalletConfig is not null;
|
||||
private bool IsOnchainLightningDerivationConfigured => _onChainWalletManager.WalletConfig?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
|
||||
public bool CanConfigureLightningNode => IsHubConnected && IsOnchainConfigured && !IsOnchainLightningDerivationConfigured && State == LightningNodeState.NotConfigured;
|
||||
public string? ConnectionString => IsOnchainLightningDerivationConfigured && _accountManager.GetUserInfo() is {} acc
|
||||
? $"type=app;user={acc.UserId}": null;
|
||||
|
||||
public LightningNodeState State
|
||||
{
|
||||
get => _state;
|
||||
private set
|
||||
{
|
||||
if (_state == value)
|
||||
return;
|
||||
var old = _state;
|
||||
_state = value;
|
||||
_logger.LogInformation("Lightning node state changed: {State}", _state);
|
||||
StateChanged?.Invoke(this, (old, value));
|
||||
}
|
||||
}
|
||||
|
||||
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
|
||||
|
||||
public LightningNodeManager(
|
||||
IAccountManager accountManager,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<LightningNodeManager> logger,
|
||||
OnChainWalletManager onChainWalletManager,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_accountManager = accountManager;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task StartNode()
|
||||
{
|
||||
if (_nodeScope is not null || State is LightningNodeState.Loaded)
|
||||
return;
|
||||
await _controlSemaphore.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (_nodeScope is null)
|
||||
{
|
||||
_nodeScope = _serviceScopeFactory.CreateScope();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
await Node.StartAsync(_cancellationTokenSource.Token);
|
||||
|
||||
State = LightningNodeState.Loaded;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_nodeScope.Dispose();
|
||||
_logger.LogError(e, "Error while starting lightning node");
|
||||
_nodeScope = null;
|
||||
State = LightningNodeState.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopNode()
|
||||
{
|
||||
if (_nodeScope is null || State is not LightningNodeState.Loaded)
|
||||
return;
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token);
|
||||
cts.CancelAfter(5000);
|
||||
await Node.StopAsync(cts.Token);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while stopping lightning node");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nodeScope?.Dispose();
|
||||
_nodeScope = null;
|
||||
_controlSemaphore.Release();
|
||||
State = LightningNodeState.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanseTask()
|
||||
{
|
||||
await StopNode();
|
||||
|
||||
if (_nodeScope is not null || State == LightningNodeState.NotConfigured) return;
|
||||
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.LightningPayments.RemoveRange(context.LightningPayments);
|
||||
// context.OutboxItems.RemoveRange(context.OutboxItems);
|
||||
context.Settings.RemoveRange(context.Settings.Where(s => s.Key.StartsWith("ln:")));
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
|
||||
State = LightningNodeState.NotConfigured;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Generate()
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != LightningNodeState.NotConfigured) return;
|
||||
if (!IsHubConnected)
|
||||
throw new InvalidOperationException("Cannot configure lightning node without BTCPay connection");
|
||||
if (!IsOnchainConfigured)
|
||||
throw new InvalidOperationException("Cannot configure lightning node without on-chain wallet configuration");
|
||||
if (IsOnchainLightningDerivationConfigured)
|
||||
throw new InvalidOperationException("On-chain wallet is already configured with a lightning derivation");
|
||||
|
||||
await _onChainWalletManager.AddDerivation(WalletDerivation.LightningScripts, "Lightning", null);
|
||||
// await _onChainWalletManager.AddDerivation(WalletDerivation.SpendableOutputs, "Lightning Spendables", null);
|
||||
State = LightningNodeState.WaitingForConnection;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) valueTuple)
|
||||
{
|
||||
switch (IsHubConnected)
|
||||
{
|
||||
case true when State == LightningNodeState.WaitingForConnection:
|
||||
State = LightningNodeState.Loading;
|
||||
break;
|
||||
case true when State is LightningNodeState.Loading or LightningNodeState.Loaded:
|
||||
_ = StopNode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnChainWalletManagerOnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
|
||||
{
|
||||
if (e.New == OnChainWalletState.Loaded)
|
||||
{
|
||||
State = LightningNodeState.Loading;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnStateChanged(object? sender, (LightningNodeState Old, LightningNodeState New) state)
|
||||
{
|
||||
LightningNodeState? newState = null;
|
||||
try
|
||||
{
|
||||
switch (state.New)
|
||||
{
|
||||
case LightningNodeState.WaitingForConnection:
|
||||
{
|
||||
if (IsHubConnected)
|
||||
newState = LightningNodeState.Loading;
|
||||
break;
|
||||
}
|
||||
case LightningNodeState.Loading:
|
||||
if (!IsHubConnected)
|
||||
{
|
||||
newState = LightningNodeState.WaitingForConnection;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!IsOnchainConfigured || !IsOnchainLightningDerivationConfigured)
|
||||
{
|
||||
newState = LightningNodeState.NotConfigured;
|
||||
break;
|
||||
}
|
||||
await StartNode();
|
||||
break;
|
||||
|
||||
case LightningNodeState.NotConfigured:
|
||||
if (CanConfigureLightningNode)
|
||||
{
|
||||
await Generate();
|
||||
}
|
||||
break;
|
||||
|
||||
case LightningNodeState.Loaded:
|
||||
await _controlSemaphore.WaitAsync();
|
||||
|
||||
_controlSemaphore.Release();
|
||||
break;
|
||||
// case LightningNodeState.Unloading:
|
||||
// _nodeScope?.Dispose();
|
||||
// State = _walletConfig is null
|
||||
// ? LightningNodeState.NotConfigured
|
||||
// : LightningNodeState.WaitingForConnection;
|
||||
// break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (newState is not null)
|
||||
State = newState.Value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
State = LightningNodeState.Init;
|
||||
StateChanged += OnStateChanged;
|
||||
_btcPayConnectionManager.ConnectionChanged += OnConnectionChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_btcPayConnectionManager.ConnectionChanged -= OnConnectionChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
StateChanged -= OnStateChanged;
|
||||
_nodeScope?.Dispose();
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
namespace BTCPayApp.Core.Wallet;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum LightningNodeState
|
||||
{
|
||||
Init,
|
||||
512
BTCPayApp.Core/Attempt2/OnChainWalletManager.cs
Normal file
512
BTCPayApp.Core/Attempt2/OnChainWalletManager.cs
Normal file
@ -0,0 +1,512 @@
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Scripting;
|
||||
using OutPoint = NBitcoin.OutPoint;
|
||||
using TxOut = NBitcoin.TxOut;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
public class OnChainWalletManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-CHAIN";
|
||||
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly ILogger<OnChainWalletManager> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private OnChainWalletState _state = OnChainWalletState.Init;
|
||||
|
||||
public WalletConfig? WalletConfig { get; private set; }
|
||||
public Network? Network => WalletConfig is null ? null : Network.GetNetwork(WalletConfig.Network);
|
||||
|
||||
public OnChainWalletState State
|
||||
{
|
||||
get => _state;
|
||||
private set
|
||||
{
|
||||
if (_state == value)
|
||||
return;
|
||||
var old = _state;
|
||||
_state = value;
|
||||
_logger.LogInformation("Wallet state changed: {State}", _state);
|
||||
StateChanged?.Invoke(this, (old, value));
|
||||
}
|
||||
}
|
||||
|
||||
public event AsyncEventHandler<(OnChainWalletState Old, OnChainWalletState New)>? StateChanged;
|
||||
|
||||
public OnChainWalletManager(
|
||||
IConfigProvider configProvider,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
ILogger<OnChainWalletManager> logger,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_logger = logger;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
StateChanged += OnStateChanged;
|
||||
_btcPayAppServerClient.OnNewBlock += OnNewBlock;
|
||||
_btcPayAppServerClient.OnTransactionDetected += OnTransactionDetected;
|
||||
_btcPayConnectionManager.ConnectionChanged += ConnectionChanged;
|
||||
WalletConfig = await _configProvider.Get<WalletConfig>(WalletConfig.Key);
|
||||
DetermineState();
|
||||
if (IsHubConnected)
|
||||
{
|
||||
await Track();
|
||||
|
||||
_ = GetBestBlock();
|
||||
State = OnChainWalletState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsMaster;
|
||||
public bool IsConfigured => WalletConfig is not null;
|
||||
|
||||
private async Task OnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
|
||||
{
|
||||
if (e is { Old: OnChainWalletState.NotConfigured or OnChainWalletState.WaitingForConnection } && IsHubConnected && !IsConfigured)
|
||||
{
|
||||
await Generate();
|
||||
}
|
||||
|
||||
if (e is {New: OnChainWalletState.Loaded} && IsConfigured)
|
||||
{
|
||||
await Track();
|
||||
}
|
||||
|
||||
if (e.New is OnChainWalletState.Loading)
|
||||
{
|
||||
DetermineState();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Generate()
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.NotConfigured || IsConfigured || !IsHubConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot generate wallet in current state");
|
||||
}
|
||||
|
||||
var mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
|
||||
var mainnet = _btcPayConnectionManager.ReportedNetwork == Network.Main;
|
||||
var path = new KeyPath($"m/84'/{(mainnet ? "0" : "1")}'/0'");
|
||||
var fingerprint = mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint();
|
||||
var xpub = mnemonic.DeriveExtKey().Derive(path).Neuter().ToString(_btcPayConnectionManager.ReportedNetwork);
|
||||
var walletConfig = new WalletConfig
|
||||
{
|
||||
Mnemonic = mnemonic.ToString(),
|
||||
Network = _btcPayConnectionManager.ReportedNetwork.ToString(),
|
||||
Derivations = new Dictionary<string, WalletDerivation>()
|
||||
{
|
||||
[WalletDerivation.NativeSegwit] = new WalletDerivation()
|
||||
{
|
||||
Name = "Native Segwit",
|
||||
Descriptor = OutputDescriptor.AddChecksum(
|
||||
$"wpkh([{fingerprint.ToString()}/{path}]{xpub}/0/*)")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest()
|
||||
{
|
||||
Derivations = walletConfig.Derivations.ToDictionary(pair => pair.Key, pair => pair.Value.Descriptor)
|
||||
}).RunSync();
|
||||
foreach (var keyValuePair in result)
|
||||
{
|
||||
walletConfig.Derivations[keyValuePair.Key].Identifier = keyValuePair.Value;
|
||||
|
||||
}
|
||||
await _configProvider.Set(WalletConfig.Key, walletConfig, true);
|
||||
WalletConfig = walletConfig;
|
||||
State = OnChainWalletState.Loaded;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddDerivation(string key, string name, string? descriptor)
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.Loaded || !IsConfigured || !IsHubConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add deriv in current state");
|
||||
}
|
||||
if (WalletConfig.Derivations.ContainsKey(key))
|
||||
throw new InvalidOperationException("Derivation already exists");
|
||||
|
||||
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest
|
||||
{
|
||||
Derivations = new Dictionary<string, string?>()
|
||||
{
|
||||
[key] = descriptor
|
||||
}
|
||||
}).RunSync();
|
||||
WalletConfig.Derivations[key] = new WalletDerivation()
|
||||
{
|
||||
Name = name,
|
||||
Descriptor = descriptor,
|
||||
Identifier = result[key]
|
||||
};
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) valueTuple)
|
||||
{
|
||||
DetermineState();
|
||||
}
|
||||
|
||||
private void DetermineState()
|
||||
{
|
||||
if (IsHubConnected && IsConfigured)
|
||||
State = OnChainWalletState.Loaded;
|
||||
else if (!IsHubConnected)
|
||||
State = OnChainWalletState.WaitingForConnection;
|
||||
else if (!IsConfigured)
|
||||
State = OnChainWalletState.NotConfigured;
|
||||
}
|
||||
|
||||
private async Task Track()
|
||||
{
|
||||
if (!IsConfigured || !IsHubConnected)
|
||||
return;
|
||||
|
||||
var identifiers = WalletConfig.Derivations.Select(pair => pair.Value.Identifier).ToArray();
|
||||
var response = await _btcPayConnectionManager.HubProxy.Handshake(new AppHandshake
|
||||
{
|
||||
Identifiers = identifiers
|
||||
}).RunSync();
|
||||
|
||||
var missing =
|
||||
WalletConfig.Derivations.Where(pair => !response.IdentifiersAcknowledged.Contains(pair.Value.Identifier));
|
||||
|
||||
if (missing.Any())
|
||||
{
|
||||
_logger.LogWarning("Some identifiers that we had asked for BtcPayServer to track were not confirmed as being listened to. Tracking will be incomplete and functionality will critically fail.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_btcPayAppServerClient.OnNewBlock -= OnNewBlock;
|
||||
_btcPayAppServerClient.OnTransactionDetected -= OnTransactionDetected;
|
||||
_btcPayConnectionManager.ConnectionChanged -= ConnectionChanged;
|
||||
WalletConfig = null;
|
||||
State = OnChainWalletState.Init;
|
||||
}
|
||||
|
||||
private async Task OnTransactionDetected(object? sender, TransactionDetectedRequest transactionDetectedRequest)
|
||||
{
|
||||
}
|
||||
|
||||
private async Task OnNewBlock(object? sender, string e)
|
||||
{
|
||||
_memoryCache.Remove("bestblock");
|
||||
_ = GetBestBlock();
|
||||
}
|
||||
|
||||
public async Task<Script> DeriveScript(string derivation)
|
||||
{
|
||||
var identifier = WalletConfig?.Derivations[derivation].Identifier;
|
||||
var addr = await _btcPayConnectionManager.HubProxy.DeriveScript(identifier).RunSync();
|
||||
return Script.FromHex(addr);
|
||||
}
|
||||
|
||||
public async Task<byte[]?> SignTransaction(byte[] psbtBytes)
|
||||
{
|
||||
var psbt = PSBT.Load(psbtBytes, Network);
|
||||
psbt = await SignTransaction(psbt);
|
||||
return psbt?.ToBytes();
|
||||
}
|
||||
public async Task<PSBT?> SignTransaction(PSBT psbt)
|
||||
{
|
||||
var identifiers = WalletConfig.Derivations.Select(derivation => derivation.Value.Identifier).ToArray();
|
||||
var updated = await _btcPayConnectionManager.HubProxy.UpdatePsbt(identifiers, psbt.ToHex()).RunSync();
|
||||
psbt = PSBT.Parse(updated, Network);
|
||||
var rootKey =new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey();
|
||||
foreach (var deriv in WalletConfig.Derivations.Values.Where(derivation => derivation.Descriptor is not null))
|
||||
{
|
||||
var data = deriv.Descriptor.ExtractFromDescriptor(Network);
|
||||
if(data is null)
|
||||
continue;
|
||||
var accKey = rootKey.Derive(data.Value.Item2);
|
||||
psbt = psbt.SignAll(data.Value.Item1.AsHDScriptPubKey(data.Value.Item3), accKey);
|
||||
if(psbt.TryFinalize(out _))
|
||||
break;
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private static ICoin ToCoin(CoinResponse response)
|
||||
{
|
||||
var outpoint = OutPoint.Parse(response.Outpoint);
|
||||
var scriptPubKey = Script.FromHex(response.Script);
|
||||
var amount = Money.Coins(response.Value);
|
||||
return new Coin(outpoint, new TxOut(amount, scriptPubKey));
|
||||
}
|
||||
|
||||
// // public class SpendableOutputDescriptorCoin : Coin,ISignableCoin
|
||||
// {
|
||||
// public SpendableOutputDescriptorCoin(OutPoint fromOutpoint, TxOut fromTxOut, SpendableOutputDescriptor descriptor) : base(fromOutpoint, fromTxOut)
|
||||
// {
|
||||
// Descriptor = descriptor;
|
||||
// }
|
||||
//
|
||||
// public SpendableOutputDescriptor Descriptor { get;}
|
||||
// public async Task<PSBT> Sign(PSBT psbt)
|
||||
// {
|
||||
//
|
||||
// UtilMethods.
|
||||
// UtilMethods.SpendableOutputDescriptor_create_spendable_outputs_psbt(new SpendableOutputDescriptor[]{Descriptor}, )
|
||||
// Descriptor.create_spendable_outpcreate_spendable_outputs_psbtuts_psbt
|
||||
// switch (Descriptor)
|
||||
// {
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_DelayedPaymentOutput spendableOutputDescriptorDelayedPaymentOutput:
|
||||
// spendableOutputDescriptorDelayedPaymentOutput.delayed_payment_output.
|
||||
// break;
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticOutput spendableOutputDescriptorStaticOutput:
|
||||
// //ignore
|
||||
// break;
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticPaymentOutput spendableOutputDescriptorStaticPaymentOutput:
|
||||
// spendableOutputDescriptorStaticPaymentOutput.static_payment_output.psb
|
||||
// break;
|
||||
// default:
|
||||
// throw new ArgumentOutOfRangeException(nameof(Descriptor));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
public class CoinWithKey : Coin,ISignableCoin
|
||||
{
|
||||
public Key Key { get; }
|
||||
|
||||
public CoinWithKey(OutPoint fromOutpoint, TxOut fromTxOut, Key key) : base(fromOutpoint, fromTxOut)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public async Task<PSBT> Sign(PSBT psbt)
|
||||
{
|
||||
return psbt.SignWithKeys(Key);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISignableCoin : ICoin
|
||||
{
|
||||
Task<PSBT> Sign(PSBT psbt);
|
||||
}
|
||||
|
||||
public async Task<TxResp[]> GetTransactions()
|
||||
{
|
||||
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
|
||||
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
|
||||
var res= await _btcPayConnectionManager.HubProxy.GetTransactions(identifiersWhichWeCanDeriveKeysFor).RunSync();
|
||||
return res.SelectMany(pair => pair.Value).OrderByDescending(resp => resp.Timestamp).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<ICoin>> GetUTXOS()
|
||||
{
|
||||
var identifiers = WalletConfig.Derivations.Values.Select(derivation => derivation.Identifier).ToArray();
|
||||
var utxos = await _btcPayConnectionManager.HubProxy.GetUTXOs(identifiers).RunSync();
|
||||
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
|
||||
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
|
||||
var result = new List<ICoin>();
|
||||
|
||||
var utxosThatWeCanDeriveKeysFor = utxos.Where(utxo => identifiersWhichWeCanDeriveKeysFor.Contains(utxo.Identifier)).ToArray();
|
||||
foreach (var coin in utxosThatWeCanDeriveKeysFor)
|
||||
{
|
||||
var derivation =
|
||||
WalletConfig.Derivations.Values.First(derivation => derivation.Identifier == coin.Identifier);
|
||||
var data = derivation.Descriptor.ExtractFromDescriptor(Network);
|
||||
if (data is null)
|
||||
continue;
|
||||
var coinKeyPath = KeyPath.Parse(coin.Path);
|
||||
var key = new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey().Derive(data.Value.Item2.KeyPath)
|
||||
.Derive(coinKeyPath).PrivateKey;
|
||||
var c = ToCoin(coin);
|
||||
|
||||
|
||||
result.Add(new CoinWithKey(c.Outpoint, c.TxOut, key));
|
||||
|
||||
}
|
||||
// if (WalletConfig.Derivations.TryGetValue(WalletDerivation.SpendableOutputs, out var spendableOutputDerivation))
|
||||
// {
|
||||
//
|
||||
// var spendableOutputUtxos = utxos.Where(response => response.Identifier == spendableOutputDerivation.Identifier).ToArray();
|
||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
// var scipts = spendableOutputUtxos.Select(response => response.Script).Distinct();
|
||||
// var spendableCoins = await context.SpendableCoins.Where(coin => scipts.Contains(coin.Script)).ToListAsync();
|
||||
//
|
||||
// foreach (var spendableOutputUtxo in spendableOutputUtxos)
|
||||
// {
|
||||
// var spendableCoin = spendableCoins.FirstOrDefault(coin => coin.Script == spendableOutputUtxo.Script);
|
||||
// if (spendableCoin is null)
|
||||
// continue;
|
||||
// var coin = ToCoin(spendableOutputUtxo);
|
||||
// var data = SpendableOutputDescriptor.read(spendableCoin.Data);
|
||||
// if(data is Result_SpendableOutputDescriptorDecodeErrorZ.Result_SpendableOutputDescriptorDecodeErrorZ_OK ok)
|
||||
// result.Add(new SpendableOutputDescriptorCoin(coin.Outpoint, coin.TxOut, ok.res));
|
||||
// }
|
||||
// }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async Task<(NBitcoin.Transaction Tx, ICoin[] SpentCoins, NBitcoin.Script Change)> CreateTransaction(
|
||||
List<TxOut> txOuts, FeeRate? feeRate, List<Coin> explicitIns = null)
|
||||
{
|
||||
var availableCoins = (await GetUTXOS()).ToList();
|
||||
feeRate ??= await GetFeeRate(1);
|
||||
//TODO: do not hardcode this constant
|
||||
var changeScript = await DeriveScript(WalletDerivation.NativeSegwit);
|
||||
var txBuilder = Network
|
||||
.CreateTransactionBuilder()
|
||||
.SetChange(changeScript)
|
||||
.SendEstimatedFees(feeRate);
|
||||
|
||||
txBuilder = txOuts.Aggregate(txBuilder, (current, c) => current.Send(c.ScriptPubKey, c.Value));
|
||||
txBuilder.SendAllRemainingToChange();
|
||||
|
||||
NBitcoin.Transaction? tx;
|
||||
if (explicitIns?.Any() is true)
|
||||
{
|
||||
txBuilder.AddCoins(explicitIns.ToArray());
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
tx = txBuilder.BuildTransaction(true);
|
||||
return (tx, txBuilder.FindSpentCoins(tx), changeScript);
|
||||
}
|
||||
catch (NotEnoughFundsException e)
|
||||
{
|
||||
if (!availableCoins.Any())
|
||||
throw;
|
||||
var newCoin = availableCoins.First();
|
||||
//TODO: switch to nuilding a psbt and signing with the ISignableCoin interface
|
||||
if(newCoin is CoinWithKey newCoinWithKey)
|
||||
{
|
||||
txBuilder.AddCoins(newCoin);
|
||||
txBuilder.AddKeys(newCoinWithKey.Key);
|
||||
}
|
||||
availableCoins.Remove(newCoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveDerivation(params string[] key)
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.Loaded || WalletConfig is null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot remove deriv in current state");
|
||||
}
|
||||
|
||||
var updated = key.Aggregate(false, (current, k) => current || WalletConfig.Derivations.Remove(k));
|
||||
if (updated)
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
public async Task<BestBlockResponse?> GetBestBlock()
|
||||
{
|
||||
var res = await _memoryCache.GetOrCreateAsync("bestblock", async entry =>
|
||||
{
|
||||
_logger.LogInformation("Getting best block");
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
try
|
||||
{
|
||||
|
||||
return await _btcPayConnectionManager.HubProxy.GetBestBlock().RunSync();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error getting best block");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("Got best block");
|
||||
}
|
||||
});
|
||||
if (res is null)
|
||||
{
|
||||
_memoryCache.Remove("bestblock");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public async Task BroadcastTransaction(Transaction valueTx, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _btcPayConnectionManager.HubProxy.BroadcastTransaction(valueTx.ToHex()).RunSync();
|
||||
}
|
||||
|
||||
public async Task<FeeRate> GetFeeRate(int blockTarget)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync($"feerate_{blockTarget}", async entry =>
|
||||
{
|
||||
_logger.LogInformation("Getting fee rate for block target {BlockTarget}", blockTarget);
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
try
|
||||
{
|
||||
return new FeeRate(await _btcPayConnectionManager.HubProxy.GetFeeRate(blockTarget).RunSync());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("Got fee rate for block target {BlockTarget}", blockTarget);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error getting fee rate, using hardcoded 100");
|
||||
return new FeeRate(100m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OnChainWalletState
|
||||
{
|
||||
Init,
|
||||
NotConfigured,
|
||||
WaitingForConnection,
|
||||
Loading,
|
||||
Loaded
|
||||
}
|
||||
9
BTCPayApp.Core/Attempt2/SetupState.cs
Normal file
9
BTCPayApp.Core/Attempt2/SetupState.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
Pending,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class SingleKeyDataProtector : IDataProtector
|
||||
{
|
||||
@ -11,7 +11,9 @@ public class SingleKeyDataProtector : IDataProtector
|
||||
public SingleKeyDataProtector(byte[] key)
|
||||
{
|
||||
if (key.Length != 32) // AES-256 key size
|
||||
{
|
||||
throw new ArgumentException("Key length must be 32 bytes.");
|
||||
}
|
||||
|
||||
_key = key;
|
||||
}
|
||||
@ -30,10 +32,14 @@ public class SingleKeyDataProtector : IDataProtector
|
||||
aes.Key = _key;
|
||||
aes.GenerateIV();
|
||||
|
||||
var iv = aes.IV;
|
||||
var encrypted = aes.EncryptCbc(plaintext, iv);
|
||||
byte[] iv = aes.IV;
|
||||
byte[] encrypted = aes.EncryptCbc(plaintext, iv);
|
||||
|
||||
return iv.Concat(encrypted).ToArray();
|
||||
byte[] result = new byte[iv.Length + encrypted.Length];
|
||||
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||
Buffer.BlockCopy(encrypted, 0, result, iv.Length, encrypted.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] Unprotect(byte[] protectedData)
|
||||
@ -41,12 +47,13 @@ public class SingleKeyDataProtector : IDataProtector
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
if (protectedData.Length == 0)
|
||||
return protectedData;
|
||||
byte[] iv = new byte[16];
|
||||
byte[] cipherText = new byte[protectedData.Length - iv.Length];
|
||||
|
||||
var iv = protectedData.Take(16).ToArray();
|
||||
var cipherText = protectedData.Skip(16).ToArray();
|
||||
Buffer.BlockCopy(protectedData, 0, iv, 0, iv.Length);
|
||||
Buffer.BlockCopy(protectedData, iv.Length, cipherText, 0, cipherText.Length);
|
||||
|
||||
return aes.DecryptCbc(cipherText, iv);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
289
BTCPayApp.Core/Attempt2/SyncService.cs
Normal file
289
BTCPayApp.Core/Attempt2/SyncService.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.VSS;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class SyncService
|
||||
{
|
||||
private readonly ILogger<SyncService> _logger;
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ISecureConfigProvider _secureConfigProvider;
|
||||
|
||||
public SyncService(
|
||||
ILogger<SyncService> logger,
|
||||
ISecureConfigProvider secureConfigProvider,
|
||||
IAccountManager accountManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_accountManager = accountManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_secureConfigProvider = secureConfigProvider;
|
||||
}
|
||||
|
||||
private async Task<IDataProtector> GetDataProtector()
|
||||
{
|
||||
var key = await _secureConfigProvider.GetOrSet("encryptionKey",
|
||||
async () => Convert.ToHexString(RandomUtils.GetBytes(32)).ToLowerInvariant());
|
||||
return new SingleKeyDataProtector(Convert.FromHexString(key));
|
||||
}
|
||||
|
||||
|
||||
private async Task<IVSSAPI> GetVSSAPI()
|
||||
{
|
||||
var account = _accountManager.GetAccount();
|
||||
if (account is null)
|
||||
throw new InvalidOperationException("Account not found");
|
||||
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
|
||||
var httpClient = _httpClientFactory.CreateClient("vss");
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", account.AccessToken);
|
||||
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
|
||||
var protector = await GetDataProtector();
|
||||
return new VSSApiEncryptorClient(vssClient, protector);
|
||||
}
|
||||
|
||||
private async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
|
||||
{
|
||||
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue()
|
||||
{
|
||||
Key = setting.EntityKey,
|
||||
Version = setting.Version
|
||||
});
|
||||
var channels = dbContext.LightningChannels.Select(channel => new KeyValue()
|
||||
{
|
||||
Key = channel.EntityKey,
|
||||
Version = channel.Version
|
||||
});
|
||||
var payments = dbContext.LightningPayments.Select(payment => new KeyValue()
|
||||
{
|
||||
Key = payment.EntityKey,
|
||||
Version = payment.Version
|
||||
});
|
||||
return await settings.Concat(channels).Concat(payments).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task SyncToLocal(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backupApi = await GetVSSAPI();
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var localVersions = await CreateLocalVersions(db);
|
||||
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
|
||||
await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var triggers = await db.Database
|
||||
.SqlQuery<TriggerRecord>($"SELECT name, sql FROM sqlite_master WHERE type = 'trigger'")
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
string.Join("; ", triggers.Select(trigger => $"DROP TRIGGER IF EXISTS {trigger.name}")),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
// delete local versions that are not in remote
|
||||
// delete local versions which are lower than remote
|
||||
|
||||
var toDelete = localVersions.Where(localVersion =>
|
||||
remoteVersions.KeyVersions.All(remoteVersion => remoteVersion.Key != localVersion.Key)
|
||||
|| remoteVersions.KeyVersions.All(remoteVersion =>
|
||||
remoteVersion.Key == localVersion.Key && remoteVersion.Version > localVersion.Version)).ToArray();
|
||||
|
||||
var toUpsert = remoteVersions.KeyVersions.Where(remoteVersion => localVersions.All(localVersion =>
|
||||
localVersion.Key != remoteVersion.Key || localVersion.Version < remoteVersion.Version));
|
||||
|
||||
foreach (var upsertItem in toUpsert)
|
||||
{
|
||||
if (upsertItem.Value is null)
|
||||
{
|
||||
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
|
||||
{
|
||||
Key = upsertItem.Key,
|
||||
}, cancellationToken);
|
||||
upsertItem.MergeFrom(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var settingsToDelete = toDelete.Where(key => key.Key.StartsWith("Setting_")).Select(key => key.Key);
|
||||
var channelsToDelete = toDelete.Where(key => key.Key.StartsWith("Channel_")).Select(key => key.Key);
|
||||
var paymentsToDelete = toDelete.Where(key => key.Key.StartsWith("Payment_")).Select(key => key.Key);
|
||||
await db.Settings.Where(setting => settingsToDelete.Contains(setting.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
await db.LightningChannels.Where(channel => channelsToDelete.Contains(channel.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
await db.LightningPayments.Where(payment => paymentsToDelete.Contains(payment.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
|
||||
// upsert the rest when needed
|
||||
var settingsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Setting_")).Select(setting => new Setting()
|
||||
{
|
||||
Key = setting.Key.Split('_')[1],
|
||||
Value = setting.Value.ToByteArray(),
|
||||
Version = setting.Version,
|
||||
Backup = true
|
||||
});
|
||||
var channelsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Channel_"))
|
||||
.Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!);
|
||||
var paymentsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Payment_")).Select(value =>
|
||||
JsonSerializer.Deserialize<AppLightningPayment>(value.Value.ToStringUtf8())!);
|
||||
|
||||
await db.Settings.UpsertRange(settingsToUpsert).On(setting => setting.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
await db.LightningChannels.UpsertRange(channelsToUpsert).On(channel => channel.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
await db.LightningPayments.UpsertRange(paymentsToUpsert).On(payment => payment.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync(string.Join("; ", triggers.Select(record => record.sql)),
|
||||
cancellationToken: cancellationToken);
|
||||
await db.Database.CommitTransactionAsync(cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await db.Database.RollbackTransactionAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
{
|
||||
switch (outbox.Entity)
|
||||
{
|
||||
case "Setting":
|
||||
var setting = await dbContext.Settings.SingleOrDefaultAsync(setting1 =>
|
||||
setting1.EntityKey == outbox.Key && setting1.Backup);
|
||||
if (setting == null)
|
||||
return null;
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(setting.Value),
|
||||
Version = setting.Version
|
||||
};
|
||||
case "Channel":
|
||||
var channel = await dbContext.LightningChannels.Include(channel1 => channel1.Aliases)
|
||||
.SingleOrDefaultAsync(channel1 => channel1.EntityKey == outbox.Key);
|
||||
|
||||
if (channel == null)
|
||||
return null;
|
||||
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(val),
|
||||
Version = channel.Version
|
||||
};
|
||||
case "Payment":
|
||||
var payment = await dbContext.LightningPayments.SingleOrDefaultAsync(lightningPayment =>
|
||||
lightningPayment.EntityKey == outbox.Key);
|
||||
if (payment == null)
|
||||
return null;
|
||||
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(paymentBytes),
|
||||
Version = payment.Version
|
||||
};
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SyncToRemote(long deviceIdentifier, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backupAPi = await GetVSSAPI();
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var putObjectRequest = new PutObjectRequest();
|
||||
var outbox = await db.OutboxItems.GroupBy(outbox1 => new {outbox1.Key})
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
foreach (var outboxItemSet in outbox)
|
||||
{
|
||||
var orderedEnumerable = outboxItemSet.OrderByDescending(outbox1 => outbox1.Version)
|
||||
.ThenByDescending(outbox1 => outbox1.ActionType).ToArray();
|
||||
foreach (var item in orderedEnumerable)
|
||||
{
|
||||
if (item.ActionType == OutboxAction.Delete)
|
||||
{
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
{
|
||||
Key = item.Key, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
|
||||
putObjectRequest.GlobalVersion = deviceIdentifier;
|
||||
await backupAPi.PutObjectAsync(putObjectRequest, cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
|
||||
|
||||
public async Task StartSync(bool local, long deviceIdentifier, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_syncTask.HasValue && _syncTask.Value.local == local)
|
||||
return;
|
||||
if (_syncTask.HasValue)
|
||||
{
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
}
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_syncTask = (ContinuouslySync(deviceIdentifier,local, cts.Token), cts, local);
|
||||
}
|
||||
|
||||
public async Task StopSync()
|
||||
{
|
||||
if (_syncTask.HasValue)
|
||||
{
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
_syncTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ContinuouslySync(long deviceIdentifier, bool local, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (local)
|
||||
await SyncToLocal(cancellationToken);
|
||||
else
|
||||
await SyncToRemote(deviceIdentifier, cancellationToken);
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while syncing to {Local}", local ? "local" : "remote");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AppPolicies
|
||||
{
|
||||
public const string CanModifySettings = "btcpay.plugin.app.canmodifysettings";
|
||||
|
||||
public static IEnumerable<string> AllPolicies
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CanModifySettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@ -11,33 +13,54 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AuthStateProvider(
|
||||
IHttpClientFactory clientFactory,
|
||||
IAuthorizationService authService,
|
||||
ISecureConfigProvider secureProvider,
|
||||
ConfigProvider configProvider,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions)
|
||||
: AuthenticationStateProvider, IAccountManager, IHostedService
|
||||
public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, IHostedService
|
||||
{
|
||||
private const string AccountKeyPrefix = "Account";
|
||||
private const string CurrentAccountKey = "CurrentAccount";
|
||||
private bool _isInitialized;
|
||||
private bool _refreshUserInfo;
|
||||
private string? _currentStoreId;
|
||||
private CancellationTokenSource? _pingCts;
|
||||
private BTCPayAccount? _account;
|
||||
|
||||
// TODO: Move _userInfo to state
|
||||
private AppUserInfo? _userInfo;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private readonly ClaimsPrincipal _unauthenticated = new(new ClaimsIdentity());
|
||||
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
|
||||
private readonly IAuthorizationService _authService;
|
||||
private readonly IConfigProvider _config;
|
||||
|
||||
public BTCPayAccount? Account { get; private set; }
|
||||
public AppUserInfo? UserInfo { get; private set; }
|
||||
public AppUserStoreInfo? CurrentStore => string.IsNullOrEmpty(_currentStoreId) ? null : GetUserStore(_currentStoreId);
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnStoreChanged { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChanged { get; set; }
|
||||
public AsyncEventHandler<string>? OnEncryptionKeyChanged { get; set; }
|
||||
public BTCPayAccount? GetAccount() => _account;
|
||||
public AppUserInfo? GetUserInfo() => _userInfo;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
|
||||
public AuthStateProvider(
|
||||
IConfigProvider config,
|
||||
IAuthorizationService authService,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions)
|
||||
{
|
||||
_config = config;
|
||||
_authService = authService;
|
||||
_identityOptions = identityOptions;
|
||||
}
|
||||
|
||||
private CancellationTokenSource? _pingCts;
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_pingCts = new CancellationTokenSource();
|
||||
_ = PingOccasionally(_pingCts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PingOccasionally(CancellationToken pingCtsToken)
|
||||
{
|
||||
while (pingCtsToken.IsCancellationRequested is false)
|
||||
{
|
||||
|
||||
await GetAuthenticationStateAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), pingCtsToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
@ -46,32 +69,15 @@ public class AuthStateProvider(
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PingOccasionally(CancellationToken pingCtsToken)
|
||||
public BTCPayAppClient GetClient(string? baseUri = null)
|
||||
{
|
||||
while (pingCtsToken.IsCancellationRequested is false)
|
||||
{
|
||||
await GetAuthenticationStateAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), pingCtsToken);
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(Account?.BaseUri))
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(_account?.BaseUri))
|
||||
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
|
||||
token ??= Account?.ModeToken ?? Account?.OwnerToken;
|
||||
return new BTCPayAppClient(baseUri ?? Account!.BaseUri, token, clientFactory.CreateClient());
|
||||
}
|
||||
|
||||
public async Task<string?> GetEncryptionKey()
|
||||
{
|
||||
return await secureProvider.Get<string>("encryptionKey");
|
||||
}
|
||||
|
||||
public async Task SetEncryptionKey(string value)
|
||||
{
|
||||
await secureProvider.Set("encryptionKey", value);
|
||||
OnEncryptionKeyChanged?.Invoke(this, value);
|
||||
var client = new BTCPayAppClient(baseUri ?? _account!.BaseUri);
|
||||
if (string.IsNullOrEmpty(baseUri) && !string.IsNullOrEmpty(_account?.AccessToken) && !string.IsNullOrEmpty(_account.RefreshToken))
|
||||
client.SetAccess(_account.AccessToken, _account.RefreshToken, _account.AccessExpiry.GetValueOrDefault());
|
||||
client.AccessRefreshed += OnAccessRefresh;
|
||||
return client;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
@ -83,56 +89,49 @@ public class AuthStateProvider(
|
||||
await _semaphore.WaitAsync();
|
||||
|
||||
// initialize with persisted account
|
||||
if (!_isInitialized && Account == null)
|
||||
if (!_isInitialized && _account == null)
|
||||
{
|
||||
Account = await secureProvider.Get<BTCPayAccount>(BTCPayAccount.Key);
|
||||
_currentStoreId = (await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key))?.CurrentStoreId;
|
||||
_account = await GetCurrentAccount();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
var oldUserInfo = UserInfo;
|
||||
var hasOwnerToken = !string.IsNullOrEmpty(Account?.OwnerToken);
|
||||
var hasModeToken = !string.IsNullOrEmpty(Account?.ModeToken);
|
||||
var needsRefresh = _refreshUserInfo || UserInfo == null;
|
||||
if (needsRefresh && hasOwnerToken)
|
||||
var oldUserInfo = _userInfo;
|
||||
if (_userInfo == null && _account?.HasTokens is true)
|
||||
{
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
UserInfo = await GetClient().GetUserInfo(cts.Token);
|
||||
_refreshUserInfo = false;
|
||||
await FetchUserInfo(cts.Token);
|
||||
}
|
||||
|
||||
if (Account != null && UserInfo != null)
|
||||
if (_userInfo != null)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, UserInfo.UserId!),
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.UserNameClaimType, UserInfo.Name ?? UserInfo.Email!),
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.EmailClaimType, UserInfo.Email!)
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, _userInfo.UserId!),
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.UserNameClaimType, _userInfo.Name ?? _userInfo.Email!),
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.EmailClaimType, _userInfo.Email!)
|
||||
};
|
||||
if (UserInfo.Roles?.Any() is true)
|
||||
claims.AddRange(UserInfo.Roles.Select(role =>
|
||||
new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
|
||||
if (UserInfo.Stores?.Any() is true)
|
||||
claims.AddRange(UserInfo.Stores.Select(store =>
|
||||
new Claim(store.Id, string.Join(',', store.Permissions ?? []))));
|
||||
if (hasOwnerToken && !hasModeToken)
|
||||
claims.Add(new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, "DeviceOwner"));
|
||||
user = new ClaimsPrincipal(new ClaimsIdentity(claims, "Greenfield"));
|
||||
if (_userInfo.Roles?.Any() is true)
|
||||
claims.AddRange(_userInfo.Roles.Select(role =>
|
||||
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
|
||||
if (_userInfo.Stores?.Any() is true)
|
||||
claims.AddRange(_userInfo.Stores.Select(store =>
|
||||
new Claim(store.Id, string.Join(',', store.Permissions))));
|
||||
user = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationSchemes.GreenfieldBearer));
|
||||
|
||||
// update account user info
|
||||
_account!.SetInfo(_userInfo.Email!, _userInfo.Name, _userInfo.ImageUrl);
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
|
||||
var res = new AuthenticationState(user);
|
||||
if (AppUserInfo.Equals(oldUserInfo, UserInfo)) return res;
|
||||
|
||||
OnUserInfoChanged?.Invoke(this, UserInfo);
|
||||
if (Account != null && UserInfo != null)
|
||||
await UpdateAccount(Account);
|
||||
if (AppUserInfo.Equals(oldUserInfo, _userInfo))
|
||||
return res;
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(res));
|
||||
return res;
|
||||
}
|
||||
catch
|
||||
{
|
||||
UserInfo = null;
|
||||
return new AuthenticationState(user);
|
||||
}
|
||||
finally
|
||||
@ -143,78 +142,78 @@ public class AuthStateProvider(
|
||||
|
||||
public async Task<bool> CheckAuthenticated(bool refreshUser = false)
|
||||
{
|
||||
if (refreshUser) _refreshUserInfo = true;
|
||||
if (refreshUser) await FetchUserInfo();
|
||||
await GetAuthenticationStateAsync();
|
||||
return UserInfo != null;
|
||||
return _userInfo != null;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorized(string policy, object? resource = null)
|
||||
{
|
||||
var authState = await GetAuthenticationStateAsync();
|
||||
var result = await authService.AuthorizeAsync(authState.User, resource, policy);
|
||||
var result = await _authService.AuthorizeAsync(authState.User, resource, policy);
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
public async Task<FormResult> SetCurrentStoreId(string? storeId)
|
||||
public async Task Logout()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
var store = GetUserStore(storeId);
|
||||
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
|
||||
|
||||
if (store.Id != CurrentStore?.Id)
|
||||
await SetCurrentStore(store);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetCurrentStore(null);
|
||||
}
|
||||
return new FormResult(true);
|
||||
_userInfo = null;
|
||||
_account!.ClearAccess();
|
||||
await UpdateAccount(_account);
|
||||
await SetCurrentAccount(null);
|
||||
}
|
||||
|
||||
private async Task SetCurrentStore(AppUserStoreInfo? store)
|
||||
public async Task<FormResult> SetCurrentStoreId(string storeId)
|
||||
{
|
||||
if (_currentStoreId == store?.Id) return;
|
||||
var store = GetUserStore(storeId);
|
||||
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
|
||||
|
||||
if (store != null)
|
||||
store = await EnsureStorePos(store);
|
||||
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
|
||||
string? message = null;
|
||||
|
||||
_currentStoreId = store?.Id;
|
||||
|
||||
var appConfig = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig();
|
||||
appConfig.CurrentStoreId = _currentStoreId;
|
||||
await configProvider.Set(BTCPayAppConfig.Key, appConfig, true);
|
||||
|
||||
OnStoreChanged?.Invoke(this, store);
|
||||
}
|
||||
|
||||
public async Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(store.PosAppId) || forceCreate is true)
|
||||
// create associated POS app if there is none
|
||||
if (string.IsNullOrEmpty(store.PosAppId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var posConfig = new PointOfSaleAppRequest { AppName = store.Name, DefaultView = PosViewType.Light };
|
||||
await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
|
||||
await CheckAuthenticated(true);
|
||||
store = GetUserStore(store.Id)!;
|
||||
var app = await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
|
||||
message = $"The Point of Sale called \"{app.AppName}\" has been created for use with the app.";
|
||||
|
||||
await FetchUserInfo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored
|
||||
return new FormResult(false, e.Message);
|
||||
}
|
||||
}
|
||||
return store;
|
||||
|
||||
_account!.CurrentStoreId = storeId;
|
||||
await UpdateAccount(_account);
|
||||
OnAfterStoreChange?.Invoke(this, store);
|
||||
|
||||
return new FormResult(true, string.IsNullOrEmpty(message) ? null : [message]);
|
||||
}
|
||||
|
||||
private AppUserStoreInfo? GetUserStore(string storeId)
|
||||
public async Task UnsetCurrentStore()
|
||||
{
|
||||
return UserInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
|
||||
_account!.CurrentStoreId = null;
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
|
||||
public AppUserStoreInfo? GetUserStore(string storeId)
|
||||
{
|
||||
return _userInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
|
||||
}
|
||||
|
||||
public AppUserStoreInfo? GetCurrentStore()
|
||||
{
|
||||
var storeId = _account?.CurrentStoreId;
|
||||
return string.IsNullOrEmpty(storeId) ? null : GetUserStore(storeId);
|
||||
}
|
||||
|
||||
public async Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default)
|
||||
{
|
||||
var urlParts = inviteUrl.Split(Constants.InviteSeparator);
|
||||
var urlParts = inviteUrl.Split("/invite/");
|
||||
var serverUrl = urlParts.First();
|
||||
var pathParts = urlParts.Last().Split("/");
|
||||
var payload = new AcceptInviteRequest
|
||||
@ -225,8 +224,8 @@ public class AuthStateProvider(
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).AcceptInvite(payload, cancellation.GetValueOrDefault());
|
||||
var account = new BTCPayAccount(serverUrl, response.Email!);
|
||||
await SetAccount(account);
|
||||
var account = await GetAccount(serverUrl, response.Email);
|
||||
await SetCurrentAccount(account);
|
||||
var message = "Invitation accepted.";
|
||||
if (response.EmailHasBeenConfirmed is true)
|
||||
message += " Your email has been confirmed.";
|
||||
@ -237,22 +236,9 @@ public class AuthStateProvider(
|
||||
: " Please set your password.";
|
||||
return new FormResult<AcceptInviteResult>(true, message, response);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new FormResult<AcceptInviteResult>(false, "Invalid invitation.", null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).LoginInfo(email, cancellation.GetValueOrDefault());
|
||||
return new FormResult<LoginInfoResult>(true, string.Empty, response);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<LoginInfoResult>(false, e.Message, null);
|
||||
return new FormResult<AcceptInviteResult>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,10 +252,11 @@ public class AuthStateProvider(
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).Login(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
|
||||
await SetAccount(account);
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -278,36 +265,16 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var client = GetClient(serverUrl);
|
||||
var response = await client.Login(code, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
var clientWithToken = GetClient(serverUrl, response.AccessToken);
|
||||
var userInfo = await clientWithToken.GetUserInfo();
|
||||
email = userInfo?.Email!;
|
||||
}
|
||||
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult(false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> AddAccountWithEncyptionKey(string serverUrl, string email, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var account = new BTCPayAccount(serverUrl, email);
|
||||
await SetAccount(account);
|
||||
await SetEncryptionKey(key);
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -318,31 +285,31 @@ public class AuthStateProvider(
|
||||
|
||||
public async Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default)
|
||||
{
|
||||
var payload = new CreateApplicationUserRequest
|
||||
var payload = new SignupRequest
|
||||
{
|
||||
Email = email,
|
||||
Password = password
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).RegisterUser(payload, cancellation.GetValueOrDefault());
|
||||
var account = new BTCPayAccount(serverUrl, email);
|
||||
var message = "Account created.";
|
||||
if (response.ContainsKey("accessToken"))
|
||||
{
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
if (string.IsNullOrEmpty(access?.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
account.OwnerToken = access.AccessToken;
|
||||
var access = response.ToObject<AccessTokenResponse>();
|
||||
account.SetAccess(access.AccessToken, access.RefreshToken, access.ExpiresIn, expiryOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
var signup = response.ToObject<ApplicationUserData>();
|
||||
if (signup?.RequiresEmailConfirmation is true)
|
||||
var signup = response.ToObject<SignupResult>();
|
||||
if (signup.RequiresConfirmedEmail)
|
||||
message += " Please confirm your email.";
|
||||
if (signup?.RequiresApproval is true)
|
||||
if (signup.RequiresUserApproval)
|
||||
message += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
await SetAccount(account);
|
||||
await SetCurrentAccount(account);
|
||||
return new FormResult(true, message);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -362,14 +329,7 @@ public class AuthStateProvider(
|
||||
try
|
||||
{
|
||||
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
|
||||
var response = await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
|
||||
if (response?.ContainsKey("accessToken") is true)
|
||||
{
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
var account = new BTCPayAccount(serverUrl, email, access!.AccessToken);
|
||||
await SetAccount(account);
|
||||
}
|
||||
|
||||
await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
|
||||
return new FormResult(true, isForgotStep
|
||||
? "You should have received an email with a password reset code."
|
||||
: "Your password has been reset.");
|
||||
@ -398,23 +358,32 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> SwitchMode(string storeId, string mode, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default)
|
||||
{
|
||||
if (Account == null || !string.IsNullOrEmpty(Account.ModeToken))
|
||||
return new FormResult(false, "Cannot switch mode in current state.");
|
||||
|
||||
var payload = new SwitchModeRequest
|
||||
var payload = new UpdateApplicationUserRequest
|
||||
{
|
||||
StoreId = storeId,
|
||||
Mode = mode
|
||||
Email = email,
|
||||
Name = name,
|
||||
ImageUrl = imageUrl
|
||||
};
|
||||
try
|
||||
{
|
||||
var response = await GetClient().SwitchMode(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
var userData = await GetClient().UpdateCurrentUser(payload, cancellation.GetValueOrDefault());
|
||||
_account!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
_userInfo!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
return new FormResult<ApplicationUserData>(true, "Your account info has been changed.", userData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<ApplicationUserData>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
Account.ModeToken = response.AccessToken;
|
||||
await SetAccount(Account);
|
||||
public async Task<FormResult> RefreshAccess(CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient().RefreshAccess(_account!.RefreshToken, cancellation);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -423,53 +392,66 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> SwitchToOwner(string password, string? otp = null, CancellationToken? cancellation = default)
|
||||
private async void OnAccessRefresh(object? sender, AccessTokenResult access)
|
||||
{
|
||||
if (Account == null || string.IsNullOrEmpty(Account.ModeToken) || string.IsNullOrEmpty(Account.OwnerToken))
|
||||
return new FormResult(false, "Cannot switch user in current state.");
|
||||
if (_account == null) return;
|
||||
_account.SetAccess(access.AccessToken, access.RefreshToken, access.Expiry);
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
|
||||
var payload = new LoginRequest
|
||||
private static string GetKey(string accountId) => $"{AccountKeyPrefix}:{accountId}";
|
||||
|
||||
public async Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null)
|
||||
{
|
||||
var prefix = $"{AccountKeyPrefix}:" + (hostFilter == null ? "" : $"{hostFilter}:");
|
||||
var keys = (await _config.List(prefix)).ToArray();
|
||||
var accounts = new List<BTCPayAccount>();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
Email = Account.Email,
|
||||
Password = password,
|
||||
TwoFactorCode = otp
|
||||
};
|
||||
try
|
||||
{
|
||||
var response = await GetClient().Login(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
Account.ModeToken = null;
|
||||
await SetAccount(Account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult(false, e.Message);
|
||||
var account = await _config.Get<BTCPayAccount>(key);
|
||||
accounts.Add(account!);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
public async Task UpdateAccount(BTCPayAccount account)
|
||||
{
|
||||
if (Account == null) return;
|
||||
Account.OwnerToken = Account.ModeToken = null;
|
||||
await SetAccount(Account);
|
||||
await _config.Set(GetKey(account.Id), account, false);
|
||||
}
|
||||
|
||||
private async Task UpdateAccount(BTCPayAccount account)
|
||||
public async Task RemoveAccount(BTCPayAccount account)
|
||||
{
|
||||
await secureProvider.Set(BTCPayAccount.Key, account);
|
||||
await _config.Set<BTCPayAccount>(GetKey(account.Id), null, false);
|
||||
}
|
||||
|
||||
private async Task SetAccount(BTCPayAccount account)
|
||||
private async Task<BTCPayAccount> GetAccount(string serverUrl, string email)
|
||||
{
|
||||
var storeId = CurrentStore?.Id;
|
||||
var accountId = BTCPayAccount.GetId(serverUrl, email);
|
||||
var account = await _config.Get<BTCPayAccount>(GetKey(accountId));
|
||||
return account ?? new BTCPayAccount(serverUrl, email);
|
||||
}
|
||||
|
||||
await UpdateAccount(account);
|
||||
Account = account;
|
||||
UserInfo = null;
|
||||
private async Task<BTCPayAccount?> GetCurrentAccount()
|
||||
{
|
||||
var accountId = await _config.Get<string>(CurrentAccountKey);
|
||||
if (string.IsNullOrEmpty(accountId)) return null;
|
||||
return await _config.Get<BTCPayAccount>(GetKey(accountId));
|
||||
}
|
||||
|
||||
private async Task SetCurrentAccount(BTCPayAccount? account)
|
||||
{
|
||||
OnBeforeAccountChange?.Invoke(this, _account);
|
||||
if (account != null) await UpdateAccount(account);
|
||||
await _config.Set(CurrentAccountKey, account?.Id, false);
|
||||
_account = account;
|
||||
_userInfo = null;
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
OnAfterAccountChange?.Invoke(this, _account);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(storeId)) await SetCurrentStoreId(storeId);
|
||||
private async Task FetchUserInfo(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_userInfo = await GetClient().GetUserInfo(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AuthorizationHandler(IOptionsMonitor<IdentityOptions> identityOptions) : AuthorizationHandler<PolicyRequirement>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity?.AuthenticationType != "Greenfield")
|
||||
return Task.CompletedTask;
|
||||
|
||||
var userId = context.User.Claims.FirstOrDefault(c => c.Type == identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType)?.Value;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Task.CompletedTask;
|
||||
|
||||
var permissionSet = new PermissionSet();
|
||||
var success = false;
|
||||
var isAdmin = context.User.IsInRole("ServerAdmin");
|
||||
var isOwner = context.User.IsInRole("DeviceOwner");
|
||||
var storeId = context.Resource as string;
|
||||
var policy = requirement.Policy;
|
||||
var requiredUnscoped = false;
|
||||
if (policy.EndsWith(':'))
|
||||
{
|
||||
policy = policy[..^1];
|
||||
requiredUnscoped = true;
|
||||
storeId = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
var permissions = context.User.Claims.FirstOrDefault(c => c.Type == storeId)?.Value;
|
||||
if (!string.IsNullOrEmpty(permissions))
|
||||
{
|
||||
permissionSet = new PermissionSet(permissions.Split(',')
|
||||
.Select(s => Permission.TryParse(s, out var permission) ? permission : null)
|
||||
.Where(s => s != null).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
if (Policies.IsServerPolicy(policy) && isAdmin)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsUserPolicy(policy) && !string.IsNullOrEmpty(userId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsStorePolicy(policy) && !string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
if (!success && permissionSet.Contains(policy, storeId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
|
||||
if (!success && requiredUnscoped && string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
else if (Policies.IsPluginPolicy(policy) && policy.StartsWith("btcpay.plugin.app"))
|
||||
{
|
||||
success = isOwner;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
namespace BTCPayApp.Core.Models;
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class FormResult(bool succeeded, string[]? messages = null)
|
||||
{
|
||||
@ -1,33 +1,34 @@
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public interface IAccountManager
|
||||
{
|
||||
public BTCPayAccount? Account { get; }
|
||||
public AppUserInfo? UserInfo { get; }
|
||||
public AppUserStoreInfo? CurrentStore { get; }
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null);
|
||||
public Task<string?> GetEncryptionKey();
|
||||
public Task SetEncryptionKey(string value);
|
||||
public BTCPayAccount? GetAccount();
|
||||
public Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null);
|
||||
public AppUserInfo? GetUserInfo();
|
||||
public BTCPayAppClient GetClient(string? baseUri = null);
|
||||
public Task<bool> CheckAuthenticated(bool refreshUser = false);
|
||||
public Task<bool> IsAuthorized(string policy, object? resource = null);
|
||||
public Task<FormResult> AddAccountWithEncyptionKey(string serverUrl, string email, string key);
|
||||
public Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Login(string serverUrl, string email, string password, string? otp, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> ResetPassword(string serverUrl, string email, string? resetCode, string? newPassword, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<ApplicationUserData>> ChangePassword(string currentPassword, string newPassword, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SwitchMode(string storeId, string mode, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SwitchToOwner(string password, string? otp, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SetCurrentStoreId(string? storeId);
|
||||
public Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false);
|
||||
public Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> RefreshAccess(CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SetCurrentStoreId(string storeId);
|
||||
public Task UnsetCurrentStore();
|
||||
public AppUserStoreInfo? GetCurrentStore();
|
||||
public AppUserStoreInfo? GetUserStore(string storeId);
|
||||
public Task Logout();
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChanged { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnStoreChanged { get; set; }
|
||||
public AsyncEventHandler<string>? OnEncryptionKeyChanged { get; set; }
|
||||
public Task UpdateAccount(BTCPayAccount account);
|
||||
public Task RemoveAccount(BTCPayAccount account);
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
}
|
||||
|
||||
11
BTCPayApp.Core/Auth/Invoice.cs
Normal file
11
BTCPayApp.Core/Auth/Invoice.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class Invoice
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? OrderId { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
10
BTCPayApp.Core/Auth/Notification.cs
Normal file
10
BTCPayApp.Core/Auth/Notification.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class Notification
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string? Body { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
}
|
||||
@ -1,13 +1,48 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAccount(string baseUri, string email, string? ownerToken = null)
|
||||
public class BTCPayAccount(string baseUri, string email)
|
||||
{
|
||||
public const string Key = "account";
|
||||
public string Id { get; private set; } = $"{new Uri(baseUri).Host}:{email}";
|
||||
public static string GetId(string baseUri, string email) => $"{new Uri(baseUri).Host}:{email}";
|
||||
public readonly string Id = GetId(baseUri, email);
|
||||
public string BaseUri { get; private set; } = WithTrailingSlash(baseUri);
|
||||
public string Email { get; private set; } = email;
|
||||
public string? OwnerToken { get; set; } = ownerToken;
|
||||
public string? ModeToken { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTimeOffset? AccessExpiry { get; set; }
|
||||
public string? CurrentStoreId { get; set; }
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, long expiresInSeconds, DateTimeOffset? expiryOffset = null)
|
||||
{
|
||||
var expiry = (expiryOffset ?? DateTimeOffset.Now) + TimeSpan.FromSeconds(expiresInSeconds);
|
||||
SetAccess(accessToken, refreshToken, expiry);
|
||||
}
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, DateTimeOffset expiry)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
AccessExpiry = expiry;
|
||||
}
|
||||
|
||||
public void ClearAccess()
|
||||
{
|
||||
AccessToken = RefreshToken = null;
|
||||
AccessExpiry = null;
|
||||
}
|
||||
|
||||
public void SetInfo(string email, string? name, string? imageUrl)
|
||||
{
|
||||
Email = email;
|
||||
Name = name;
|
||||
ImageUrl = imageUrl;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasTokens => !string.IsNullOrEmpty(AccessToken) && !string.IsNullOrEmpty(RefreshToken);
|
||||
|
||||
private static string WithTrailingSlash(string s)
|
||||
{
|
||||
|
||||
@ -1,43 +1,56 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="AsyncKeyedLock" Version="7.0.0" />
|
||||
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.SqlLite" Version="8.0.3" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.6" />
|
||||
<PackageReference Include="org.ldk" Version="0.0.123" />
|
||||
<PackageReference Include="TypedSignalR.Client" Version="3.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayApp.VSS\BTCPayApp.VSS.csproj" />
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj"/>
|
||||
<PackageReference Include="AsyncKeyedLock" Version="7.1.4"/>
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.0"/>
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.SqlLite" Version="8.1.2"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="NBitcoin" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageReference Include="org.ldk" Version="0.1.2" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SQLite.Maui" Version="1.9.7" />
|
||||
<PackageReference Include="TypedSignalR.Client" Version="3.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.15" />
|
||||
<PackageReference Include="VSS" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,22 +1,129 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using BTCPayApp.Core.Models;
|
||||
using System.Web;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using AccessTokenResponse = BTCPayApp.Core.AspNetRip.AccessTokenResponse;
|
||||
using ProblemDetails = BTCPayApp.Core.AspNetRip.ProblemDetails;
|
||||
using RefreshRequest = BTCPayApp.Core.AspNetRip.RefreshRequest;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient? client = null) : BTCPayServerClient(new Uri(baseUri), apiKey, client)
|
||||
public class BTCPayAppClient(string baseUri) : BTCPayServerClient(new Uri(baseUri))
|
||||
{
|
||||
private const string RefreshPath = "btcpayapp/refresh";
|
||||
private DateTimeOffset? AccessExpiry { get; set; } // TODO: Incorporate in refresh check
|
||||
private string? AccessToken { get; set; }
|
||||
private string? RefreshToken { get; set; }
|
||||
|
||||
public event EventHandler<AccessTokenResult>? AccessRefreshed;
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, DateTimeOffset expiry)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
AccessExpiry = expiry;
|
||||
}
|
||||
|
||||
private void ClearAccess()
|
||||
{
|
||||
AccessToken = RefreshToken = null;
|
||||
AccessExpiry = null;
|
||||
}
|
||||
|
||||
protected override HttpRequestMessage CreateHttpRequest(string path, Dictionary<string, object>? queryPayload = null, HttpMethod? method = null)
|
||||
{
|
||||
var req = base.CreateHttpRequest(path, queryPayload, method);
|
||||
req.Headers.Add("User-Agent", "BTCPayAppClient");
|
||||
req.Headers.Add("Accept", "application/json");
|
||||
if (!string.IsNullOrEmpty(AccessToken))
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override async Task<T> HandleResponse<T>(HttpResponseMessage res)
|
||||
{
|
||||
if (res is { IsSuccessStatusCode: false })
|
||||
{
|
||||
var req = res.RequestMessage;
|
||||
if (res.StatusCode == HttpStatusCode.Unauthorized && !string.IsNullOrEmpty(RefreshToken))
|
||||
{
|
||||
// try refresh and recurse if the token could be renewed
|
||||
var uri = req!.RequestUri;
|
||||
var path = uri!.AbsolutePath;
|
||||
if (!path.EndsWith(RefreshPath))
|
||||
{
|
||||
var (refresh, _) = await RefreshAccess(RefreshToken);
|
||||
if (refresh != null)
|
||||
{
|
||||
if (req.Content is not null)
|
||||
{
|
||||
var content = await req.Content.ReadAsStringAsync();
|
||||
var payload = JsonConvert.DeserializeObject<T>(content);
|
||||
return await SendHttpRequest<T>(path, bodyPayload: payload, method: req.Method);
|
||||
}
|
||||
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
var queryPayload = query.HasKeys() ? query.AllKeys.ToDictionary(k => k, k => query[k]) : null;
|
||||
return await SendHttpRequest<T>(path, queryPayload, method: req.Method);
|
||||
}
|
||||
}
|
||||
ClearAccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
// try parsing as ProblemDetails
|
||||
try
|
||||
{
|
||||
var content = await res.Content.ReadAsStringAsync();
|
||||
var err = JsonConvert.DeserializeObject<ProblemDetails>(content);
|
||||
if (err?.Status != null && !string.IsNullOrEmpty(err.Detail))
|
||||
{
|
||||
var error = new GreenfieldAPIError("unauthorized", err.Detail);
|
||||
throw new GreenfieldAPIException(err.Status.Value, error);
|
||||
}
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
return await base.HandleResponse<T>(res);
|
||||
}
|
||||
|
||||
private AccessTokenResult HandleAccessTokenResponse(AccessTokenResponse response, DateTimeOffset expiryOffset)
|
||||
{
|
||||
var expiry = expiryOffset + TimeSpan.FromSeconds(response.ExpiresIn);
|
||||
SetAccess(response.AccessToken, response.RefreshToken, expiry);
|
||||
return new AccessTokenResult(response.AccessToken, response.RefreshToken, expiry);
|
||||
}
|
||||
|
||||
public async Task<(AccessTokenResult? success, string? errorCode)> RefreshAccess(string? refreshToken = null, CancellationToken? cancellation = default)
|
||||
{
|
||||
var token = refreshToken ?? RefreshToken;
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new ArgumentException("No refresh token present or provided.", nameof(refreshToken));
|
||||
|
||||
var payload = new RefreshRequest { RefreshToken = token };
|
||||
var now = DateTimeOffset.Now;
|
||||
try
|
||||
{
|
||||
var tokenResponse = await SendHttpRequest<AccessTokenResponse>(RefreshPath, bodyPayload: payload, method: HttpMethod.Post);
|
||||
var res = HandleAccessTokenResponse(tokenResponse, now);
|
||||
AccessRefreshed?.Invoke(this, res);
|
||||
return (res, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return (null, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AppInstanceInfo?> GetInstanceInfo(CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AppInstanceInfo>("btcpayapp/instance", null, HttpMethod.Get, cancellation);
|
||||
@ -32,66 +139,30 @@ public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient?
|
||||
return await SendHttpRequest<CreateStoreData>("btcpayapp/create-store", null, HttpMethod.Get, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject> RegisterUser(CreateApplicationUserRequest payload, CancellationToken cancellation = default)
|
||||
public async Task<JObject> RegisterUser(SignupRequest payload, CancellationToken cancellation)
|
||||
{
|
||||
return await SendHttpRequest<JObject>("btcpayapp/register", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LoginInfoResult> LoginInfo(string email, CancellationToken cancellation = default)
|
||||
public async Task<AccessTokenResponse> Login(LoginRequest payload, CancellationToken cancellation)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { { "email", email } };
|
||||
return await SendHttpRequest<LoginInfoResult>("btcpayapp/login-info", payload, HttpMethod.Get, cancellation);
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResponse> Login(LoginRequest payload, CancellationToken cancellation = default)
|
||||
public async Task<AccessTokenResponse> Login(string loginCode, CancellationToken cancellation)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login/code", loginCode, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResponse> SwitchMode(SwitchModeRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/switch-mode", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResponse> Login(string loginCode, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login/code", loginCode, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AcceptInviteResult> AcceptInvite(AcceptInviteRequest payload, CancellationToken cancellation = default)
|
||||
public async Task<AcceptInviteResult> AcceptInvite(AcceptInviteRequest payload, CancellationToken cancellation)
|
||||
{
|
||||
return await SendHttpRequest<AcceptInviteResult>("btcpayapp/accept-invite", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation = default)
|
||||
public async Task ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation)
|
||||
{
|
||||
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
|
||||
var path = isForgotStep ? "btcpayapp/forgot-password" : "btcpayapp/reset-password";
|
||||
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
if (req.DiscountPercent != null) query.Add("discount", req.DiscountPercent.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.Tip != null) query.Add("tip", req.Tip.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.PosData != null) query.Add("posData", req.PosData);
|
||||
return await SendHttpRequest<JObject?>($"apps/{req.AppId}/pos/light", query, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<string> SubmitLNURLWithdrawForInvoice(SubmitLnUrlRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<string>($"plugins/NFC", req, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public virtual async Task<T> UploadFileRequest<T>(string apiPath, StreamContent fileContent, string fileName, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
using MultipartFormDataContent multipartContent = new();
|
||||
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
|
||||
multipartContent.Add(fileContent, "file", fileName);
|
||||
var req = CreateHttpRequest(apiPath, null, HttpMethod.Post);
|
||||
req.Content = multipartContent;
|
||||
using var resp = await _httpClient.SendAsync(req, token);
|
||||
return await HandleResponse<T>(resp);
|
||||
await SendHttpRequest<EmptyResult>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,6 @@ namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAppConfig
|
||||
{
|
||||
public const string Key = "appconfig";
|
||||
public bool RecoveryPhraseVerified { get; set; }
|
||||
public bool UseBiometricAuth { get; set; }
|
||||
public const string Key = "AppConfig";
|
||||
public string? Passcode { get; set; }
|
||||
public string? CurrentStoreId { get; set; }
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public static class AppToServerHelper
|
||||
{
|
||||
|
||||
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningInvoice
|
||||
{
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = lightningPayment.Value,
|
||||
PaymentHash = lightningPayment.PaymentHash?.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
ExpiresAt = lightningPayment.AdditionalData[PaymentsManager.LightningPaymentExpiryKey].GetDateTimeOffset(),
|
||||
PaidAt = lightningPayment.Status == LightningPaymentStatus.Complete
|
||||
? DateTimeOffset.UtcNow
|
||||
: null, //TODO: store these in ln payment
|
||||
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
|
||||
Status = lightningPayment.Status == LightningPaymentStatus.Complete
|
||||
? LightningInvoiceStatus.Paid
|
||||
: lightningPayment.PaymentRequest?.ExpiryDate < DateTimeOffset.UtcNow
|
||||
? LightningInvoiceStatus.Expired
|
||||
: LightningInvoiceStatus.Unpaid,
|
||||
AmountReceived = lightningPayment.Status == LightningPaymentStatus.Complete? lightningPayment.Value: null
|
||||
};
|
||||
}
|
||||
|
||||
public static LightningPayment ToPayment(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningPayment
|
||||
{
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
|
||||
PaymentHash = lightningPayment.PaymentHash?.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
|
||||
Status = lightningPayment.Status,
|
||||
Fee = lightningPayment.AdditionalData.TryGetValue("feePaid", out var feePaid) ? LightMoney.MilliSatoshis((long)feePaid.GetInt64()) : null,
|
||||
CreatedAt = lightningPayment.Timestamp
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<List<LightningPayment>> ToPayments(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToPayment).ToList();
|
||||
}
|
||||
|
||||
public static async Task<List<LightningInvoice>> ToInvoices(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToInvoice).ToList();
|
||||
}
|
||||
}
|
||||
@ -1,237 +0,0 @@
|
||||
using System.Text;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
|
||||
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServiceProvider _serviceProvider)
|
||||
: IBTCPayAppHubClient
|
||||
{
|
||||
public event AsyncEventHandler<string>? OnNewBlock;
|
||||
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
|
||||
public event AsyncEventHandler<string>? OnNotifyNetwork;
|
||||
public event AsyncEventHandler<string>? OnServerNodeInfo;
|
||||
public event AsyncEventHandler<long?>? OnMasterUpdated;
|
||||
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
|
||||
|
||||
private LDKNode? Node => _serviceProvider.GetRequiredService<LightningNodeManager>().Node;
|
||||
private PaymentsManager? PaymentsManager => Node?.PaymentsManager;
|
||||
private LightningAPIKeyManager? ApiKeyManager => Node?.ApiKeyManager;
|
||||
|
||||
public async Task NotifyServerEvent(ServerEvent ev)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerEvent: {Event}", ev.ToString());
|
||||
if (OnNotifyServerEvent is null) return;
|
||||
await OnNotifyServerEvent.Invoke(this, ev);
|
||||
}
|
||||
|
||||
public async Task NotifyNetwork(string network)
|
||||
{
|
||||
_logger.LogInformation("NotifyNetwork: {Network}", network);
|
||||
if (OnNotifyNetwork is null) return;
|
||||
await OnNotifyNetwork.Invoke(this, network);
|
||||
}
|
||||
|
||||
public async Task NotifyServerNode(string nodeInfo)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerNode: {NodeInfo}", nodeInfo);
|
||||
if (OnServerNodeInfo is null) return;
|
||||
await OnServerNodeInfo.Invoke(this, nodeInfo);
|
||||
}
|
||||
|
||||
public async Task TransactionDetected(TransactionDetectedRequest request)
|
||||
{
|
||||
_logger.LogInformation("OnTransactionDetected: {TxId}", request.TxId);
|
||||
if (OnTransactionDetected is null) return;
|
||||
await OnTransactionDetected.Invoke(this, request);
|
||||
}
|
||||
|
||||
public async Task NewBlock(string block)
|
||||
{
|
||||
_logger.LogInformation("NewBlock: {Block}", block);
|
||||
if (OnNewBlock is null) return;
|
||||
await OnNewBlock.Invoke(this, block);
|
||||
}
|
||||
|
||||
public async Task StartListen(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
_serviceProvider
|
||||
.GetRequiredService<LightningNodeManager>().Node?
|
||||
.GetServiceProvider()
|
||||
.GetRequiredService<BTCPayPaymentsNotifier>()
|
||||
.StartListen();
|
||||
}
|
||||
|
||||
private async Task AssertPermission(string key, APIKeyPermission permission)
|
||||
{
|
||||
if (ApiKeyManager is null)
|
||||
throw new HubException("Api Key Manager not available");
|
||||
if (!await ApiKeyManager.CheckPermission(key, permission))
|
||||
throw new HubException("Permission denied");
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
|
||||
false);
|
||||
return (await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
|
||||
createLightningInvoiceRequest.Expiry, descHash)).ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var invoices = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invoices.FirstOrDefault()?.ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var invoices = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invoices.FirstOrDefault()?.ToPayment();
|
||||
}
|
||||
|
||||
public async Task CancelInvoice(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Write);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
await PaymentsManager.CancelInbound(paymentHash);
|
||||
}
|
||||
|
||||
public async Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound))
|
||||
.ToPayments();
|
||||
}
|
||||
|
||||
public async Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound)).ToInvoices();
|
||||
}
|
||||
|
||||
public async Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Write);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var config = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetConfig();
|
||||
var network = config?.NBitcoinNetwork;
|
||||
if (network is null) throw new HubException("Network info not available");
|
||||
|
||||
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
|
||||
try
|
||||
{
|
||||
var result = await PaymentsManager.PayInvoice(bolt,
|
||||
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
|
||||
return new PayResponse
|
||||
{
|
||||
Result = result.Status switch
|
||||
{
|
||||
LightningPaymentStatus.Unknown => PayResult.Unknown,
|
||||
LightningPaymentStatus.Pending => PayResult.Unknown,
|
||||
LightningPaymentStatus.Complete => PayResult.Ok,
|
||||
LightningPaymentStatus.Failed => PayResult.Error,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Details = new PayDetails
|
||||
{
|
||||
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
|
||||
Status = result.Status
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error paying invoice");
|
||||
return new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Task MasterUpdated(long? deviceIdentifier)
|
||||
{
|
||||
_logger.LogInformation("MasterUpdated: {DeviceIdentifier}", deviceIdentifier);
|
||||
OnMasterUpdated?.Invoke(this, deviceIdentifier);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetLightningNodeInfo(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (Node is null) throw new HubException("Lightning Node not available");
|
||||
|
||||
var config = await Node.GetConfig();
|
||||
var peers = await Node.GetPeers();
|
||||
var chans = await Node.GetChannels() ?? [];
|
||||
var channels = chans
|
||||
.Where(channel => channel.channelDetails is not null)
|
||||
.Select(channel => channel.channelDetails)
|
||||
.OfType<ChannelDetails>()
|
||||
.ToArray();
|
||||
var bb = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetBestBlock();
|
||||
return new LightningNodeInformation
|
||||
{
|
||||
Alias = config.Alias,
|
||||
Color = config.Color,
|
||||
Version = "preprepreprealpha",
|
||||
BlockHeight = bb?.BlockHeight ?? 0,
|
||||
PeersCount = peers.Length,
|
||||
ActiveChannelsCount = channels.Count(channel => channel.get_is_usable()),
|
||||
PendingChannelsCount = channels.Count(channel => !channel.get_is_usable() && !channel.get_is_channel_ready()),
|
||||
InactiveChannelsCount = channels.Count(channel => !channel.get_is_usable() && channel.get_is_channel_ready())
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LightningNodeBalance> GetLightningBalance(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (Node is null) throw new HubException("Lightning Node not available");
|
||||
|
||||
var chans = await Node.GetChannels() ?? [];
|
||||
var channels = chans
|
||||
.Where(channel => channel.channelDetails is not null)
|
||||
.Select(channel => channel.channelDetails)
|
||||
.OfType<ChannelDetails>()
|
||||
.ToArray();
|
||||
var balances = Node.ClaimableBalances;
|
||||
var closing = balances
|
||||
.Where(b => b is Balance.Balance_ClaimableAwaitingConfirmations)
|
||||
.ToArray();
|
||||
return new LightningNodeBalance
|
||||
{
|
||||
OffchainBalance = new OffchainBalance
|
||||
{
|
||||
Local = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_outbound_capacity_msat())),
|
||||
Remote = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_inbound_capacity_msat())),
|
||||
Closing = LightMoney.Satoshis(closing.Sum(balance => balance.claimable_amount_satoshis()))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,389 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayConnectionManager(
|
||||
IServiceProvider serviceProvider,
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface,
|
||||
ConfigProvider configProvider,
|
||||
SyncService syncService)
|
||||
: BaseHostedService(logger), IHubConnectionObserver
|
||||
{
|
||||
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
|
||||
private CancellationTokenSource _cts = new();
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private IDisposable? _subscription;
|
||||
private IBTCPayAppHubServer? _hubProxy;
|
||||
public IBTCPayAppHubServer? HubProxy
|
||||
{
|
||||
get => Connection?.State == HubConnectionState.Connected ? _hubProxy : null;
|
||||
private set => _hubProxy = value;
|
||||
}
|
||||
private HubConnection? Connection { get; set; }
|
||||
public Network? ReportedNetwork { get; private set; }
|
||||
public string? ReportedNodeInfo { get; set; }
|
||||
private bool ForceSlaveMode { get; set; }
|
||||
public bool RunningInBackground { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
|
||||
|
||||
public BTCPayConnectionState ConnectionState
|
||||
{
|
||||
get => _connectionState;
|
||||
private set
|
||||
{
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
if (_connectionState == value) return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
logger.LogInformation("Connection state changed{BgInfo}: {Old} -> {ConnectionState}", BgInfo, old, _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
ConnectionChanged += OnConnectionChanged;
|
||||
authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
|
||||
btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
|
||||
btcPayAppServerClient.OnMasterUpdated += OnMasterUpdated;
|
||||
accountManager.OnEncryptionKeyChanged += OnEncryptionKeyChanged;
|
||||
await OnConnectionChanged(this, (BTCPayConnectionState.Init, BTCPayConnectionState.Init));
|
||||
}
|
||||
|
||||
private async Task OnMasterUpdated(object? sender, long? masterId)
|
||||
{
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
if (masterId is null && ConnectionState == BTCPayConnectionState.ConnectedAsSecondary && !ForceSlaveMode)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Syncing slave {DeviceId}", BgInfo, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
else if (deviceId == masterId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Setting master to {DeviceId}", BgInfo, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.ConnectedAsPrimary;
|
||||
}
|
||||
else if (ConnectionState == BTCPayConnectionState.ConnectedAsPrimary && masterId != deviceId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: New master {MasterId} - Device: {DeviceId}", BgInfo, masterId, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task OnEncryptionKeyChanged(object? sender, string encryptionKey)
|
||||
{
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.WaitingForEncryptionKey)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}, _cts.Token);
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
}
|
||||
|
||||
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
var deviceIdentifier = await configProvider.GetDeviceIdentifier();
|
||||
var newState = e.New;
|
||||
try
|
||||
{
|
||||
var account = accountManager.Account;
|
||||
switch (e.New)
|
||||
{
|
||||
case BTCPayConnectionState.Init:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
case BTCPayConnectionState.WaitingForAuth:
|
||||
await syncService.StopSync();
|
||||
if (account is not null && await accountManager.CheckAuthenticated())
|
||||
{
|
||||
newState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.Connecting:
|
||||
if (account is null)
|
||||
{
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
await Kill();
|
||||
var url = new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString();
|
||||
var connection = new HubConnectionBuilder()
|
||||
.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(options.PayloadSerializerSettings);
|
||||
options.PayloadSerializerSettings.Converters.Add(new global::BTCPayServer.Lightning.JsonConverters.LightMoneyJsonConverter());
|
||||
})
|
||||
.WithUrl(url, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () =>
|
||||
Task.FromResult(accountManager.Account?.OwnerToken);
|
||||
options.HttpMessageHandlerFactory = serviceProvider
|
||||
.GetService<Func<HttpMessageHandler, HttpMessageHandler>>();
|
||||
options.WebSocketConfiguration =
|
||||
serviceProvider.GetService<Action<ClientWebSocketOptions>>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
_subscription = connection.Register(btcPayAppServerClientInterface);
|
||||
HubProxy = new ExceptionWrappedHubProxy(connection, logger);
|
||||
|
||||
if (connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Closed += OnClosed;
|
||||
connection.Reconnected += OnReconnected;
|
||||
connection.Reconnecting += OnReconnecting;
|
||||
await connection.StartAsync();
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await accountManager.Logout();
|
||||
logger.LogInformation("Signed out user because of unauthorized response{BgInfo}", BgInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (ex is not TaskCanceledException)
|
||||
logger.LogError("Error while connecting to hub{BgInfo}: {Message}", BgInfo, ex.Message);
|
||||
}
|
||||
}
|
||||
Connection = connection;
|
||||
newState = Connection.State switch
|
||||
{
|
||||
HubConnectionState.Connected => BTCPayConnectionState.Syncing,
|
||||
HubConnectionState.Connecting => BTCPayConnectionState.Connecting,
|
||||
_ => BTCPayConnectionState.WaitingForAuth
|
||||
};
|
||||
break;
|
||||
case BTCPayConnectionState.Syncing:
|
||||
await syncService.StopSync();
|
||||
if (await syncService.EncryptionKeyRequiresImport())
|
||||
{
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
logger.LogWarning(
|
||||
"Existing state found but encryption key is missing, waiting until key is provided");
|
||||
}
|
||||
else
|
||||
{
|
||||
//check if we are the master previously to process outbox items
|
||||
var masterDevice = await HubProxy!.GetCurrentMaster();
|
||||
if (deviceIdentifier == masterDevice)
|
||||
{
|
||||
logger.LogInformation("Syncing master to remote{BgInfo}: {DeviceId}", BgInfo, deviceIdentifier);
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Syncing to local{BgInfo}. Master: {MasterId} - Device: {DeviceId}", BgInfo, masterDevice, deviceIdentifier);
|
||||
await syncService.SyncToLocal();
|
||||
}
|
||||
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;
|
||||
|
||||
var config = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key);
|
||||
if (!string.IsNullOrEmpty(config?.CurrentStoreId))
|
||||
{
|
||||
await accountManager.SetCurrentStoreId(config.CurrentStoreId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedFinishedInitialSync:
|
||||
if (ForceSlaveMode)
|
||||
{
|
||||
await HubProxy!.DeviceMasterSignal(deviceIdentifier, false);
|
||||
ForceSlaveMode = false;
|
||||
newState = BTCPayConnectionState.ConnectedAsSecondary;
|
||||
}
|
||||
else if (!await HubProxy!.DeviceMasterSignal(deviceIdentifier, true))
|
||||
{
|
||||
newState = BTCPayConnectionState.ConnectedAsSecondary;
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsPrimary:
|
||||
await syncService.StartSync(false);
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSecondary:
|
||||
await syncService.StartSync(true);
|
||||
break;
|
||||
case BTCPayConnectionState.Disconnected:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Security.Cryptography.CryptographicException ex) when (newState is BTCPayConnectionState.Syncing or BTCPayConnectionState.Connecting)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = Task.Run(() => ConnectionState = newState);
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnServerNodeInfo(object? sender, string? e)
|
||||
{
|
||||
ReportedNodeInfo = e;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
logger.LogInformation("OnNotifyServerEvent{BgInfo}: {Type} - {Details}", BgInfo, e.Type, e.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnNotifyNetwork(object? sender, string e)
|
||||
{
|
||||
ReportedNetwork = Network.GetNetwork(e);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
{
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
var authState = await accountManager.CheckAuthenticated();
|
||||
if (ConnectionState == BTCPayConnectionState.WaitingForAuth && authState)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
else if (ConnectionState > BTCPayConnectionState.WaitingForAuth && !authState)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while handling authentication state change{BgInfo}", BgInfo);
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task Kill()
|
||||
{
|
||||
if (Connection is not null)
|
||||
{
|
||||
logger.LogWarning("Killing connection{BgInfo}", BgInfo);
|
||||
}
|
||||
var conn = Connection;
|
||||
Connection = null;
|
||||
if (conn is not null)
|
||||
{
|
||||
conn.Closed -= OnClosed;
|
||||
conn.Reconnected -= OnReconnected;
|
||||
conn.Reconnecting -= OnReconnecting;
|
||||
|
||||
await conn.StopAsync();
|
||||
}
|
||||
_subscription?.Dispose();
|
||||
_subscription = null;
|
||||
HubProxy = null;
|
||||
await syncService.StopSync();
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}{BgInfo}", deviceId, BgInfo);
|
||||
await syncService.StopSync();
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
if (HubProxy is not null)
|
||||
{
|
||||
await HubProxy.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
await Kill();
|
||||
authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
btcPayAppServerClient.OnNotifyNetwork -= OnNotifyNetwork;
|
||||
accountManager.OnEncryptionKeyChanged -= OnEncryptionKeyChanged;
|
||||
ConnectionChanged -= OnConnectionChanged;
|
||||
}
|
||||
|
||||
public Task OnClosed(Exception? ex)
|
||||
{
|
||||
logger.LogError("Hub connection closed{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
if (Connection?.State == HubConnectionState.Disconnected && ConnectionState != BTCPayConnectionState.Connecting)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnected(string? connectionId)
|
||||
{
|
||||
logger.LogInformation("Hub connection reconnected{BgInfo}", BgInfo);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnecting(Exception? ex)
|
||||
{
|
||||
logger.LogWarning("Hub connection reconnecting{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SwitchToSecondary()
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
ForceSlaveMode = true;
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}", deviceId);
|
||||
await syncService.StopSync();
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
await HubProxy!.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
private string BgInfo => RunningInBackground ? " (in background mode)" : string.Empty;
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
Disconnected,
|
||||
WaitingForAuth,
|
||||
Connecting,
|
||||
Syncing,
|
||||
WaitingForEncryptionKey,
|
||||
ConnectedAsPrimary,
|
||||
ConnectedAsSecondary,
|
||||
ConnectedFinishedInitialSync
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager,
|
||||
BTCPayConnectionManager connectionManager)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private bool _listening;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
connectionManager.ConnectionChanged += ConnectionManagerOnConnectionChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task ConnectionManagerOnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
_listening = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
|
||||
{
|
||||
if (!_listening || connectionManager.HubProxy is null) return;
|
||||
await connectionManager.HubProxy.SendInvoiceUpdate(e.ToInvoice());
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StartListen()
|
||||
{
|
||||
_listening = true;
|
||||
}
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
|
||||
{
|
||||
private readonly IBTCPayAppHubServer _hubProxy;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ExceptionWrappedHubProxy(HubConnection connection, ILogger logger)
|
||||
{
|
||||
_hubProxy = connection.CreateHubProxy<IBTCPayAppHubServer>();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<T> Wrap<T>(Func<Task<T>> func)
|
||||
{
|
||||
return await AsyncExtensions.RunInOtherThread(async () =>
|
||||
{
|
||||
// executes in thread pool
|
||||
try
|
||||
{
|
||||
return await func();
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
_logger.LogError(e, $"Error while calling hub method");
|
||||
return default!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while calling hub method");
|
||||
return default!;
|
||||
|
||||
}
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
public async Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.DeviceMasterSignal(deviceIdentifier, active));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> Pair(PairRequest request)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.Pair(request));
|
||||
}
|
||||
|
||||
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.Handshake(request));
|
||||
}
|
||||
|
||||
public async Task<bool> BroadcastTransaction(string tx)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.BroadcastTransaction(tx));
|
||||
}
|
||||
|
||||
public async Task<decimal> GetFeeRate(int blockTarget)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetFeeRate(blockTarget));
|
||||
}
|
||||
|
||||
public async Task<BestBlockResponse?> GetBestBlock()
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetBestBlock());
|
||||
}
|
||||
|
||||
public async Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.FetchTxsAndTheirBlockHeads(identifier, txIds, outpoints));
|
||||
}
|
||||
|
||||
public async Task<ScriptResponse> DeriveScript(string identifier)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.DeriveScript(identifier));
|
||||
}
|
||||
|
||||
public async Task TrackScripts(string identifier, string[] scripts)
|
||||
{
|
||||
await Wrap(() => Task.FromResult(_hubProxy.TrackScripts(identifier, scripts)));
|
||||
}
|
||||
|
||||
public async Task<string> UpdatePsbt(string[] identifiers, string psbt)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.UpdatePsbt(identifiers, psbt));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, CoinResponse[]>> GetUTXOs(string[] identifiers)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetUTXOs(identifiers));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetTransactions(identifiers));
|
||||
}
|
||||
|
||||
public async Task SendInvoiceUpdate(LightningInvoice lightningInvoice)
|
||||
{
|
||||
await Wrap(() => Task.FromResult(_hubProxy.SendInvoiceUpdate(lightningInvoice)));
|
||||
}
|
||||
|
||||
public async Task<long?> GetCurrentMaster()
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetCurrentMaster());
|
||||
}
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
//methods available on the hub in the client
|
||||
public interface IBTCPayAppHubClient
|
||||
{
|
||||
Task NotifyServerEvent(ServerEvent ev);
|
||||
Task NotifyNetwork(string network);
|
||||
Task NotifyServerNode(string nodeInfo);
|
||||
Task TransactionDetected(TransactionDetectedRequest request);
|
||||
Task NewBlock(string block);
|
||||
Task StartListen(string key);
|
||||
|
||||
Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest);
|
||||
Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash);
|
||||
Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash);
|
||||
Task CancelInvoice(string key, uint256 paymentHash);
|
||||
Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request);
|
||||
Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request);
|
||||
Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi);
|
||||
Task MasterUpdated(long? deviceIdentifier);
|
||||
Task<LightningNodeInformation> GetLightningNodeInfo(string key);
|
||||
Task<LightningNodeBalance> GetLightningBalance(string key);
|
||||
}
|
||||
|
||||
//methods available on the hub in the server
|
||||
public interface IBTCPayAppHubServer
|
||||
{
|
||||
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
|
||||
Task<Dictionary<string,string>> Pair(PairRequest request);
|
||||
Task<AppHandshakeResponse> Handshake(AppHandshake request);
|
||||
Task<bool> BroadcastTransaction(string tx);
|
||||
Task<decimal> GetFeeRate(int blockTarget);
|
||||
Task<BestBlockResponse?> GetBestBlock();
|
||||
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints);
|
||||
Task<ScriptResponse> DeriveScript(string identifier);
|
||||
Task TrackScripts(string identifier, string[] scripts);
|
||||
Task<string> UpdatePsbt(string[] identifiers, string psbt);
|
||||
Task<Dictionary<string, CoinResponse[]>> GetUTXOs(string[] identifiers);
|
||||
Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers);
|
||||
Task SendInvoiceUpdate(LightningInvoice lightningInvoice);
|
||||
Task<long?> GetCurrentMaster();
|
||||
}
|
||||
|
||||
public class ServerEvent
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public string? StoreId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? AppId { get; set; }
|
||||
public string? InvoiceId { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
public record TxResp
|
||||
{
|
||||
public string TransactionId { get; set; } = null!;
|
||||
public long Confirmations { get; set; }
|
||||
public long? Height { get; set; }
|
||||
public decimal BalanceChange { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{ Confirmations = {Confirmations}, Height = {Height}, BalanceChange = {BalanceChange}, Timestamp = {Timestamp}, TransactionId = {TransactionId} }}";
|
||||
}
|
||||
}
|
||||
|
||||
public class TransactionDetectedRequest
|
||||
{
|
||||
public string? Identifier { get; set; }
|
||||
public string? TxId { get; set; }
|
||||
public string[]? SpentScripts { get; set; }
|
||||
public string[]? ReceivedScripts { get; set; }
|
||||
public bool Confirmed { get; set; }
|
||||
}
|
||||
|
||||
public class CoinResponse
|
||||
{
|
||||
public bool Confirmed { get; set; }
|
||||
public string? Script { get; set; }
|
||||
public string? Outpoint { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
public class TxInfoResponse
|
||||
{
|
||||
public Dictionary<string,TransactionResponse>? Txs { get; set; }
|
||||
public Dictionary<string,string>? BlockHeaders { get; set; }
|
||||
public Dictionary<string,int>? BlockHeights { get; set; }
|
||||
}
|
||||
|
||||
public class TransactionResponse
|
||||
{
|
||||
public string? BlockHash { get; set; }
|
||||
public string? Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class BestBlockResponse
|
||||
{
|
||||
public string? BlockHash { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string? BlockHeader { get; set; }
|
||||
}
|
||||
|
||||
public class ScriptResponse
|
||||
{
|
||||
public string Script { get; set; } = null!;
|
||||
public string KeyPath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class AppHandshake
|
||||
{
|
||||
public string[]? Identifiers { get; set; }
|
||||
}
|
||||
|
||||
//response about identifiers being tracked successfully
|
||||
public class AppHandshakeResponse
|
||||
{
|
||||
public string[]? IdentifiersAcknowledged { get; set; }
|
||||
}
|
||||
|
||||
public class PairRequest
|
||||
{
|
||||
public Dictionary<string, DerivationItem> Derivations { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DerivationItem
|
||||
{
|
||||
public string? Descriptor { get; set; }
|
||||
public int Index { get; set; }
|
||||
public OutPoint[] KnownCoins { get; set; } = [];
|
||||
}
|
||||
@ -1,423 +0,0 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using VSS;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
|
||||
public class SyncService(
|
||||
ConfigProvider configProvider,
|
||||
ILogger<SyncService> logger,
|
||||
IAccountManager accountManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
: IDisposable
|
||||
{
|
||||
public AsyncEventHandler<(List<Outbox> OutboxItemsProcesed, PutObjectRequest RemoteRequest)>? RemoteObjectUpdated;
|
||||
public AsyncEventHandler<string[]>? LocalUpdated;
|
||||
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
|
||||
private async Task<IDataProtector?> GetDataProtector()
|
||||
{
|
||||
var key = await accountManager.GetEncryptionKey();
|
||||
return string.IsNullOrEmpty(key) ? null : new SingleKeyDataProtector(Convert.FromHexString(key));
|
||||
}
|
||||
|
||||
public async Task<bool> EncryptionKeyRequiresImport()
|
||||
{
|
||||
var dataProtector = await GetDataProtector();
|
||||
if (dataProtector is not null)
|
||||
return false;
|
||||
|
||||
var api = await GetUnencryptedVSSAPI();
|
||||
try
|
||||
{
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
|
||||
if (res.Value is null or {Value.Length: 0})
|
||||
return false;
|
||||
|
||||
if (dataProtector is null)
|
||||
return true;
|
||||
|
||||
var decrypted = dataProtector.Unprotect(res.Value.ToByteArray());
|
||||
return "kukks" == Encoding.UTF8.GetString(decrypted);
|
||||
}
|
||||
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while checking if encryption key requires import");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SetEncryptionKey(Mnemonic mnemonic)
|
||||
{
|
||||
var key = mnemonic.DeriveExtKey().Derive(1337).PrivateKey.ToBytes();
|
||||
return await SetEncryptionKey(Convert.ToHexString(key));
|
||||
}
|
||||
|
||||
public async Task<bool> SetEncryptionKey(string key)
|
||||
{
|
||||
if (key.Contains(' ')) return await SetEncryptionKey(new Mnemonic(key));
|
||||
|
||||
var dataProtector = new SingleKeyDataProtector(Convert.FromHexString(key));
|
||||
var encrypted = dataProtector.Protect("kukks"u8.ToArray());
|
||||
var api = await GetUnencryptedVSSAPI();
|
||||
|
||||
try
|
||||
{
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
|
||||
if (res.Value is {Value.Length: > 0})
|
||||
{
|
||||
var decrypted = dataProtector.Unprotect(res.Value.Value.ToByteArray());
|
||||
if ("kukks" == Encoding.UTF8.GetString(decrypted))
|
||||
{
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Error while setting encryption key: {Message}", e.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
await api.PutObjectAsync(new PutObjectRequest
|
||||
{
|
||||
GlobalVersion = await configProvider.GetDeviceIdentifier(),
|
||||
TransactionItems =
|
||||
{
|
||||
new KeyValue
|
||||
{
|
||||
Key = "encryptionKeyTest",
|
||||
Value = ByteString.CopyFrom(encrypted)
|
||||
}
|
||||
},
|
||||
});
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task<IVSSAPI> GetUnencryptedVSSAPI()
|
||||
{
|
||||
var account = accountManager.Account;
|
||||
if (account is null)
|
||||
throw new InvalidOperationException("Account not found");
|
||||
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
|
||||
var httpClient = httpClientFactory.CreateClient("vss");
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", account.OwnerToken);
|
||||
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
|
||||
return Task.FromResult<IVSSAPI>(vssClient);
|
||||
}
|
||||
|
||||
private async Task<IVSSAPI?> GetVSSAPI()
|
||||
{
|
||||
var dataProtector = await GetDataProtector();
|
||||
return dataProtector is null ? null : new VSSApiEncryptorClient(await GetUnencryptedVSSAPI(), dataProtector);
|
||||
}
|
||||
|
||||
private static async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
|
||||
{
|
||||
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue
|
||||
{
|
||||
Key = setting.EntityKey,
|
||||
Version = setting.Version
|
||||
});
|
||||
var channels = dbContext.LightningChannels.Select(channel => new KeyValue
|
||||
{
|
||||
Key = channel.EntityKey,
|
||||
Version = channel.Version
|
||||
});
|
||||
var payments = dbContext.LightningPayments.Select(payment => new KeyValue
|
||||
{
|
||||
Key = payment.EntityKey,
|
||||
Version = payment.Version
|
||||
});
|
||||
return await settings.Concat(channels).Concat(payments).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task SyncToLocal(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backupApi = await GetVSSAPI();
|
||||
if (backupApi is null)
|
||||
return;
|
||||
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var localVersions = await CreateLocalVersions(db);
|
||||
|
||||
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
|
||||
await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var triggers = await db.Database
|
||||
.SqlQuery<TriggerRecord>($"SELECT name, sql FROM sqlite_master WHERE type = 'trigger'")
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
string.Join("; ", triggers.Select(trigger => $"DROP TRIGGER IF EXISTS {trigger.name}")),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
// delete local versions that are not in remote
|
||||
// delete local versions which are lower than remote
|
||||
|
||||
var toDelete = localVersions.Where(localVersion =>
|
||||
remoteVersions.KeyVersions.All(remoteVersion => remoteVersion.Key != localVersion.Key)
|
||||
|| remoteVersions.KeyVersions.All(remoteVersion =>
|
||||
remoteVersion.Key == localVersion.Key && remoteVersion.Version > localVersion.Version)).ToArray();
|
||||
|
||||
var toUpsert = remoteVersions.KeyVersions.Where(remoteVersion => localVersions.All(localVersion =>
|
||||
localVersion.Key != remoteVersion.Key || localVersion.Version < remoteVersion.Version)).Where(value => value.Key != "encryptionKeyTest").ToArray();
|
||||
|
||||
if (toDelete.Length == 0 && toUpsert.Length == 0)
|
||||
return;
|
||||
logger.LogInformation("Syncing to local: {ToDelete} to delete, {ToUpsert} to upsert", toDelete.Length,
|
||||
toUpsert.Length);
|
||||
|
||||
foreach (var upsertItem in toUpsert)
|
||||
{
|
||||
if (upsertItem.Value is not (null or { Length: 0 })) continue;
|
||||
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
|
||||
{
|
||||
Key = upsertItem.Key,
|
||||
}, cancellationToken);
|
||||
upsertItem.MergeFrom(item.Value);
|
||||
}
|
||||
|
||||
var settingsToDelete = toDelete.Where(key => key.Key.StartsWith("Setting_")).Select(key => key.Key);
|
||||
var channelsToDelete = toDelete.Where(key => key.Key.StartsWith("Channel_")).Select(key => key.Key);
|
||||
var paymentsToDelete = toDelete.Where(key => key.Key.StartsWith("Payment_")).Select(key => key.Key);
|
||||
var deleteCount = 0;
|
||||
deleteCount += await db.Settings.Where(setting => settingsToDelete.Contains(setting.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
deleteCount += await db.LightningChannels.Where(channel => channelsToDelete.Contains(channel.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
deleteCount += await db.LightningPayments.Where(payment => paymentsToDelete.Contains(payment.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
|
||||
// upsert the rest when needed
|
||||
var settingsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Setting_")).Select(setting => new Setting()
|
||||
{
|
||||
Key = setting.Key.Replace("Setting_", ""),
|
||||
Value = setting.Value.ToByteArray(),
|
||||
Version = setting.Version,
|
||||
Backup = true
|
||||
}).ToArray();
|
||||
var channelsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Channel_"))
|
||||
.Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!);
|
||||
var paymentsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Payment_")).Select(value =>
|
||||
JsonSerializer.Deserialize<AppLightningPayment>(value.Value.ToStringUtf8())!);
|
||||
var upsertCount = 0;
|
||||
upsertCount += await db.Settings.UpsertRange(settingsToUpsert).On(setting => setting.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
upsertCount += await db.LightningChannels.UpsertRange(channelsToUpsert).On(channel => channel.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
upsertCount += await db.LightningPayments.UpsertRange(paymentsToUpsert).On(payment => payment.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync(string.Join("; ", triggers.Select(record => record.sql)),
|
||||
cancellationToken: cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.Database.CommitTransactionAsync(cancellationToken);
|
||||
logger.LogInformation("Synced to local: {DeleteCount} deleted, {UpsertCount} upserted", deleteCount,
|
||||
upsertCount);
|
||||
LocalUpdated?.Invoke(this, toDelete.Concat(toUpsert).Select(key => key.Key).ToArray());
|
||||
settingsToUpsert.Select(setting => setting.Key).Concat(settingsToDelete).Distinct().ToList()
|
||||
.ForEach(key => configProvider.Updated?.Invoke(this, key));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await db.Database.RollbackTransactionAsync(cancellationToken);
|
||||
logger.LogError(e, "Error while syncing to local");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
{
|
||||
switch (outbox.Entity)
|
||||
{
|
||||
case "Setting":
|
||||
var setting = await dbContext.Settings.SingleOrDefaultAsync(setting1 =>
|
||||
setting1.EntityKey == outbox.Key && setting1.Backup);
|
||||
if (setting == null)
|
||||
return null;
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(setting.Value),
|
||||
Version = setting.Version
|
||||
};
|
||||
case "Channel":
|
||||
var channel = await dbContext.LightningChannels.Include(channel1 => channel1.Aliases)
|
||||
.SingleOrDefaultAsync(channel1 => channel1.EntityKey == outbox.Key);
|
||||
|
||||
if (channel == null)
|
||||
return null;
|
||||
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
|
||||
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(val),
|
||||
Version = channel.Version
|
||||
};
|
||||
case "Payment":
|
||||
var payment = await dbContext.LightningPayments.SingleOrDefaultAsync(lightningPayment =>
|
||||
lightningPayment.EntityKey == outbox.Key);
|
||||
if (payment == null)
|
||||
return null;
|
||||
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(paymentBytes),
|
||||
Version = payment.Version
|
||||
};
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SyncToRemote(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _syncLock.WaitAsync(cancellationToken);
|
||||
|
||||
var backupAPi = await GetVSSAPI();
|
||||
if (backupAPi is null)
|
||||
return;
|
||||
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var putObjectRequest = new PutObjectRequest
|
||||
{
|
||||
GlobalVersion = await configProvider.GetDeviceIdentifier()
|
||||
};
|
||||
var outbox = await db.OutboxItems.GroupBy(outbox1 => outbox1.Key)
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
if (outbox.Count != 0)
|
||||
{
|
||||
logger.LogInformation("Syncing to remote {Count} outbox items", outbox.Count);
|
||||
}
|
||||
var removedOutboxItems = new List<Outbox>();
|
||||
foreach (var outboxItemSet in outbox)
|
||||
{
|
||||
var orderedEnumerable = outboxItemSet.OrderByDescending(outbox1 => outbox1.Version)
|
||||
.ThenByDescending(outbox1 => outbox1.ActionType).ToArray();
|
||||
foreach (var item in orderedEnumerable)
|
||||
{
|
||||
if (item.ActionType == OutboxAction.Delete)
|
||||
{
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
{
|
||||
Key = item.Key, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
removedOutboxItems.AddRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
|
||||
if (putObjectRequest.TransactionItems.Count == 0 && putObjectRequest.DeleteItems.Count == 0 && _syncTask is not null) return;
|
||||
|
||||
await backupAPi.PutObjectAsync(putObjectRequest, cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Synced to remote {TransactionItemsCount} items and deleted {DeleteItemsCount} items {Join}",
|
||||
putObjectRequest.TransactionItems.Count,
|
||||
putObjectRequest.DeleteItems.Count,
|
||||
string.Join(", ", putObjectRequest.TransactionItems.Select(kv => kv.Key + " " + kv.Version)));
|
||||
RemoteObjectUpdated?.Invoke(this, (removedOutboxItems, putObjectRequest.Clone()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
}
|
||||
public async Task StartSync(bool local,CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_syncTask.HasValue && _syncTask.Value.local == local && !_syncTask.Value.cts.IsCancellationRequested)
|
||||
return;
|
||||
if (_syncTask.HasValue && _syncTask.Value.local != local)
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_syncTask = (ContinuouslySync(local, cts.Token), cts, local);
|
||||
}
|
||||
|
||||
public async Task StopSync()
|
||||
{
|
||||
if (_syncTask.HasValue)
|
||||
{
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
_syncTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ContinuouslySync(bool local, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (local)
|
||||
await SyncToLocal(cancellationToken);
|
||||
else
|
||||
await SyncToRemote(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while syncing to {Target}", local ? "local" : "remote");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RemoteObjectUpdated = null;
|
||||
LocalUpdated = null;
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const string LoginCodeSeparator = ";";
|
||||
public const string EncryptionKeySeparator = "*";
|
||||
public const string InviteSeparator = "/invite/";
|
||||
public const string POSQRLoginSeparator = "loginCode";
|
||||
}
|
||||
@ -1,16 +1,8 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public abstract class ConfigProvider : IDisposable
|
||||
public interface IConfigProvider
|
||||
{
|
||||
public abstract Task<T?> Get<T>(string key);
|
||||
public abstract Task Set<T>(string key, T? value, bool backup);
|
||||
public abstract Task<IEnumerable<string>> List(string prefix);
|
||||
public AsyncEventHandler<string>? Updated;
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Updated = null;
|
||||
}
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value, bool backup);
|
||||
Task<IEnumerable<string>> List(string prefix);
|
||||
}
|
||||
@ -3,5 +3,4 @@
|
||||
public interface IDataDirectoryProvider
|
||||
{
|
||||
Task<string> GetAppDataDirectory();
|
||||
Task<string> GetCacheDirectory();
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BTCPayApp.Core.Contracts
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null);
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
public interface INfcService: IDisposable
|
||||
{
|
||||
event EventHandler<NfcCardData> OnNfcDataReceived;
|
||||
void StartNfc();
|
||||
void EndNfc();
|
||||
}
|
||||
|
||||
public class NfcCardData
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public byte[] Payload { get; set; }
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public interface ISecureConfigProvider
|
||||
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
Pending,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@ -1,49 +1,57 @@
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayServer.Lightning;
|
||||
using Laraue.EfCoreTriggers.Common.Extensions;
|
||||
using Laraue.EfCoreTriggers.Common.TriggerBuilders.Actions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Setting> Settings { get; set; }
|
||||
|
||||
public DbSet<Channel> LightningChannels { get; set; }
|
||||
public DbSet<ChannelAlias> ChannelAliases { get; set; }
|
||||
public DbSet<AppLightningPayment> LightningPayments { get; set; }
|
||||
public DbSet<Outbox> OutboxItems { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Outbox>()
|
||||
.HasKey(w => new {w.Entity, w.Key, w.ActionType, w.Version});
|
||||
modelBuilder.Entity<Outbox>().Property(payment => payment.Timestamp).HasDefaultValueSql("datetime('now')");
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<Setting>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<Channel>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentRequest)
|
||||
.HasConversion(
|
||||
request => request!.ToString(),
|
||||
request => request.ToString(),
|
||||
str => NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network)));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Secret)
|
||||
.HasConversion(
|
||||
request => request!.ToString(),
|
||||
request => request.ToString(),
|
||||
str => uint256.Parse(str));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentHash)
|
||||
.HasConversion(
|
||||
request => request!.ToString(),
|
||||
request => request.ToString(),
|
||||
str => uint256.Parse(str));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Value)
|
||||
.HasConversion(
|
||||
request => request!.MilliSatoshi,
|
||||
request => request.MilliSatoshi,
|
||||
str => new LightMoney(str));
|
||||
|
||||
modelBuilder.Entity<Channel>().Property(channel => channel.AdditionalData).HasJsonConversion();
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.AdditionalData).HasJsonConversion();
|
||||
modelBuilder.Entity<AppLightningPayment>()
|
||||
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
|
||||
@ -69,7 +77,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Condition(@ref => @ref.New.Backup)
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.New.Version,
|
||||
@ -82,7 +90,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Condition(@ref => @ref.Old.Backup)
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Setting" && outbox.Key == @ref.Old.Key,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.Old.Version,
|
||||
@ -95,10 +103,10 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
// .Condition(@ref => @ref.Old.Value != @ref.New.Value)
|
||||
.Update<Setting>(
|
||||
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
|
||||
(tableRefs, setting) => new Setting { Key = tableRefs.Old.Key, Version = tableRefs.Old.Version + 1 })
|
||||
(tableRefs, setting) => new Setting() {Version = tableRefs.Old.Version + 1})
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.Old.Version + 1,
|
||||
@ -122,7 +130,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.New.Version,
|
||||
@ -133,7 +141,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Channel" && outbox.Key == @ref.Old.Id,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version,
|
||||
@ -143,9 +151,9 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Channel>(
|
||||
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
|
||||
(tableRefs, setting) => new Channel { Id = tableRefs.Old.Id, Version = tableRefs.Old.Version + 1 }).Insert(
|
||||
(tableRefs, setting) => new Channel() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version +1,
|
||||
@ -158,7 +166,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Payment" && outbox.Key == @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.New.Version,
|
||||
@ -169,7 +177,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Payment" && outbox.Key == @ref.Old.PaymentHash+ "_"+@ref.Old.PaymentId+ "_"+@ref.Old.Inbound,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version,
|
||||
@ -177,14 +185,14 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group =>
|
||||
|
||||
.Action(group =>
|
||||
|
||||
group.Update<AppLightningPayment>(
|
||||
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
|
||||
(tableRefs, setting) => new AppLightningPayment {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) =>
|
||||
(tableRefs, setting) => new AppLightningPayment() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) =>
|
||||
// outbox.Version != @ref.New.Version || outbox.ActionType != OutboxAction.Update || outbox.Entity != "Payment" || outbox.Key != @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version +1,
|
||||
@ -193,4 +201,4 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
})));
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,13 @@ namespace BTCPayApp.Core.Data;
|
||||
public class AppLightningPayment : VersionedData
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256? PaymentHash { get; set; }
|
||||
public uint256 PaymentHash { get; set; }
|
||||
|
||||
public string? PaymentId { get; set; }
|
||||
public string PaymentId { get; set; }
|
||||
public string? Preimage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256? Secret { get; set; }
|
||||
public uint256 Secret { get; set; }
|
||||
|
||||
public bool Inbound { get; set; }
|
||||
|
||||
@ -23,13 +23,13 @@ public class AppLightningPayment : VersionedData
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney? Value { get; set; }
|
||||
public LightMoney Value { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LightningPaymentStatus Status { get; set; }
|
||||
|
||||
[JsonConverter(typeof(BOLT11PaymentRequestJsonConverter))]
|
||||
public BOLT11PaymentRequest? PaymentRequest { get; set; }
|
||||
public BOLT11PaymentRequest PaymentRequest { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
|
||||
|
||||
@ -38,4 +38,4 @@ public class AppLightningPayment : VersionedData
|
||||
get => $"Payment_{PaymentHash}_{PaymentId}_{Inbound}";
|
||||
init { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Channel:VersionedData
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public byte[]? Data { get; set; }
|
||||
public List<ChannelAlias> Aliases { get; set; } = [];
|
||||
public long Checkpoint { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
|
||||
public string Id { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
public List<ChannelAlias> Aliases { get; set; }
|
||||
|
||||
public override string EntityKey
|
||||
{
|
||||
@ -22,11 +17,11 @@ public class Channel:VersionedData
|
||||
|
||||
public class ChannelAlias
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public string? ChannelId { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
[JsonIgnore]
|
||||
public Channel? Channel { get; set; }
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ public class LightningConfig
|
||||
public string ScriptDerivationKey { get; set; } = WalletDerivation.NativeSegwit; //when ldk asks for an address, where do we get it from?
|
||||
public string LightningDerivationPath { get; set; } = "m/666'";// your lightning node derivation path
|
||||
public string Color { get; set; } = "#51B13E";
|
||||
public Uri? RapidGossipSyncUrl { get; set; }
|
||||
|
||||
public string? JITLSP { get; set; } // Just In Time Lightning Service Provider
|
||||
|
||||
@ -18,7 +17,7 @@ public class LightningConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Color)){ return [0,0,0];}
|
||||
if(string.IsNullOrEmpty(Color)){ return [0,0,0];}
|
||||
|
||||
if (Color.StartsWith("#"))
|
||||
{
|
||||
@ -39,8 +38,8 @@ public class LightningConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Dictionary<string, PeerInfo> Peers { get; set; } = new();
|
||||
|
||||
public bool AcceptInboundConnection{ get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogDbContext(DbContextOptions<LogDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<LogEntry> Logs { get; set; }
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Level { get; set; }
|
||||
public DateTime TimeStamp { get; set; }
|
||||
public string RenderedMessage { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public string Properties { get; set; }
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class LoggingConfig
|
||||
{
|
||||
private const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}";
|
||||
|
||||
public static void ConfigureLogging(IServiceCollection serviceCollection)
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
var dirProvider = serviceProvider.GetRequiredService<IDataDirectoryProvider>();
|
||||
var appDir = dirProvider.GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var dbPath = $"{appDir}/logs.db";
|
||||
var isDevEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
|
||||
var minLogLevel = isDevEnv ? LogEventLevel.Verbose : LogEventLevel.Information;
|
||||
|
||||
serviceCollection.AddSerilog();
|
||||
serviceCollection.AddDbContextFactory<LogDbContext>((_, options) =>
|
||||
{
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
});
|
||||
|
||||
/*
|
||||
"LDK": "Trace",
|
||||
"LDK.lightning::ln::peer_handler": "Debug",
|
||||
"LDK.lightning::routing::gossip": "Information",
|
||||
"LDK.BTCPayApp.Core.LDK.LDKPeerHandler": "Information",
|
||||
"LDK.lightning_background_processor": "Information"*/
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SQLite(dbPath)
|
||||
.WriteTo.Console(outputTemplate: OutputTemplate, restrictedToMinimumLevel: minLogLevel)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning).CreateLogger();
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ public class Outbox
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
|
||||
public OutboxAction ActionType { get; set; }
|
||||
public required string Key { get; set; }
|
||||
public required string Entity { get; set; }
|
||||
public required long Version { get; set; }
|
||||
}
|
||||
public string Key { get; set; }
|
||||
public string Entity { get; set; }
|
||||
public long Version { get; set; }
|
||||
}
|
||||
@ -1,11 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,8 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public record PeerInfo
|
||||
{
|
||||
[JsonConverter(typeof(EndPointJsonConverter))]
|
||||
public EndPoint? Endpoint { get; set; }
|
||||
public string Endpoint { get; set; }
|
||||
public bool Persistent { get; set; }
|
||||
public bool Trusted { get; set; }
|
||||
public string? Label { get; set; }
|
||||
}
|
||||
30
BTCPayApp.Core/Data/RemoteToLocalSyncService.cs
Normal file
30
BTCPayApp.Core/Data/RemoteToLocalSyncService.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Attempt2;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
class TriggerRecord
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string sql { get; set; }
|
||||
}
|
||||
public class RemoteToLocalSyncService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
|
||||
public RemoteToLocalSyncService(IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
BTCPayConnectionManager btcPayConnectionManager)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
}
|
||||
|
||||
// on connected to btcpay, sync all the data from the remote to the local
|
||||
// if we are the active node
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -5,8 +5,8 @@ namespace BTCPayApp.Core.Data;
|
||||
public class Setting:VersionedData
|
||||
{
|
||||
[Key]
|
||||
public required string Key { get; set; }
|
||||
public byte[]? Value { get; set; }
|
||||
public string Key { get; set; }
|
||||
public byte[] Value { get; set; }
|
||||
public bool Backup { get; set; } = true;
|
||||
|
||||
public override string EntityKey
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
class TriggerRecord
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string sql { get; set; }
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using NBitcoin;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
@ -10,40 +8,14 @@ public class WalletConfig
|
||||
|
||||
public required string Mnemonic { get; set; }
|
||||
public required string Network { get; set; }
|
||||
|
||||
|
||||
//key is the identifier of the tracker, value is a sub wallet format.
|
||||
//key is the identifier of the tracker, value is a sub wallet format.
|
||||
//for example, we will track native segwit wallet, the descriptor will be wpkh([fingerprint/84'/0'/0']xpub/0/*)
|
||||
// or for LN specifics, the descriptor is null, and we track non deterministic scripts
|
||||
public Dictionary<string, WalletDerivation> Derivations { get; set; } = new();
|
||||
[JsonIgnore]
|
||||
|
||||
public string Fingerprint => new Mnemonic(Mnemonic).DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString();
|
||||
[JsonIgnore]
|
||||
public Network? NBitcoinNetwork => NBitcoin.Network.GetNetwork(Network);
|
||||
|
||||
public required BlockSnapshot Birthday { get; set; }
|
||||
|
||||
public required CoinSnapshot CoinSnapshot { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class CoinSnapshot
|
||||
{
|
||||
public required BlockSnapshot BlockSnapshot { get; set; }
|
||||
public required Dictionary<string, SavedCoin[]> Coins { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class SavedCoin
|
||||
{
|
||||
[JsonConverter(typeof(BitcoinSerializableJsonConverterFactory))]
|
||||
public required OutPoint Outpoint { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
public KeyPath? Path { get; set; }
|
||||
}
|
||||
|
||||
public class BlockSnapshot
|
||||
{
|
||||
public required uint BlockHeight { get; set; }
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public required uint256 BlockHash { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,15 +1,12 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class WalletDerivation
|
||||
{
|
||||
public string Identifier { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? Descriptor { get; set; }
|
||||
|
||||
public const string NativeSegwit = "segwit";
|
||||
public const string LightningScripts = "lightningScripts";
|
||||
// public const string SpendableOutputs = "spendableOutputs";
|
||||
|
||||
public required string Name { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? Descriptor { get; set; }
|
||||
|
||||
// TODO: this is useful when restoring, to tell NBX to generate addresses up to this to prevent address reuse.
|
||||
public int? LastKnownIndex{ get; set; }
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,7 @@
|
||||
// public VSSMapperInterceptor(BTCPayConnectionManager btcPayConnectionManager, ILogger<VSSMapperInterceptor> logger)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private ConcurrentDictionary<EventId, object> PendingEvents = new ConcurrentDictionary<EventId, object>();
|
||||
// public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
|
||||
// CancellationToken cancellationToken = new CancellationToken())
|
||||
@ -39,12 +39,12 @@
|
||||
// {
|
||||
// if (entry.State == EntityState.Deleted)
|
||||
// {
|
||||
//
|
||||
//
|
||||
// api.DeleteObjectAsync(new DeleteObjectRequest
|
||||
// {
|
||||
// KeyValue = new KeyValue()
|
||||
// {
|
||||
//
|
||||
//
|
||||
// }
|
||||
// Key = $"LightningPayment/{lightningPayment.Id}"
|
||||
// });
|
||||
@ -52,14 +52,14 @@
|
||||
// }
|
||||
// if (entry.Entity is Channel channel)
|
||||
// {
|
||||
//
|
||||
//
|
||||
// }
|
||||
// if (entry.Entity is Setting setting)
|
||||
// {
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
// }
|
||||
//
|
||||
@ -76,11 +76,12 @@
|
||||
// PendingEvents.Remove(eventData.EventId, out _);
|
||||
// return base.SaveChangesFailedAsync(eventData, cancellationToken);
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
|
||||
|
||||
using System.Text.Json;
|
||||
using AsyncKeyedLock;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
@ -88,29 +89,32 @@ using BTCPayApp.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class DatabaseConfigProvider(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<DatabaseConfigProvider> logger)
|
||||
: ConfigProvider
|
||||
public class DatabaseConfigProvider: IConfigProvider
|
||||
{
|
||||
private readonly AsyncKeyedLocker<string> _lock = new();
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<DatabaseConfigProvider> _logger;
|
||||
private AsyncKeyedLocker<string> _lock = new();
|
||||
|
||||
public override async Task<T?> Get<T>(string key) where T : default
|
||||
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory, ILogger<DatabaseConfigProvider> logger)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<T?> Get<T>(string key)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var config = await dbContext.Settings.FindAsync(key);
|
||||
if (typeof(T) == typeof(byte[]))
|
||||
return (T?) (config?.Value as object);
|
||||
return config is null ? default : JsonSerializer.Deserialize<T>(config.Value);
|
||||
}
|
||||
|
||||
public override async Task Set<T>(string key, T? value, bool backup) where T : default
|
||||
public async Task Set<T>(string key, T? value, bool backup)
|
||||
{
|
||||
using var releaser = await _lock.LockAsync(key);
|
||||
logger.LogDebug("Setting {Key} to {Value} {Backup}", key, value, backup ? "backup": "no backup");
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
_logger.LogDebug("Setting {key} to {value} {backup}", key, value, backup? "backup": "no backup");
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
if (value is null)
|
||||
{
|
||||
try
|
||||
@ -127,11 +131,12 @@ public class DatabaseConfigProvider(
|
||||
var newValue = typeof(T) == typeof(byte[])? value as byte[]:JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
var setting = new Setting {Key = key, Value = newValue, Backup = backup};
|
||||
await dbContext.Upsert(setting, CancellationToken.None);
|
||||
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<string>> List(string prefix)
|
||||
public async Task<IEnumerable<string>> List(string prefix)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Settings.Where(s => s.Key.StartsWith(prefix)).Select(s => s.Key).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
#if DEBUG
|
||||
|
||||
using System.Net.Security;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace BTCPayApp.Core.Extensions;
|
||||
|
||||
public class DangerousHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public static bool ServerValidate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
|
||||
{
|
||||
if (errors == SslPolicyErrors.None) return true;
|
||||
return certificate?.Subject.Equals("CN=localhost") is true;
|
||||
}
|
||||
|
||||
private static HttpClientHandler GetInsecureHandler()
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
handler.ServerCertificateCustomValidationCallback = ServerValidate;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient(GetInsecureHandler());
|
||||
}
|
||||
}
|
||||
|
||||
#if ANDROID
|
||||
public class DangerousAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
|
||||
{
|
||||
protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
|
||||
=> new CustomHostnameVerifier();
|
||||
|
||||
private sealed class CustomHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier
|
||||
{
|
||||
public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
|
||||
{
|
||||
return session?.PeerPrincipal?.Name == "CN=localhost";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public static class DebugExtensions
|
||||
{
|
||||
public static IServiceCollection AddDangerousSSLSettingsForDev(this IServiceCollection services)
|
||||
{
|
||||
services.Replace(ServiceDescriptor.Singleton<IHttpClientFactory, DangerousHttpClientFactory>());
|
||||
|
||||
services.AddSingleton<Func<HttpMessageHandler, HttpMessageHandler>>(handler =>
|
||||
{
|
||||
if (handler is HttpClientHandler clientHandler)
|
||||
{
|
||||
// always verify the SSL certificate
|
||||
clientHandler.ServerCertificateCustomValidationCallback += DangerousHttpClientFactory.ServerValidate;
|
||||
return clientHandler;
|
||||
}
|
||||
#if ANDROID
|
||||
return new DangerousAndroidMessageHandler();
|
||||
#else
|
||||
return handler;
|
||||
#endif
|
||||
});
|
||||
|
||||
services.AddSingleton<Action<ClientWebSocketOptions>>(provider => wsc =>
|
||||
{
|
||||
wsc.RemoteCertificateValidationCallback = DangerousHttpClientFactory.ServerValidate;
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -6,24 +6,16 @@ namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class AsyncExtensions
|
||||
{
|
||||
public static async Task RunInOtherThread(Action action)
|
||||
|
||||
public static async Task RunSync(this Task task)
|
||||
{
|
||||
await Task.Factory.StartNew(action);
|
||||
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<T> RunInOtherThread<T>(Func<T> action)
|
||||
public static async Task<T> RunSync<T>(this Task<T> task)
|
||||
{
|
||||
return await Task.Factory.StartNew(action);
|
||||
}
|
||||
|
||||
public static async Task RunInOtherThread(this Task task)
|
||||
{
|
||||
await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
}
|
||||
|
||||
public static async Task<T> RunInOtherThread<T>(this Task<T> task)
|
||||
{
|
||||
return await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
/// <summary>
|
||||
/// Allows a cancellation token to be awaited.
|
||||
@ -54,7 +46,7 @@ public static class AsyncExtensions
|
||||
public object GetResult()
|
||||
{
|
||||
// this is called by compiler generated methods when the
|
||||
// task has completed. Instead of returning a result, we
|
||||
// task has completed. Instead of returning a result, we
|
||||
// just throw an exception.
|
||||
if (IsCompleted) throw new OperationCanceledException();
|
||||
else throw new InvalidOperationException("The cancellation token has not yet been cancelled.");
|
||||
@ -72,4 +64,4 @@ public static class AsyncExtensions
|
||||
public void UnsafeOnCompleted(Action continuation) =>
|
||||
CancellationToken.Register(continuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
// Copied from BTCPayServer
|
||||
public static class AuthorizationOptionsExtensions
|
||||
{
|
||||
public static AuthorizationOptions AddPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
// BTCPay policies
|
||||
foreach (var p in Policies.AllPolicies)
|
||||
{
|
||||
options.AddPolicy(p);
|
||||
}
|
||||
options.AddPolicy(Policies.CanModifyStoreSettingsUnscoped);
|
||||
options.AddPolicy(CanGetRates.Key);
|
||||
// app policies
|
||||
foreach (var p in AppPolicies.AllPolicies)
|
||||
{
|
||||
options.AddPolicy(p);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void AddPolicy(this AuthorizationOptions options, string policy)
|
||||
{
|
||||
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
|
||||
}
|
||||
|
||||
private class CanGetRates
|
||||
{
|
||||
public const string Key = "btcpay.store.cangetrates";
|
||||
}
|
||||
}
|
||||
@ -1,42 +1,38 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public abstract class BaseHostedService(ILogger logger) : IHostedService, IDisposable
|
||||
public abstract class BaseHostedService : IHostedService, IDisposable
|
||||
{
|
||||
protected CancellationTokenSource CancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim ControlSemaphore = new(1, 1);
|
||||
protected CancellationTokenSource _cancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim _controlSemaphore = new(1, 1);
|
||||
private Task? _currentTask;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
await _controlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
CancellationTokenSource = new CancellationTokenSource();
|
||||
await ExecuteStartAsync(CancellationTokenSource.CreateLinkedTokenSource(CancellationTokenSource.Token, cancellationToken).Token);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
await ExecuteStartAsync(CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken).Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ControlSemaphore.Release();
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Stopping service");
|
||||
await CancellationTokenSource.CancelAsync();
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
await _controlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await ExecuteStopAsync(CancellationTokenSource.Token);
|
||||
|
||||
logger.LogInformation("Stopped");
|
||||
await ExecuteStopAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ControlSemaphore.Release();
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,20 +41,7 @@ public abstract class BaseHostedService(ILogger logger) : IHostedService, IDispo
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
CancellationTokenSource?.Dispose();
|
||||
ControlSemaphore?.Dispose();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_controlSemaphore?.Dispose();
|
||||
}
|
||||
|
||||
protected async Task WrapInLock(Func<Task> act, CancellationToken cancellationToken)
|
||||
{
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ControlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,10 +9,10 @@ namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ChannelExtensions
|
||||
{
|
||||
|
||||
|
||||
public static IDisposable SubscribeToEventWithChannelQueue<TEvent>(
|
||||
Action<AsyncEventHandler<TEvent>> add,
|
||||
Action<AsyncEventHandler<TEvent>> remove,
|
||||
Action<AsyncEventHandler<TEvent>> remove,
|
||||
Func<TEvent, CancellationToken, Task> processor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@ -26,42 +26,44 @@ public static class ChannelExtensions
|
||||
add(OnEvent);
|
||||
_ = channel.ProcessChannel(processor, cancellationToken);
|
||||
|
||||
return new DisposableWrapper(() =>
|
||||
return new DisposableWrapper(async () =>
|
||||
{
|
||||
remove(OnEvent);
|
||||
channel.Writer.Complete();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task ProcessChannel<TEvent>(this Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
|
||||
public static async Task ProcessChannel<TEvent>(this Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
|
||||
{
|
||||
while (await channel.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
while (channel.Reader.TryRead(out var item))
|
||||
while (channel.Reader.TryRead(out TEvent item))
|
||||
{
|
||||
await processor(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (BitcoinExtPubKey, RootedKeyPath?, ScriptPubKeyType)? ExtractFromDescriptor(this string descriptor, Network network)
|
||||
|
||||
public static (BitcoinExtPubKey, RootedKeyPath, ScriptPubKeyType)? ExtractFromDescriptor(this string descriptor, Network network)
|
||||
{
|
||||
var od = OutputDescriptor.Parse(descriptor, network);
|
||||
|
||||
(BitcoinExtPubKey, RootedKeyPath?) ExtractFromPkProvider(PubKeyProvider pubKeyProvider)
|
||||
(BitcoinExtPubKey, RootedKeyPath) ExtractFromPkProvider(PubKeyProvider pubKeyProvider)
|
||||
{
|
||||
switch (pubKeyProvider)
|
||||
{
|
||||
case PubKeyProvider.Const _:
|
||||
throw new FormatException("Only HD output descriptors are supported.");
|
||||
case PubKeyProvider.HD hd:
|
||||
if (hd.Path is not null && hd.Path.ToString() != "0")
|
||||
if (hd.Path != null && hd.Path.ToString() != "0")
|
||||
{
|
||||
throw new FormatException("Custom change paths are not supported.");
|
||||
}
|
||||
return (hd.Extkey, null);
|
||||
case PubKeyProvider.Origin origin:
|
||||
var innerResult = ExtractFromPkProvider(origin.Inner);
|
||||
return (innerResult.Item1, origin.KeyOriginInfo);
|
||||
return (innerResult.Item1, origin.KeyOriginInfo );
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
@ -79,25 +81,20 @@ public static class ChannelExtensions
|
||||
public static UserConfig AsLDKUserConfig(this LightningConfig config)
|
||||
{
|
||||
var result = UserConfig.with_default();
|
||||
// var channelConfig = ChannelConfig.with_default();
|
||||
// channelConfig.set
|
||||
// result.set_channel_config(channelConfig);
|
||||
result.set_accept_intercept_htlcs(true);
|
||||
//result.set_accept_mpp_keysend(true);
|
||||
result.set_accept_mpp_keysend(true);
|
||||
result.set_manually_accept_inbound_channels(true);
|
||||
var channelHandshakeConfig = ChannelHandshakeConfig.with_default();
|
||||
//channelHandshakeConfig.set_announced_channel(false);
|
||||
// channelHandshakeConfig.set(false);
|
||||
channelHandshakeConfig.set_announced_channel(false);
|
||||
channelHandshakeConfig.set_negotiate_anchors_zero_fee_htlc_tx(true);
|
||||
channelHandshakeConfig.set_minimum_depth(1);
|
||||
result.set_channel_handshake_config(channelHandshakeConfig);
|
||||
var channelHandshakeLimits = ChannelHandshakeLimits.with_default();
|
||||
channelHandshakeLimits.set_force_announced_channel_preference(true);
|
||||
channelHandshakeLimits.set_max_funding_satoshis(Money.Coins(100m).Satoshi);
|
||||
result.set_channel_handshake_limits(channelHandshakeLimits);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// public static async Task Process<T>(this Channel<T> channel, Func<T, CancellationToken, Task> processor,
|
||||
// CancellationToken cancellationToken)
|
||||
// {
|
||||
@ -175,4 +172,4 @@ public static class ChannelExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,16 +31,17 @@ public static class ChannelManagerHelper
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public static ChannelManager? Load(ChannelMonitor[] channelMonitors, byte[] channelManagerSerialized,
|
||||
EntropySource entropySource, SignerProvider signerProvider,
|
||||
NodeSigner nodeSigner, FeeEstimator feeEstimator,
|
||||
Watch watch, BroadcasterInterface txBroadcaster,
|
||||
Router router, MessageRouter messageRouter, Logger logger, UserConfig config, Filter filter)
|
||||
Router router, Logger logger, UserConfig config, Filter filter)
|
||||
{
|
||||
var resManager = UtilMethods.C2Tuple_ThirtyTwoBytesChannelManagerZ_read(channelManagerSerialized, entropySource,
|
||||
nodeSigner, signerProvider, feeEstimator,
|
||||
watch, txBroadcaster,
|
||||
router, messageRouter, logger, config, channelMonitors);
|
||||
router, logger, config, channelMonitors);
|
||||
if (!resManager.is_ok())
|
||||
{
|
||||
throw new SerializationException("Serialized ChannelManager was corrupt");
|
||||
@ -54,4 +55,4 @@ public static class ChannelManagerHelper
|
||||
return (resManager as Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ.
|
||||
Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ_OK)?.res.get_b();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ConfigExtensions
|
||||
{
|
||||
private const string ConfigDeviceIdentifierKey = "deviceIdentifier";
|
||||
|
||||
/*
|
||||
public static async Task<long> GetDeviceIdentifier(this ISecureConfigProvider configProvider)
|
||||
{
|
||||
var id = await configProvider.Get<long>(ConfigDeviceIdentifierKey);
|
||||
if (id == 0)
|
||||
{
|
||||
id = RandomUtils.GetInt64();
|
||||
await configProvider.Set(ConfigDeviceIdentifierKey, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
*/
|
||||
|
||||
public static async Task<long> GetDeviceIdentifier(this ConfigProvider configProvider)
|
||||
{
|
||||
return await configProvider.GetOrSet(ConfigDeviceIdentifierKey, () => Task.FromResult(RandomUtils.GetInt64()), false);
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ConfigHelpers
|
||||
{
|
||||
public static async Task<T?> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key, Func<Task<T>> factory)
|
||||
{
|
||||
var value = await secureConfigProvider.Get<T>(key);
|
||||
if (!Equals(value, default(T))) return value;
|
||||
value = await factory();
|
||||
await secureConfigProvider.Set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public static async Task<T?> GetOrSet<T>(this ConfigProvider configProvider, string key, Func<Task<T>> factory, bool backup)
|
||||
{
|
||||
var value = await configProvider.Get<T>(key);
|
||||
if (!Equals(value, default(T))) return value;
|
||||
value = await factory();
|
||||
await configProvider.Set(key, value, backup);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -2,27 +2,30 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
//from wasabi
|
||||
//from wasabi
|
||||
public static class EndPointParser
|
||||
{
|
||||
|
||||
public static IPEndPoint IPEndPoint(this EndPoint endPoint)
|
||||
{
|
||||
|
||||
if(endPoint is IPEndPoint ipEndPoint)
|
||||
return ipEndPoint;
|
||||
if(endPoint is not DnsEndPoint dnsEndPoint)
|
||||
throw new FormatException($"Invalid endpoint: {endPoint}");
|
||||
|
||||
var addresses = Dns.GetHostAddresses(dnsEndPoint.Host);
|
||||
|
||||
|
||||
var addresses = System.Net.Dns.GetHostAddresses(dnsEndPoint.Host);
|
||||
if (addresses.Length == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Unable to retrieve address from specified host name.",
|
||||
"Unable to retrieve address from specified host name.",
|
||||
"hostName"
|
||||
);
|
||||
}
|
||||
return new IPEndPoint(addresses[0], dnsEndPoint.Port); // Port gets validated here.
|
||||
}
|
||||
|
||||
|
||||
public static string Host(this EndPoint me)
|
||||
{
|
||||
if (me is DnsEndPoint dnsEndPoint)
|
||||
@ -38,7 +41,7 @@ public static class EndPointParser
|
||||
throw new FormatException($"Invalid endpoint: {me}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int? Port(this EndPoint me)
|
||||
{
|
||||
var result = 0;
|
||||
@ -60,7 +63,7 @@ public static class EndPointParser
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static string ToString(this EndPoint me, int defaultPort)
|
||||
{
|
||||
string host = me.Host();
|
||||
@ -70,9 +73,11 @@ public static class EndPointParser
|
||||
return endPointString;
|
||||
}
|
||||
|
||||
/// <param name="defaultPort">If invalid and it's needed to use, then this function returns false.</param>
|
||||
public static bool TryParse(string? endPointString, int defaultPort, [NotNullWhen(true)] out EndPoint? endPoint)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(endPointString) && System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
|
||||
if(System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
{
|
||||
if (ipEndPoint.Port == 0)
|
||||
{
|
||||
@ -81,12 +86,15 @@ public static class EndPointParser
|
||||
endPoint = ipEndPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
endPoint = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endPointString)) return false;
|
||||
if (string.IsNullOrWhiteSpace(endPointString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
endPointString = endPointString.TrimEnd(':', '/');
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public delegate Task AsyncEventHandler<in TEventArgs>(object? sender, TEventArgs e);
|
||||
public delegate Task AsyncEventHandler<TEventArgs>(object? sender, TEventArgs e);
|
||||
public delegate Task AsyncEventHandler(object? sender);
|
||||
|
||||
public static class EventHandlers
|
||||
@ -24,12 +24,12 @@ public static class EventHandlers
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
private static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
public static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
this Func<object, TArgs, Task> callback,
|
||||
Func<Exception, Task> errorHandler)
|
||||
where TArgs : EventArgs
|
||||
{
|
||||
return new EventHandler<TArgs>(async void (s, e) =>
|
||||
return new EventHandler<TArgs>(async (object s, TArgs e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -41,4 +41,4 @@ public static class EventHandlers
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,9 @@ namespace BTCPayApp.Core.Helpers;
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged where TKey : notnull
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
private const string? IndexerName = "Item[]";
|
||||
private const string IndexerName = "Item[]";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> class that is empty, has the
|
||||
@ -109,10 +109,10 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
}
|
||||
|
||||
/// <summary>Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.</summary>
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
/// <summary>Occurs when a property value changes.</summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Uses the specified functions to add a key/value pair to the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> if the
|
||||
@ -275,7 +275,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
/// the default value of the <typeparamref name="TValue"/> type if <paramref name="key"/> does not exist.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if the object was removed successfully; otherwise, <see langword="false"/>.</returns>
|
||||
public new bool TryRemove(TKey key, out TValue? value)
|
||||
public new bool TryRemove(TKey key, out TValue value)
|
||||
{
|
||||
if (base.TryRemove(key, out value))
|
||||
{
|
||||
@ -319,5 +319,5 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
|
||||
/// <summary>Raises the <see cref="PropertyChanged" /> event.</summary>
|
||||
/// <param name="propertyName">Name of the property that has changed.</param>
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
// Copied from BTCPayServer
|
||||
public class PolicyRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public PolicyRequirement(string policy)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(policy);
|
||||
Policy = policy;
|
||||
}
|
||||
public string Policy { get; }
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class StoreHelpers
|
||||
{
|
||||
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)>
|
||||
GetCurrentStorePaymentMethods(this IAccountManager accountManager)
|
||||
{
|
||||
var storeId = accountManager.CurrentStore?.Id;
|
||||
var pms = await accountManager.GetClient().GetStorePaymentMethods(storeId, includeConfig: true);
|
||||
var onchain = pms.FirstOrDefault(pm => pm.PaymentMethodId == OnChainWalletManager.PaymentMethodId);
|
||||
var lightning = pms.FirstOrDefault(pm => pm.PaymentMethodId == LightningNodeManager.PaymentMethodId);
|
||||
return (onchain, lightning);
|
||||
}
|
||||
|
||||
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)?> TryApplyingAppPaymentMethodsToCurrentStore(
|
||||
this IAccountManager accountManager,
|
||||
OnChainWalletManager onChainWalletManager, LightningNodeManager lightningNodeService, bool applyOnchain, bool applyLighting)
|
||||
{
|
||||
var storeId = accountManager.CurrentStore?.Id;
|
||||
var userId = accountManager.UserInfo?.UserId;
|
||||
var config = await onChainWalletManager.GetConfig();
|
||||
if (// are user and store present?
|
||||
string.IsNullOrEmpty(userId) ||
|
||||
string.IsNullOrEmpty(storeId) ||
|
||||
// is user permitted? (store owner)
|
||||
!await accountManager.IsAuthorized(Policies.CanModifyStoreSettings, storeId) ||
|
||||
// is the onchain wallet configured?
|
||||
!OnChainWalletManager.IsConfigured(config)) return null;
|
||||
// check the store's payment methods
|
||||
var (onchain, lightning) = await GetCurrentStorePaymentMethods(accountManager);
|
||||
|
||||
// onchain
|
||||
if (applyOnchain && config?.Derivations.TryGetValue(WalletDerivation.NativeSegwit, out var derivation) is true && onchain is null)
|
||||
{
|
||||
onchain = await accountManager.GetClient().UpdateStorePaymentMethod(storeId, OnChainWalletManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = derivation.Descriptor
|
||||
});
|
||||
}
|
||||
|
||||
// lightning
|
||||
if (applyLighting && lightning is null && lightningNodeService is { IsActive: true, Node.ApiKeyManager: { } apiKeyManager })
|
||||
{
|
||||
var key = await apiKeyManager.GetKeyForStore(storeId, APIKeyPermission.Write);
|
||||
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId,
|
||||
LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = key.ConnectionString(userId)
|
||||
});
|
||||
}
|
||||
|
||||
return (onchain, lightning);
|
||||
}
|
||||
|
||||
public static async Task<bool> IsOnChainOurs(this OnChainWalletManager onChainWalletManager, GenericPaymentMethodData? onchain)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(onchain?.Config.ToString()))
|
||||
{
|
||||
var config = await onChainWalletManager.GetConfig();
|
||||
using var jsonDoc = JsonDocument.Parse(onchain.Config.ToString());
|
||||
if (jsonDoc.RootElement.TryGetProperty("accountDerivation", out var derivationSchemeElement) &&
|
||||
derivationSchemeElement.GetString() is { } derivationScheme &&
|
||||
config?.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}") is true)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<bool> IsLightningOurs(this LightningNodeManager lightningNodeManager, GenericPaymentMethodData? lightning)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(lightning?.Config.ToString()))
|
||||
{
|
||||
var node = lightningNodeManager.Node;
|
||||
var apiKeyManager = node?.ApiKeyManager;
|
||||
if (apiKeyManager == null) return false;
|
||||
using var jsonDoc = JsonDocument.Parse(lightning.Config.ToString());
|
||||
if (jsonDoc.RootElement.TryGetProperty("connectionString", out var connectionStringElement) &&
|
||||
connectionStringElement.GetString() is { } connectionString &&
|
||||
LightningConnectionStringHelper.ExtractValues(connectionString, out var lnConnectionString) is { } lnValues &&
|
||||
lnConnectionString == "app" && lnValues.TryGetValue("key", out var key) && key is not null &&
|
||||
await node!.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// from https://github.com/dotnet/runtime/issues/31433#issuecomment-2148885279
|
||||
/// </summary>
|
||||
public static class SystemTextJsonMergeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges the specified Json Node into the base JsonNode for which this method is called.
|
||||
/// It is null safe and can be easily used with null-check & null coalesce operators for fluent calls.
|
||||
/// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects
|
||||
/// specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all
|
||||
/// fields so that they can be added to the base.
|
||||
///
|
||||
/// Source taken directly from the open-source Gist here:
|
||||
/// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="jsonBase"></param>
|
||||
/// <param name="jsonMerge"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static JsonNode Merge(this JsonNode jsonBase, JsonNode? jsonMerge)
|
||||
{
|
||||
if (jsonMerge == null)
|
||||
return jsonBase;
|
||||
|
||||
switch (jsonBase)
|
||||
{
|
||||
case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj:
|
||||
{
|
||||
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be
|
||||
// re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach...
|
||||
var mergeNodesArray = jsonMergeObj.ToArray();
|
||||
jsonMergeObj.Clear();
|
||||
|
||||
foreach (var prop in mergeNodesArray)
|
||||
{
|
||||
jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch
|
||||
{
|
||||
JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge(jsonMergeChildObj),
|
||||
JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge(jsonMergeChildArray),
|
||||
_ => prop.Value
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray:
|
||||
{
|
||||
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array,
|
||||
// so they can then be re-assigned to the target/base Json...
|
||||
var mergeNodesArray = jsonMergeArray.ToArray();
|
||||
jsonMergeArray.Clear();
|
||||
foreach(var mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " +
|
||||
$"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same.");
|
||||
|
||||
}
|
||||
|
||||
return jsonBase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the specified Dictionary of values into the base JsonNode for which this method is called.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="jsonBase"></param>
|
||||
/// <param name="dictionary"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static JsonNode MergeDictionary<TKey, TValue>(this JsonNode jsonBase, IDictionary<TKey, TValue> dictionary, JsonSerializerOptions? options = null)
|
||||
=> jsonBase.Merge(JsonSerializer.SerializeToNode(dictionary, options));
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BOLT11PaymentRequestJsonConverter : GenericStringJsonConverter<BOLT11PaymentRequest>
|
||||
{
|
||||
public override BOLT11PaymentRequest Create(string str)
|
||||
{
|
||||
return NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network));
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BitcoinSerializableJsonConverter<T> : GenericStringJsonConverter<T> where T : IBitcoinSerializable
|
||||
{
|
||||
public override T Create(string str)
|
||||
{
|
||||
var bytes = Convert.FromHexString(str);
|
||||
var instance = Activator.CreateInstance<T>();
|
||||
return NetworkHelper.Try(network =>
|
||||
{
|
||||
instance.ReadWrite(bytes, network);
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public override string? ToString(T? instance)
|
||||
{
|
||||
return Convert.ToHexString(instance.ToBytes()).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BitcoinSerializableJsonConverterFactory : JsonConverterFactory
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeof(IBitcoinSerializable).IsAssignableFrom(typeToConvert);
|
||||
}
|
||||
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var converterType = typeof(BitcoinSerializableJsonConverter<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter) Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Null:
|
||||
return default;
|
||||
case JsonTokenType.Number:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
|
||||
case JsonTokenType.String:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()!));
|
||||
}
|
||||
|
||||
throw new JsonException("Expected number or string with a unix timestamp value");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value.ToUnixTimeSeconds());
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public abstract class GenericStringJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
public abstract T Create(string str);
|
||||
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null) return default;
|
||||
|
||||
if (reader.TokenType != JsonTokenType.String ||
|
||||
reader.GetString() is not { } str ||
|
||||
string.IsNullOrEmpty(str))
|
||||
throw new JsonException("Expected string");
|
||||
|
||||
return Create(str);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStringValue(ToString(value));
|
||||
}
|
||||
|
||||
public virtual string? ToString(T? value)
|
||||
{
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class KeyPathJsonConverter : GenericStringJsonConverter<KeyPath>
|
||||
{
|
||||
public override KeyPath Create(string str)
|
||||
{
|
||||
return new KeyPath(str);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class LightMoneyJsonConverter : GenericStringJsonConverter<LightMoney>
|
||||
{
|
||||
public override LightMoney Create(string str)
|
||||
{
|
||||
return LightMoney.Parse(str);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user