Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395495fef5 | ||
|
|
aee8c5f032 | ||
|
|
0785a1ab39 | ||
|
|
ae06fd4404 | ||
|
|
e2d9b4e87c | ||
|
|
621adc1804 | ||
|
|
831f1e2c5b | ||
|
|
9f56451b41 | ||
|
|
427ba10f5e | ||
|
|
409822d1a2 | ||
|
|
1e84841af1 | ||
|
|
29136fe4c4 | ||
|
|
1b462e81da | ||
|
|
e75d011eb4 | ||
|
|
ef534215f9 | ||
|
|
b147ddba3f | ||
|
|
fdd3e0fc33 | ||
|
|
797e7a33c7 | ||
|
|
f4f988b113 | ||
|
|
b9000dfc75 | ||
|
|
9d44aee152 | ||
|
|
698afcd033 | ||
|
|
ac98a426c0 | ||
|
|
d232fbc68c | ||
|
|
c9d6f5d9d7 | ||
|
|
336daad4c6 | ||
|
|
4dbab4038c | ||
|
|
5b86d4f28f | ||
|
|
f64768a383 | ||
|
|
ccd160c4e0 | ||
|
|
5bd52b6f86 | ||
|
|
c2e7a79bb8 | ||
|
|
4a0bedd7b0 | ||
|
|
7f917a2a92 | ||
|
|
957da11d89 | ||
|
|
b489cd0ec7 | ||
|
|
f86b996303 | ||
|
|
5f59fa7987 | ||
|
|
48368cecbb | ||
|
|
df637acb54 | ||
|
|
9b8890d04e | ||
|
|
b5ce32c67b | ||
|
|
ad870cae50 | ||
|
|
612ef699b7 | ||
|
|
186e995c41 | ||
|
|
489d78bc40 | ||
|
|
74ec312c31 | ||
|
|
796a512b50 | ||
|
|
fd3bbf68e5 | ||
|
|
2061e93db3 | ||
|
|
3834ef4c4f | ||
|
|
cdf6c78b0a | ||
|
|
b0d1e83bda | ||
|
|
a678ad4d9e | ||
|
|
855046c161 | ||
|
|
f55c394c0c | ||
|
|
f5486a087b | ||
|
|
0b04dd03d4 | ||
|
|
85ca2d9658 | ||
|
|
72885feea7 | ||
|
|
0aa42e93a8 | ||
|
|
ef7faed0d8 | ||
|
|
d63ef814ca | ||
|
|
4868cf9b1f | ||
|
|
d076afbd85 | ||
|
|
3e6db4d9e9 | ||
|
|
22f1da33f3 | ||
|
|
d9dc06eb01 | ||
|
|
f94a1aa262 | ||
|
|
4e047acc8e | ||
|
|
e4e68c1cf4 | ||
|
|
28af1bfecb | ||
|
|
4e54057447 | ||
|
|
25eadbd822 | ||
|
|
e87131338b | ||
|
|
9ff0c0166b | ||
|
|
ea8344f537 | ||
|
|
6698d81efa | ||
|
|
426f50e1cd | ||
|
|
42af818d85 | ||
|
|
8babd11c1e | ||
|
|
244682f390 | ||
|
|
dfeb073c3c | ||
|
|
8dd2a33d16 | ||
|
|
3b3d82f873 | ||
|
|
d24e971a09 | ||
|
|
206b569fd4 | ||
|
|
cad7b343e4 | ||
|
|
18acd479f1 | ||
|
|
553948a38c | ||
|
|
375a320fb0 | ||
|
|
e00b9b8d7b | ||
|
|
fe33ec8769 | ||
|
|
df790001c0 | ||
|
|
83b567d02c | ||
|
|
712f2a5e5c | ||
|
|
128ee5b744 | ||
|
|
ab0bb4156a | ||
|
|
c5446985f9 | ||
|
|
ff1489fba7 | ||
|
|
7cf70000f3 | ||
|
|
e0b07afc80 | ||
|
|
7f4e12dd2c | ||
|
|
c7f2c4781a | ||
|
|
903869d04a | ||
|
|
3a8727ade2 | ||
|
|
71a9bf4cc5 | ||
|
|
1375719773 | ||
|
|
90bd8f0f6f | ||
|
|
6bcc0547d6 | ||
|
|
80076556e1 | ||
|
|
13ffd0371b | ||
|
|
75c87e96ed | ||
|
|
cb6f10999a | ||
|
|
63235396af | ||
|
|
7247bbc409 | ||
|
|
49d5ef1e47 | ||
|
|
ef9a3cd062 | ||
|
|
c4aa16992c | ||
|
|
46c4eead09 | ||
|
|
3de73adb19 | ||
|
|
ccd80fad4c | ||
|
|
34f28284a0 | ||
|
|
a3e9a36dbe | ||
|
|
9faf94b12c | ||
|
|
747e9bd1db | ||
|
|
5c8453c3e3 | ||
|
|
96cbcb5f88 | ||
|
|
afe395c9ff | ||
|
|
4efb05744b | ||
|
|
386f9fa774 | ||
|
|
a4a5d9fbdc | ||
|
|
e3412b1433 | ||
|
|
ea55664f93 | ||
|
|
866532332e | ||
|
|
f93b676f40 | ||
|
|
c3fc4fc82b | ||
|
|
2da390c091 | ||
|
|
b76e9392a6 | ||
|
|
3c2295f744 | ||
|
|
ca50033d21 | ||
|
|
5faf6b21c5 | ||
|
|
4aef5c078c | ||
|
|
c6c1b49cd2 | ||
|
|
4c3860edf6 | ||
|
|
7adeb99e66 | ||
|
|
401fc3f1a4 | ||
|
|
c38ad051e4 | ||
|
|
6d7d708e71 | ||
|
|
f573916392 | ||
|
|
ffeb9fbea8 | ||
|
|
01586fd857 | ||
|
|
81daa88618 | ||
|
|
ef093a480b | ||
|
|
ee8b8b991b | ||
|
|
11f621ad9e | ||
|
|
0bedbc5cb2 | ||
|
|
be8f7ad5cc | ||
|
|
7db3b597ea | ||
|
|
b03987cb87 | ||
|
|
f0584a032e | ||
|
|
44bda68311 | ||
|
|
5d20274f4c | ||
|
|
1d4797d1da | ||
|
|
abc7713131 | ||
|
|
2fdf6394a3 | ||
|
|
4caf6baedd | ||
|
|
e29d57ab80 | ||
|
|
85d89ceeb6 | ||
|
|
1eb87753f3 | ||
|
|
e5ccf2f579 | ||
|
|
f92294e4e7 | ||
|
|
42d3e36eb9 | ||
|
|
52c6f0ed6d | ||
|
|
b5810cb433 | ||
|
|
9c4836fbb6 | ||
|
|
a32866ba9c | ||
|
|
4e53925020 | ||
|
|
e36d83d54e | ||
|
|
a5421a92e6 | ||
|
|
a13156c1d3 | ||
|
|
b5c6ae88f3 | ||
|
|
921340f15a | ||
|
|
e1c60245a8 | ||
|
|
7623a9e6ce | ||
|
|
7e4871bda0 | ||
|
|
0e974b4848 | ||
|
|
99d5b33756 | ||
|
|
4ad3a9b333 | ||
|
|
1625f8817e | ||
|
|
1e081c4f0a | ||
|
|
ab79da7ba4 | ||
|
|
730e355b0f | ||
|
|
8cc75c2941 | ||
|
|
305acc2488 | ||
|
|
f869b6cb26 | ||
|
|
ebb64e05c6 | ||
|
|
336bafd0f5 | ||
|
|
5314b03cf6 | ||
|
|
19812fc880 | ||
|
|
13c7a2aed4 | ||
|
|
de70c59a26 | ||
|
|
f85f361300 | ||
|
|
8bc4c07edf | ||
|
|
8012a810f1 | ||
|
|
3c4ce6c026 | ||
|
|
7ff8b56cee | ||
|
|
11981fde2e | ||
|
|
3f96a1717e | ||
|
|
436d398bd4 | ||
|
|
cd8c95ba8c | ||
|
|
f73d964d6f | ||
|
|
9bf84303fb | ||
|
|
151f7059d8 | ||
|
|
9de6dd42fa | ||
|
|
0f0fe8d064 | ||
|
|
c4d2ce57c6 | ||
|
|
69fedc0767 | ||
|
|
a9da978061 | ||
|
|
d29bef6ce6 | ||
|
|
9bb42417b8 | ||
|
|
39895f780b | ||
|
|
0b7c25aa54 | ||
|
|
96dd48f6ba | ||
|
|
66befda61f | ||
|
|
3c413fb2c9 | ||
|
|
c54c3e3aea | ||
|
|
82746713d0 | ||
|
|
f1a149ffa3 | ||
|
|
f9e7e82e50 | ||
|
|
dd1ce2cc94 | ||
|
|
113eecae61 | ||
|
|
c1dce9f922 | ||
|
|
02e5f960a6 | ||
|
|
9e6aa4d7e0 | ||
|
|
00d221d411 | ||
|
|
604455cdd5 | ||
|
|
647662f12a | ||
|
|
2aa4c2473b | ||
|
|
c75eca0c21 | ||
|
|
8dca3455a8 | ||
|
|
582d14aaca | ||
|
|
b096d83a5e | ||
|
|
d53616224d | ||
|
|
72fcfad8d6 | ||
|
|
19d7bb8736 | ||
|
|
2304f76f6c | ||
|
|
71404327c1 | ||
|
|
68137f96f1 | ||
|
|
c1400db8a9 | ||
|
|
e75fd6b78f | ||
|
|
d30d6bd4f9 | ||
|
|
29bc2a4924 | ||
|
|
cbceadbe6c | ||
|
|
515a765232 | ||
|
|
b46bbaec50 | ||
|
|
cceb71956a | ||
|
|
a0ddb8d2b2 | ||
|
|
5ff026d263 | ||
|
|
697466f317 | ||
|
|
fe084a41fd | ||
|
|
b8ddba72b1 | ||
|
|
c38e944bcf | ||
|
|
57355d4e6b | ||
|
|
7554573605 | ||
|
|
05de7d871b | ||
|
|
4287d3b9a3 | ||
|
|
87f9de6d49 | ||
|
|
495fa9175f | ||
|
|
3b493ab98e | ||
|
|
172b3e72a1 | ||
|
|
b914cc0d8d | ||
|
|
8de8c62027 | ||
|
|
9603cf9d11 | ||
|
|
94af2bf529 | ||
|
|
fdd84c9f17 | ||
|
|
933e74649b | ||
|
|
83e4434f7b | ||
|
|
a3e49c9c2b | ||
|
|
8d0880ac85 | ||
|
|
cac99f6e87 | ||
|
|
550db94272 | ||
|
|
277386d333 | ||
|
|
6b03fdb4bc | ||
|
|
2e9f628f9c | ||
|
|
58334b1b37 | ||
|
|
ef76b76876 | ||
|
|
f796edf061 | ||
|
|
5fc526a999 | ||
|
|
42e6cbebef | ||
|
|
ce7e7f46b3 | ||
|
|
3780883fa7 | ||
|
|
b30f8b1bb8 | ||
|
|
454e556e56 | ||
|
|
4d7d14571f |
477
.github/workflows/build-test.yml
vendored
477
.github/workflows/build-test.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
|
||||
@ -20,6 +20,9 @@ env:
|
||||
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
|
||||
@ -33,201 +36,23 @@ jobs:
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# Setup infrastructure
|
||||
- name: Start containers
|
||||
run: docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
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
|
||||
nohup dotnet run -c Release --project BTCPayServer &
|
||||
while ! curl -s http://localhost:14142/api/v1/health > /dev/null; do
|
||||
# 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 -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
# Stop infrastructure
|
||||
|
||||
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@v1
|
||||
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 -r osx-x64 -c Release -o publish/osx-x64
|
||||
|
||||
- name: Build mac arm64
|
||||
run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r osx-arm64 -c Release -o publish/osx-arm64
|
||||
|
||||
# Create the .app bundle
|
||||
- name: Create .app bundle for x64
|
||||
run: |
|
||||
mkdir -p BTCPayApp_x64.app/Contents/MacOS
|
||||
cp publish/osx-x64/BTCPayApp.Photino BTCPayApp_x64.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>com.example.BTCPayApp</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>BTCPayApp</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
</dict>
|
||||
</plist>" > BTCPayApp_x64.app/Contents/Info.plist
|
||||
|
||||
- name: Create .app bundle for arm64
|
||||
run: |
|
||||
mkdir -p BTCPayApp_arm64.app/Contents/MacOS
|
||||
cp publish/osx-arm64/BTCPayApp.Photino BTCPayApp_arm64.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>com.example.BTCPayApp</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>BTCPayApp</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
</dict>
|
||||
</plist>" > BTCPayApp_arm64.app/Contents/Info.plist
|
||||
|
||||
# Sign the .app bundles using the dynamic certificate name
|
||||
- name: Sign x64 .app bundle
|
||||
run: |
|
||||
codesign --deep --force --options runtime --sign "$CERT_NAME" BTCPayApp_x64.app
|
||||
|
||||
- name: Sign arm64 .app bundle
|
||||
run: |
|
||||
codesign --deep --force --options runtime --sign "$CERT_NAME" BTCPayApp_arm64.app
|
||||
|
||||
# # Verify the signing
|
||||
# - name: Verify signing for x64
|
||||
# run: spctl -a -t exec -vv BTCPayApp_x64.app
|
||||
#
|
||||
# - name: Verify signing for arm64
|
||||
# run: spctl -a -t exec -vv BTCPayApp_arm64.app
|
||||
#
|
||||
# Create DMG files
|
||||
- name: Create DMG for x64
|
||||
run: |
|
||||
mkdir -p dist
|
||||
hdiutil create -volname "BTCPayApp" -srcfolder BTCPayApp_x64.app -ov -format UDZO dist/BTCPayApp_x64.dmg
|
||||
|
||||
- name: Create DMG for arm64
|
||||
run: |
|
||||
mkdir -p dist
|
||||
hdiutil create -volname "BTCPayApp" -srcfolder BTCPayApp_arm64.app -ov -format UDZO dist/BTCPayApp_arm64.dmg
|
||||
|
||||
# Sign the DMG files
|
||||
- name: Sign x64 DMG
|
||||
run: |
|
||||
codesign --force --sign "$CERT_NAME" dist/BTCPayApp_x64.dmg
|
||||
|
||||
- name: Sign arm64 DMG
|
||||
run: |
|
||||
codesign --force --sign "$CERT_NAME" dist/BTCPayApp_arm64.dmg
|
||||
|
||||
# Verify DMG signing
|
||||
- name: Verify x64 DMG signing
|
||||
run: spctl -a -t open --context context:primary-signature -v dist/BTCPayApp_x64.dmg
|
||||
|
||||
- name: Verify arm64 DMG signing
|
||||
run: spctl -a -t open --context context:primary-signature -v dist/BTCPayApp_arm64.dmg
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload DMG artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mac-dmg
|
||||
path: dist
|
||||
|
||||
dotnet test -c Release -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
|
||||
build-android:
|
||||
runs-on: windows-latest
|
||||
@ -242,48 +67,244 @@ jobs:
|
||||
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
|
||||
run: dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-android -c Debug -o publish/android
|
||||
# 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: android build
|
||||
path: |
|
||||
publish/android
|
||||
#
|
||||
# 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
|
||||
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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.idea
|
||||
**/bin
|
||||
**/obj
|
||||
**/tmp
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@ -38,6 +39,8 @@ 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,4 +1,4 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
·<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="" />
|
||||
@ -15,4 +15,4 @@
|
||||
<option name="EXTRA_MLAUNCH_PARAMETERS" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
<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>
|
||||
</component>
|
||||
|
||||
@ -5,27 +5,22 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class AppDatabaseMigrator: IHostedService
|
||||
public class AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory) : IHostedService
|
||||
{
|
||||
private readonly ILogger<AppDatabaseMigrator> _logger;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
|
||||
public AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
|
||||
if (pendingMigrationsAsync.Any())
|
||||
if (pendingMigrationsAsync.Length != 0)
|
||||
{
|
||||
_logger.LogInformation($"Applying {pendingMigrationsAsync.Length} migrations");
|
||||
logger.LogInformation("Applying {Length} migrations", pendingMigrationsAsync.Length);
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
_logger.LogInformation("Migrations applied: " + string.Join(", ", pendingMigrationsAsync));
|
||||
logger.LogInformation("Migrations applied: {Migrations}", string.Join(", ", pendingMigrationsAsync));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken) { }
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
// 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; }
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
This directory contains code extracted from Aspnet core libs to not depend on the web framework here, as we use this project in a Maui app which is not supported.
|
||||
@ -1,13 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
14
BTCPayApp.Core/Auth/AppPolicies.cs
Normal file
14
BTCPayApp.Core/Auth/AppPolicies.cs
Normal file
@ -0,0 +1,14 @@
|
||||
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,8 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client.App.Models;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@ -14,30 +13,25 @@ namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AuthStateProvider(
|
||||
IHttpClientFactory clientFactory,
|
||||
ConfigProvider configProvider,
|
||||
IAuthorizationService authService,
|
||||
ISecureConfigProvider secureProvider,
|
||||
ConfigProvider configProvider,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions)
|
||||
: AuthenticationStateProvider, IAccountManager, IHostedService
|
||||
{
|
||||
private const string AccountKeyPrefix = "Account";
|
||||
private const string CurrentAccountKey = "CurrentAccount";
|
||||
private bool _isInitialized;
|
||||
private bool _refreshUserInfo;
|
||||
private BTCPayAccount? _account;
|
||||
private AppUserInfo? _userInfo;
|
||||
private string? _currentStoreId;
|
||||
private CancellationTokenSource? _pingCts;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private readonly ClaimsPrincipal _unauthenticated = new(new ClaimsIdentity());
|
||||
|
||||
public BTCPayAccount? GetAccount() => _account;
|
||||
public AppUserInfo? GetUserInfo() => _userInfo;
|
||||
|
||||
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 AsyncEventHandler<BTCPayAccount?>? OnAccountInfoChange { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChange { get; set; }
|
||||
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 Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -46,6 +40,12 @@ public class AuthStateProvider(
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_pingCts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PingOccasionally(CancellationToken pingCtsToken)
|
||||
{
|
||||
while (pingCtsToken.IsCancellationRequested is false)
|
||||
@ -55,21 +55,23 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
|
||||
{
|
||||
_pingCts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
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 BTCPayAppClient GetClient(string? baseUri = null)
|
||||
public async Task<string?> GetEncryptionKey()
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(_account?.BaseUri))
|
||||
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
|
||||
var client = new BTCPayAppClient(baseUri ?? _account!.BaseUri, clientFactory.CreateClient());
|
||||
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;
|
||||
return await secureProvider.Get<string>("encryptionKey");
|
||||
}
|
||||
|
||||
public async Task SetEncryptionKey(string value)
|
||||
{
|
||||
await secureProvider.Set("encryptionKey", value);
|
||||
OnEncryptionKeyChanged?.Invoke(this, value);
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
@ -81,57 +83,56 @@ public class AuthStateProvider(
|
||||
await _semaphore.WaitAsync();
|
||||
|
||||
// initialize with persisted account
|
||||
if (!_isInitialized && _account == null)
|
||||
if (!_isInitialized && Account == null)
|
||||
{
|
||||
_account = await GetCurrentAccount();
|
||||
Account = await secureProvider.Get<BTCPayAccount>(BTCPayAccount.Key);
|
||||
_currentStoreId = (await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key))?.CurrentStoreId;
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
var oldUserInfo = _userInfo;
|
||||
var needsRefresh = _refreshUserInfo || _userInfo == null;
|
||||
if (needsRefresh && _account?.HasTokens is 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 cts = new CancellationTokenSource(5000);
|
||||
_userInfo = await GetClient().GetUserInfo(cts.Token);
|
||||
UserInfo = await GetClient().GetUserInfo(cts.Token);
|
||||
_refreshUserInfo = false;
|
||||
}
|
||||
|
||||
if (_userInfo != null)
|
||||
if (Account != null && 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 =>
|
||||
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, "Greenfield.Bearer"));
|
||||
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"));
|
||||
}
|
||||
|
||||
var res = new AuthenticationState(user);
|
||||
if (AppUserInfo.Equals(oldUserInfo, _userInfo)) return res;
|
||||
if (AppUserInfo.Equals(oldUserInfo, UserInfo)) return res;
|
||||
|
||||
//TODO: should this check against old user info?s
|
||||
if (_userInfo != null)
|
||||
{
|
||||
OnUserInfoChange?.Invoke(this, _userInfo);
|
||||
// update account user info
|
||||
_account!.SetInfo(_userInfo.Email!, _userInfo.Name, _userInfo.ImageUrl);
|
||||
OnAccountInfoChange?.Invoke(this, _account);
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
OnUserInfoChanged?.Invoke(this, UserInfo);
|
||||
if (Account != null && UserInfo != null)
|
||||
await UpdateAccount(Account);
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(res));
|
||||
return res;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_userInfo = null;
|
||||
UserInfo = null;
|
||||
return new AuthenticationState(user);
|
||||
}
|
||||
finally
|
||||
@ -144,7 +145,7 @@ public class AuthStateProvider(
|
||||
{
|
||||
if (refreshUser) _refreshUserInfo = true;
|
||||
await GetAuthenticationStateAsync();
|
||||
return _userInfo != null;
|
||||
return UserInfo != null;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorized(string policy, object? resource = null)
|
||||
@ -154,45 +155,37 @@ public class AuthStateProvider(
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
public async Task<FormResult> SetCurrentStoreId(string? storeId)
|
||||
{
|
||||
_userInfo = null;
|
||||
_account!.ClearAccess();
|
||||
OnUserInfoChange?.Invoke(this, _userInfo);
|
||||
await UpdateAccount(_account);
|
||||
await SetCurrentAccount(null);
|
||||
}
|
||||
|
||||
public async Task<FormResult> SetCurrentStoreId(string 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 != GetCurrentStore()?.Id)
|
||||
await SetCurrentStore(store);
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task SetCurrentStore(AppUserStoreInfo store)
|
||||
private async Task SetCurrentStore(AppUserStoreInfo? store)
|
||||
{
|
||||
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
|
||||
if (_currentStoreId == store?.Id) return;
|
||||
|
||||
// create associated POS app if there is none
|
||||
store = await EnsureStorePos(store);
|
||||
if (store != null)
|
||||
store = await EnsureStorePos(store);
|
||||
|
||||
_account!.CurrentStoreId = store.Id;
|
||||
await UpdateAccount(_account);
|
||||
_currentStoreId = store?.Id;
|
||||
|
||||
OnAfterStoreChange?.Invoke(this, store);
|
||||
}
|
||||
var appConfig = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig();
|
||||
appConfig.CurrentStoreId = _currentStoreId;
|
||||
await configProvider.Set(BTCPayAppConfig.Key, appConfig, true);
|
||||
|
||||
public async Task UnsetCurrentStore()
|
||||
{
|
||||
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
|
||||
_account!.CurrentStoreId = null;
|
||||
await UpdateAccount(_account);
|
||||
OnAfterStoreChange?.Invoke(this, null);
|
||||
OnStoreChanged?.Invoke(this, store);
|
||||
}
|
||||
|
||||
public async Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false)
|
||||
@ -206,7 +199,7 @@ public class AuthStateProvider(
|
||||
await CheckAuthenticated(true);
|
||||
store = GetUserStore(store.Id)!;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
@ -214,20 +207,14 @@ public class AuthStateProvider(
|
||||
return store;
|
||||
}
|
||||
|
||||
public AppUserStoreInfo? GetUserStore(string storeId)
|
||||
private 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);
|
||||
return UserInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
|
||||
}
|
||||
|
||||
public async Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default)
|
||||
{
|
||||
var urlParts = inviteUrl.Split("/invite/");
|
||||
var urlParts = inviteUrl.Split(Constants.InviteSeparator);
|
||||
var serverUrl = urlParts.First();
|
||||
var pathParts = urlParts.Last().Split("/");
|
||||
var payload = new AcceptInviteRequest
|
||||
@ -238,8 +225,8 @@ public class AuthStateProvider(
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).AcceptInvite(payload, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, response.Email);
|
||||
await SetCurrentAccount(account);
|
||||
var account = new BTCPayAccount(serverUrl, response.Email!);
|
||||
await SetAccount(account);
|
||||
var message = "Invitation accepted.";
|
||||
if (response.EmailHasBeenConfirmed is true)
|
||||
message += " Your email has been confirmed.";
|
||||
@ -250,9 +237,22 @@ 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<AcceptInviteResult>(false, e.Message, null);
|
||||
return new FormResult<LoginInfoResult>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,11 +266,10 @@ public class AuthStateProvider(
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).Login(payload, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
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);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -279,16 +278,36 @@ 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());
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
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);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -306,24 +325,24 @@ public class AuthStateProvider(
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).RegisterUser(payload, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
var account = new BTCPayAccount(serverUrl, email);
|
||||
var message = "Account created.";
|
||||
if (response.ContainsKey("accessToken"))
|
||||
{
|
||||
var access = response.ToObject<AccessTokenResponse>();
|
||||
account.SetAccess(access!.AccessToken, access.RefreshToken, access.ExpiresIn, expiryOffset);
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
if (string.IsNullOrEmpty(access?.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
account.OwnerToken = access.AccessToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
var signup = response.ToObject<ApplicationUserData>();
|
||||
if (signup.RequiresEmailConfirmation)
|
||||
if (signup?.RequiresEmailConfirmation is true)
|
||||
message += " Please confirm your email.";
|
||||
if (signup.RequiresApproval)
|
||||
if (signup?.RequiresApproval is true)
|
||||
message += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
await SetCurrentAccount(account);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true, message);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -343,14 +362,12 @@ public class AuthStateProvider(
|
||||
try
|
||||
{
|
||||
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
|
||||
if (response?.ContainsKey("accessToken") is true)
|
||||
{
|
||||
var access = response.ToObject<AccessTokenResponse>();
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(access!.AccessToken, access.RefreshToken, access.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
var account = new BTCPayAccount(serverUrl, email, access!.AccessToken);
|
||||
await SetAccount(account);
|
||||
}
|
||||
|
||||
return new FormResult(true, isForgotStep
|
||||
@ -381,37 +398,23 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult> SwitchMode(string storeId, string mode, CancellationToken? cancellation = default)
|
||||
{
|
||||
var payload = new UpdateApplicationUserRequest
|
||||
if (Account == null || !string.IsNullOrEmpty(Account.ModeToken))
|
||||
return new FormResult(false, "Cannot switch mode in current state.");
|
||||
|
||||
var payload = new SwitchModeRequest
|
||||
{
|
||||
Email = email,
|
||||
Name = name,
|
||||
ImageUrl = imageUrl
|
||||
StoreId = storeId,
|
||||
Mode = mode
|
||||
};
|
||||
try
|
||||
{
|
||||
var userData = await GetClient().UpdateCurrentUser(payload, cancellation.GetValueOrDefault());
|
||||
_account!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
OnAccountInfoChange?.Invoke(this, _account);
|
||||
if (_userInfo != null)
|
||||
{
|
||||
_userInfo.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
OnUserInfoChange?.Invoke(this, _userInfo);
|
||||
}
|
||||
return new FormResult<ApplicationUserData>(true, "Your account info has been changed.", userData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<ApplicationUserData>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
var response = await GetClient().SwitchMode(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
|
||||
public async Task<FormResult> RefreshAccess(CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient().RefreshAccess(_account!.RefreshToken, cancellation);
|
||||
Account.ModeToken = response.AccessToken;
|
||||
await SetAccount(Account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -420,64 +423,53 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnAccessRefresh(object? sender, AccessTokenResult access)
|
||||
public async Task<FormResult> SwitchToOwner(string password, string? otp = null, CancellationToken? cancellation = default)
|
||||
{
|
||||
if (_account == null) return;
|
||||
_account.SetAccess(access.AccessToken, access.RefreshToken, access.Expiry);
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
if (Account == null || string.IsNullOrEmpty(Account.ModeToken) || string.IsNullOrEmpty(Account.OwnerToken))
|
||||
return new FormResult(false, "Cannot switch user in current state.");
|
||||
|
||||
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 configProvider.List(prefix)).ToArray();
|
||||
var accounts = new List<BTCPayAccount>();
|
||||
foreach (var key in keys)
|
||||
var payload = new LoginRequest
|
||||
{
|
||||
var account = await configProvider.Get<BTCPayAccount>(key);
|
||||
accounts.Add(account!);
|
||||
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);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public async Task UpdateAccount(BTCPayAccount account)
|
||||
public async Task Logout()
|
||||
{
|
||||
await configProvider.Set(GetKey(account.Id), account, false);
|
||||
if (Account == null) return;
|
||||
Account.OwnerToken = Account.ModeToken = null;
|
||||
await SetAccount(Account);
|
||||
}
|
||||
|
||||
public async Task RemoveAccount(BTCPayAccount account)
|
||||
private async Task UpdateAccount(BTCPayAccount account)
|
||||
{
|
||||
await configProvider.Set<BTCPayAccount>(GetKey(account.Id), null, false);
|
||||
await secureProvider.Set(BTCPayAccount.Key, account);
|
||||
}
|
||||
|
||||
private async Task<BTCPayAccount> GetAccount(string serverUrl, string email)
|
||||
private async Task SetAccount(BTCPayAccount account)
|
||||
{
|
||||
var accountId = BTCPayAccount.GetId(serverUrl, email);
|
||||
var account = await configProvider.Get<BTCPayAccount>(GetKey(accountId));
|
||||
return account ?? new BTCPayAccount(serverUrl, email);
|
||||
}
|
||||
var storeId = CurrentStore?.Id;
|
||||
|
||||
private async Task<BTCPayAccount?> GetCurrentAccount()
|
||||
{
|
||||
var accountId = await configProvider.Get<string>(CurrentAccountKey);
|
||||
if (string.IsNullOrEmpty(accountId)) return null;
|
||||
return await configProvider.Get<BTCPayAccount>(GetKey(accountId));
|
||||
}
|
||||
|
||||
private async Task SetCurrentAccount(BTCPayAccount? account)
|
||||
{
|
||||
OnBeforeAccountChange?.Invoke(this, _account);
|
||||
if (account != null) await UpdateAccount(account);
|
||||
await configProvider.Set(CurrentAccountKey, account?.Id, false);
|
||||
_account = account;
|
||||
_userInfo = null;
|
||||
await UpdateAccount(account);
|
||||
Account = account;
|
||||
UserInfo = null;
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
OnAfterAccountChange?.Invoke(this, _account);
|
||||
|
||||
var store = GetCurrentStore();
|
||||
if (store != null) await SetCurrentStore(store);
|
||||
if (!string.IsNullOrEmpty(storeId)) await SetCurrentStoreId(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,18 +6,11 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class BearerAuthorizationHandler(IOptionsMonitor<IdentityOptions> identityOptions) : AuthorizationHandler<PolicyRequirement>
|
||||
public class AuthorizationHandler(IOptionsMonitor<IdentityOptions> identityOptions) : AuthorizationHandler<PolicyRequirement>
|
||||
{
|
||||
// Copied from BTCPayServer, because we cannot reference them. We need to keep the same values!
|
||||
private const string ServerAdminRole = "ServerAdmin";
|
||||
private const string GreenfieldBearerAuthenticationScheme = "Greenfield.Bearer";
|
||||
|
||||
//TODO: In the future, we will add these store permissions to actual aspnet roles, and remove this class.
|
||||
private static readonly PermissionSet _serverAdminRolePermissions = new([Permission.Create(Policies.CanViewStoreSettings)]);
|
||||
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity?.AuthenticationType != GreenfieldBearerAuthenticationScheme)
|
||||
if (context.User.Identity?.AuthenticationType != "Greenfield")
|
||||
return Task.CompletedTask;
|
||||
|
||||
var userId = context.User.Claims.FirstOrDefault(c => c.Type == identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType)?.Value;
|
||||
@ -26,7 +19,8 @@ public class BearerAuthorizationHandler(IOptionsMonitor<IdentityOptions> identit
|
||||
|
||||
var permissionSet = new PermissionSet();
|
||||
var success = false;
|
||||
var isAdmin = context.User.IsInRole(ServerAdminRole);
|
||||
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;
|
||||
@ -43,7 +37,7 @@ public class BearerAuthorizationHandler(IOptionsMonitor<IdentityOptions> identit
|
||||
if (!string.IsNullOrEmpty(permissions))
|
||||
{
|
||||
permissionSet = new PermissionSet(permissions.Split(',')
|
||||
.Select(s => Permission.TryCreatePermission(s, storeId, out var permission) ? permission : null)
|
||||
.Select(s => Permission.TryParse(s, out var permission) ? permission : null)
|
||||
.Where(s => s != null).ToArray());
|
||||
}
|
||||
}
|
||||
@ -58,11 +52,6 @@ public class BearerAuthorizationHandler(IOptionsMonitor<IdentityOptions> identit
|
||||
}
|
||||
else if (Policies.IsStorePolicy(policy) && !string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
if (isAdmin && !string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
success = _serverAdminRolePermissions.Contains(policy, storeId);
|
||||
}
|
||||
|
||||
if (!success && permissionSet.Contains(policy, storeId))
|
||||
{
|
||||
success = true;
|
||||
@ -73,6 +62,10 @@ public class BearerAuthorizationHandler(IOptionsMonitor<IdentityOptions> identit
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
else if (Policies.IsPluginPolicy(policy) && policy.StartsWith("btcpay.plugin.app"))
|
||||
{
|
||||
success = isOwner;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
@ -1,37 +1,33 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client.App.Models;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public interface IAccountManager
|
||||
{
|
||||
public BTCPayAccount? GetAccount();
|
||||
public Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null);
|
||||
public AppUserInfo? GetUserInfo();
|
||||
public BTCPayAppClient GetClient(string? baseUri = null);
|
||||
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 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<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<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 Logout();
|
||||
public Task UpdateAccount(BTCPayAccount account);
|
||||
public Task RemoveAccount(BTCPayAccount account);
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAccountInfoChange { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChanged { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnStoreChanged { get; set; }
|
||||
public AsyncEventHandler<string>? OnEncryptionKeyChanged { get; set; }
|
||||
}
|
||||
|
||||
@ -1,48 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAccount(string baseUri, string email)
|
||||
public class BTCPayAccount(string baseUri, string email, string? ownerToken = null)
|
||||
{
|
||||
public static string GetId(string baseUri, string email) => $"{new Uri(baseUri).Host}:{email}";
|
||||
public readonly string Id = GetId(baseUri, email);
|
||||
public const string Key = "account";
|
||||
public string Id { get; private set; } = $"{new Uri(baseUri).Host}:{email}";
|
||||
public string BaseUri { get; private set; } = WithTrailingSlash(baseUri);
|
||||
public string Email { get; private set; } = email;
|
||||
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);
|
||||
public string? OwnerToken { get; set; } = ownerToken;
|
||||
public string? ModeToken { get; set; }
|
||||
|
||||
private static string WithTrailingSlash(string s)
|
||||
{
|
||||
|
||||
@ -1,46 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.11" />
|
||||
<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="7.0.46" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageReference Include="org.ldk" Version="0.0.123" />
|
||||
<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.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
||||
<PackageReference Include="VSS" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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,135 +1,22 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Web;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.App.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
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, HttpClient client) : BTCPayServerClient(new Uri(baseUri), client)
|
||||
public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient? client = null) : BTCPayServerClient(new Uri(baseUri), apiKey, client)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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
|
||||
{
|
||||
AccessToken = response.AccessToken,
|
||||
RefreshToken = response.RefreshToken,
|
||||
Expiry = 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);
|
||||
@ -150,14 +37,25 @@ public class BTCPayAppClient(string baseUri, HttpClient client) : BTCPayServerCl
|
||||
return await SendHttpRequest<JObject>("btcpayapp/register", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AccessTokenResponse> Login(LoginRequest payload, CancellationToken cancellation = default)
|
||||
public async Task<LoginInfoResult> LoginInfo(string email, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
var payload = new Dictionary<string, object> { { "email", email } };
|
||||
return await SendHttpRequest<LoginInfoResult>("btcpayapp/login-info", payload, HttpMethod.Get, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AccessTokenResponse> Login(string loginCode, CancellationToken cancellation = default)
|
||||
public async Task<AuthenticationResponse> Login(LoginRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login/code", loginCode, HttpMethod.Post, cancellation);
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login", payload, 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)
|
||||
@ -172,13 +70,28 @@ public class BTCPayAppClient(string baseUri, HttpClient client) : BTCPayServerCl
|
||||
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> CreatePosInvoice(CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
if (req.Total != null) query.Add("amount", req.Total.Value.ToString(CultureInfo.InvariantCulture));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAppConfig
|
||||
{
|
||||
public const string Key = "AppConfig";
|
||||
public const string Key = "appconfig";
|
||||
public bool RecoveryPhraseVerified { get; set; }
|
||||
public bool UseBiometricAuth { get; set; }
|
||||
public string? Passcode { get; set; }
|
||||
public string? CurrentStoreId { get; set; }
|
||||
}
|
||||
|
||||
@ -6,47 +6,54 @@ namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public static class AppToServerHelper
|
||||
{
|
||||
|
||||
|
||||
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
return new LightningInvoice
|
||||
{
|
||||
Id = lightningPayment.PaymentHash.ToString(),
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = lightningPayment.Value,
|
||||
PaymentHash = lightningPayment.PaymentHash.ToString(),
|
||||
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,
|
||||
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()
|
||||
return new LightningPayment
|
||||
{
|
||||
Id = lightningPayment.PaymentHash.ToString(),
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
|
||||
PaymentHash = lightningPayment.PaymentHash.ToString(),
|
||||
PaymentHash = lightningPayment.PaymentHash?.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
BOLT11 = lightningPayment.PaymentRequest.ToString(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ using System.Text;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client.App;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@ -10,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
@ -24,34 +24,43 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
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: {ev}", ev);
|
||||
await OnNotifyServerEvent?.Invoke(this, 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);
|
||||
await OnNotifyNetwork?.Invoke(this, 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);
|
||||
await OnServerNodeInfo?.Invoke(this, 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: {request.TxId}");
|
||||
await OnTransactionDetected?.Invoke(this, 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);
|
||||
await OnNewBlock?.Invoke(this, block);
|
||||
_logger.LogInformation("NewBlock: {Block}", block);
|
||||
if (OnNewBlock is null) return;
|
||||
await OnNewBlock.Invoke(this, block);
|
||||
}
|
||||
|
||||
public async Task StartListen(string key)
|
||||
@ -64,22 +73,19 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
.StartListen();
|
||||
}
|
||||
|
||||
private PaymentsManager PaymentsManager =>
|
||||
_serviceProvider.GetRequiredService<LightningNodeManager>().Node?.PaymentsManager;
|
||||
private LightningAPIKeyManager ApiKeyManager =>
|
||||
_serviceProvider.GetRequiredService<LightningNodeManager>().Node?.ApiKeyManager;
|
||||
|
||||
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,
|
||||
@ -88,52 +94,64 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
|
||||
public async Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash)
|
||||
{
|
||||
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
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 invs.FirstOrDefault()?.ToInvoice();
|
||||
return invoices.FirstOrDefault()?.ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
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 invs.FirstOrDefault()?.ToPayment();
|
||||
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);
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default)
|
||||
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);
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default).ToInvoices();
|
||||
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 bolt = BOLT11PaymentRequest.Parse(bolt11, config.NBitcoinNetwork);
|
||||
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()
|
||||
return new PayResponse
|
||||
{
|
||||
Result = result.Status switch
|
||||
{
|
||||
@ -143,7 +161,7 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
LightningPaymentStatus.Failed => PayResult.Error,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Details = new PayDetails()
|
||||
Details = new PayDetails
|
||||
{
|
||||
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
|
||||
Status = result.Status
|
||||
@ -157,50 +175,62 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MasterUpdated(long? deviceIdentifier)
|
||||
public Task MasterUpdated(long? deviceIdentifier)
|
||||
{
|
||||
_logger.LogInformation("MasterUpdated: {deviceIdentifier}", 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);
|
||||
var node = _serviceProvider.GetRequiredService<LightningNodeManager>().Node;
|
||||
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();
|
||||
var config = await node.GetConfig();
|
||||
var peers = await node.GetPeers();
|
||||
var channels = (await node.GetChannels()).Where(channel => channel.Value.channelDetails is not null)
|
||||
.Select(channel => channel.Value.channelDetails).ToArray();
|
||||
return new LightningNodeInformation()
|
||||
return new LightningNodeInformation
|
||||
{
|
||||
Alias = config.Alias,
|
||||
Color = config.Color,
|
||||
Version = "preprepreprealpha",
|
||||
BlockHeight = bb.BlockHeight,
|
||||
BlockHeight = bb?.BlockHeight ?? 0,
|
||||
PeersCount = peers.Length,
|
||||
ActiveChannelsCount = channels.Count(channel => channel.get_is_usable()),
|
||||
InactiveChannelsCount =
|
||||
channels.Count(channel => !channel.get_is_usable() && channel.get_is_channel_ready()),
|
||||
PendingChannelsCount =
|
||||
channels.Count(channel => !channel.get_is_usable() && !channel.get_is_channel_ready())
|
||||
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);
|
||||
var channels = (await _serviceProvider.GetRequiredService<LightningNodeManager>().Node.GetChannels())
|
||||
.Where(channel => channel.Value.channelDetails is not null).Select(channel => channel.Value.channelDetails)
|
||||
.ToArray();
|
||||
if (Node is null) throw new HubException("Lightning Node not available");
|
||||
|
||||
return new LightningNodeBalance()
|
||||
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()
|
||||
OffchainBalance = new OffchainBalance
|
||||
{
|
||||
Local = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_balance_msat())),
|
||||
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,10 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client.App;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -14,136 +13,98 @@ using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayConnectionManager : BaseHostedService, IHubConnectionObserver
|
||||
public class BTCPayConnectionManager(
|
||||
IServiceProvider serviceProvider,
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface,
|
||||
ConfigProvider configProvider,
|
||||
SyncService syncService)
|
||||
: BaseHostedService(logger), IHubConnectionObserver
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly AuthenticationStateProvider _authStateProvider;
|
||||
private readonly ILogger<BTCPayConnectionManager> _logger;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly IBTCPayAppHubClient _btcPayAppServerClientInterface;
|
||||
private readonly ConfigProvider _configProvider;
|
||||
private readonly SyncService _syncService;
|
||||
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; }
|
||||
public string? ReportedNodeInfo { get; set; }
|
||||
private bool ForceSlaveMode { get; set; }
|
||||
public bool RunningInBackground { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
|
||||
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
|
||||
|
||||
private SemaphoreSlim _lock = new(1, 1);
|
||||
public BTCPayConnectionState ConnectionState
|
||||
{
|
||||
get => _connectionState;
|
||||
private set
|
||||
{
|
||||
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
|
||||
if (_connectionState == value)
|
||||
return;
|
||||
if (_connectionState == value) return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
_logger.LogInformation($"Connection state changed: {_connectionState} from {old}" );
|
||||
logger.LogInformation("Connection state changed{BgInfo}: {Old} -> {ConnectionState}", BgInfo, old, _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public BTCPayConnectionManager(
|
||||
IServiceProvider serviceProvider,
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface,
|
||||
ConfigProvider configProvider,
|
||||
SyncService syncService) : base(logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_accountManager = accountManager;
|
||||
_authStateProvider = authStateProvider;
|
||||
_logger = logger;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_btcPayAppServerClientInterface = btcPayAppServerClientInterface;
|
||||
_configProvider = configProvider;
|
||||
_syncService = syncService;
|
||||
}
|
||||
|
||||
private CancellationTokenSource _cts = new();
|
||||
private IBTCPayAppHubServer? _hubProxy;
|
||||
|
||||
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;
|
||||
_syncService.EncryptionKeyChanged += EncryptionKeyChanged;
|
||||
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));
|
||||
_ = MonitorHubConnection(_cts.Token);
|
||||
}
|
||||
|
||||
private async Task MonitorHubConnection(CancellationToken cancellationToken)
|
||||
{
|
||||
// while (!cancellationToken.IsCancellationRequested)
|
||||
// {
|
||||
// await WrapInLock(async () =>
|
||||
// {
|
||||
// if (Connection?.State is HubConnectionState.Disconnected)
|
||||
// {
|
||||
// await OnClosed(new Exception("MonitorHubConnection"));
|
||||
// }
|
||||
// }
|
||||
// , cancellationToken);
|
||||
// }
|
||||
//
|
||||
// await Task.Delay(500, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
private async Task OnMasterUpdated(object? sender, long? e)
|
||||
private async Task OnMasterUpdated(object? sender, long? masterId)
|
||||
{
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
return;
|
||||
if (e is null && ConnectionState == BTCPayConnectionState.ConnectedAsSlave && !ForceSlaveMode)
|
||||
|
||||
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 (await _configProvider.GetDeviceIdentifier() == e)
|
||||
else if (deviceId == masterId)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.ConnectedAsMaster;
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Setting master to {DeviceId}", BgInfo, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.ConnectedAsPrimary;
|
||||
}
|
||||
else if (ConnectionState == BTCPayConnectionState.ConnectedAsMaster && e != await _configProvider.GetDeviceIdentifier())
|
||||
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 EncryptionKeyChanged(object? sender)
|
||||
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)
|
||||
@ -151,174 +112,171 @@ public class BTCPayConnectionManager : BaseHostedService, IHubConnectionObserver
|
||||
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 deviceIdentifier = await configProvider.GetDeviceIdentifier();
|
||||
var newState = e.New;
|
||||
try
|
||||
{
|
||||
// await _lock.WaitAsync();
|
||||
|
||||
var account = _accountManager.GetAccount();
|
||||
switch (e.New)
|
||||
{
|
||||
case BTCPayConnectionState.Init:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
case BTCPayConnectionState.WaitingForAuth:
|
||||
|
||||
if (account is not null && await _accountManager.CheckAuthenticated())
|
||||
{
|
||||
newState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
|
||||
break;
|
||||
case BTCPayConnectionState.Connecting:
|
||||
if (account is null)
|
||||
{
|
||||
var account = accountManager.Account;
|
||||
switch (e.New)
|
||||
{
|
||||
case BTCPayConnectionState.Init:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
await Kill();
|
||||
|
||||
var connection = new HubConnectionBuilder()
|
||||
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());
|
||||
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>>();
|
||||
})
|
||||
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(),
|
||||
options =>
|
||||
{
|
||||
options.AccessTokenProvider = () =>
|
||||
Task.FromResult(_accountManager.GetAccount()?.AccessToken);
|
||||
options.HttpMessageHandlerFactory = _serviceProvider
|
||||
.GetService<Func<HttpMessageHandler, HttpMessageHandler>>();
|
||||
options.WebSocketConfiguration =
|
||||
_serviceProvider.GetService<Action<ClientWebSocketOptions>>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
_subscription = connection.Register(_btcPayAppServerClientInterface);
|
||||
HubProxy = new ExceptionWrappedHubProxy(this, connection, _logger);
|
||||
|
||||
_subscription = connection.Register(btcPayAppServerClientInterface);
|
||||
HubProxy = new ExceptionWrappedHubProxy(connection, logger);
|
||||
|
||||
if (connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
try
|
||||
if (connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
await connection.StartAsync();
|
||||
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
var result = await _accountManager.RefreshAccess();
|
||||
if (result.Succeeded)
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Successfully refreshed access token");
|
||||
connection.Closed += OnClosed;
|
||||
connection.Reconnected += OnReconnected;
|
||||
connection.Reconnecting += OnReconnecting;
|
||||
await connection.StartAsync();
|
||||
}
|
||||
else
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.LogError(ex, $"Could not refresh access token because: {string.Join(',', result.Messages)}");
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
Connection = connection;
|
||||
newState = Connection.State switch
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (ex is not TaskCanceledException)
|
||||
_logger.LogError(ex, "Error while connecting to hub");
|
||||
}
|
||||
}
|
||||
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 previosuly to process outbox items
|
||||
var masterDevice = await HubProxy.GetCurrentMaster();
|
||||
if (deviceIdentifier == masterDevice)
|
||||
HubConnectionState.Connected => BTCPayConnectionState.Syncing,
|
||||
HubConnectionState.Connecting => BTCPayConnectionState.Connecting,
|
||||
_ => BTCPayConnectionState.WaitingForAuth
|
||||
};
|
||||
break;
|
||||
case BTCPayConnectionState.Syncing:
|
||||
await syncService.StopSync();
|
||||
if (await syncService.EncryptionKeyRequiresImport())
|
||||
{
|
||||
await _syncService.SyncToRemote(CancellationToken.None);
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
logger.LogWarning(
|
||||
"Existing state found but encryption key is missing, waiting until key is provided");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _syncService.SyncToLocal();
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
||||
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;
|
||||
}
|
||||
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedFinishedInitialSync:
|
||||
if (ForceSlaveMode)
|
||||
{
|
||||
await HubProxy.DeviceMasterSignal(deviceIdentifier, false);
|
||||
ForceSlaveMode = false;
|
||||
newState = BTCPayConnectionState.ConnectedAsSlave;
|
||||
}
|
||||
|
||||
else if (!await HubProxy.DeviceMasterSignal(deviceIdentifier, true))
|
||||
{
|
||||
newState = BTCPayConnectionState.ConnectedAsSlave;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsMaster:
|
||||
await _syncService.StartSync(false);
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSlave:
|
||||
await _syncService.StartSync(true);
|
||||
break;
|
||||
case BTCPayConnectionState.Disconnected:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
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
|
||||
{
|
||||
// _lock.Release();
|
||||
_ = Task.Run(() => ConnectionState = newState);
|
||||
|
||||
_ = Task.Run(() => ConnectionState = newState);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ForceSlaveMode { get; set; }
|
||||
|
||||
private async Task OnServerNodeInfo(object? sender, string e)
|
||||
private Task OnServerNodeInfo(object? sender, string? e)
|
||||
{
|
||||
ReportedNodeInfo = e;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
private Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
_logger.LogInformation("OnNotifyServerEvent: {Type} - {Details}", e.Type, e.ToString());
|
||||
logger.LogInformation("OnNotifyServerEvent{BgInfo}: {Type} - {Details}", BgInfo, e.Type, e.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnNotifyNetwork(object? sender, string e)
|
||||
private Task OnNotifyNetwork(object? sender, string e)
|
||||
{
|
||||
ReportedNetwork = Network.GetNetwork(e);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
@ -328,10 +286,9 @@ public class BTCPayConnectionManager : BaseHostedService, IHubConnectionObserver
|
||||
try
|
||||
{
|
||||
await task;
|
||||
var authState = await _accountManager.CheckAuthenticated();
|
||||
var authState = await accountManager.CheckAuthenticated();
|
||||
if (ConnectionState == BTCPayConnectionState.WaitingForAuth && authState)
|
||||
{
|
||||
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
else if (ConnectionState > BTCPayConnectionState.WaitingForAuth && !authState)
|
||||
@ -341,56 +298,58 @@ public class BTCPayConnectionManager : BaseHostedService, IHubConnectionObserver
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while handling authentication state change");
|
||||
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");
|
||||
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();
|
||||
await syncService.StopSync();
|
||||
}
|
||||
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsMaster)
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
_logger.LogInformation("Sending device master signal to turn off");
|
||||
var deviceIdentifier = await _configProvider.GetDeviceIdentifier();
|
||||
await _syncService.StopSync();
|
||||
await _syncService.SyncToRemote(CancellationToken.None);
|
||||
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(deviceIdentifier, false);
|
||||
await HubProxy.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
await Kill();
|
||||
_authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
_btcPayAppServerClient.OnNotifyNetwork -= OnNotifyNetwork;
|
||||
|
||||
_syncService.EncryptionKeyChanged -= EncryptionKeyChanged;
|
||||
authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
btcPayAppServerClient.OnNotifyNetwork -= OnNotifyNetwork;
|
||||
accountManager.OnEncryptionKeyChanged -= OnEncryptionKeyChanged;
|
||||
ConnectionChanged -= OnConnectionChanged;
|
||||
}
|
||||
|
||||
|
||||
public Task OnClosed(Exception? exception)
|
||||
public Task OnClosed(Exception? ex)
|
||||
{
|
||||
_logger.LogError(exception, "Hub connection closed");
|
||||
logger.LogError("Hub connection closed{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
if (Connection?.State == HubConnectionState.Disconnected && ConnectionState != BTCPayConnectionState.Connecting)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Disconnected;
|
||||
@ -399,27 +358,32 @@ public class BTCPayConnectionManager : BaseHostedService, IHubConnectionObserver
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnReconnected(string? connectionId)
|
||||
public Task OnReconnected(string? connectionId)
|
||||
{
|
||||
_logger.LogInformation("Hub connection reconnected");
|
||||
logger.LogInformation("Hub connection reconnected{BgInfo}", BgInfo);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnReconnecting(Exception? exception)
|
||||
public Task OnReconnecting(Exception? ex)
|
||||
{
|
||||
_logger.LogWarning(exception, "Hub connection reconnecting");
|
||||
logger.LogWarning("Hub connection reconnecting{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SwitchToSlave()
|
||||
public async Task SwitchToSecondary()
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsMaster)
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
ForceSlaveMode = true;
|
||||
_logger.LogInformation("Sending device master signal to turn off");
|
||||
await _syncService.StopSync();
|
||||
await _syncService.SyncToRemote( CancellationToken.None);
|
||||
await HubProxy.DeviceMasterSignal(await _configProvider.GetDeviceIdentifier(), false);
|
||||
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,5 +1,8 @@
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
@ -8,7 +11,7 @@ public enum BTCPayConnectionState
|
||||
Connecting,
|
||||
Syncing,
|
||||
WaitingForEncryptionKey,
|
||||
ConnectedAsMaster,
|
||||
ConnectedAsSlave,
|
||||
ConnectedAsPrimary,
|
||||
ConnectedAsSecondary,
|
||||
ConnectedFinishedInitialSync
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,25 +4,20 @@ using BTCPayApp.Core.LDK;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayPaymentsNotifier : IScopedHostedService
|
||||
public class BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager,
|
||||
BTCPayConnectionManager connectionManager)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private readonly PaymentsManager _paymentsManager;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private bool _listening;
|
||||
|
||||
public BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager, BTCPayConnectionManager connectionManager)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager = paymentsManager;
|
||||
_connectionManager = connectionManager;
|
||||
paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
connectionManager.ConnectionChanged += ConnectionManagerOnConnectionChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
_connectionManager.ConnectionChanged += ConnectionManagerOnConnectionChanged;
|
||||
}
|
||||
private bool _listening = false;
|
||||
|
||||
private Task ConnectionManagerOnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
_listening = false;
|
||||
@ -31,20 +26,18 @@ public class BTCPayPaymentsNotifier : IScopedHostedService
|
||||
|
||||
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
|
||||
{
|
||||
if (!_listening)
|
||||
return;
|
||||
await _connectionManager.HubProxy
|
||||
.SendInvoiceUpdate(e.ToInvoice());
|
||||
if (!_listening || connectionManager.HubProxy is null) return;
|
||||
await connectionManager.HubProxy.SendInvoiceUpdate(e.ToInvoice());
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StartListen()
|
||||
{
|
||||
_listening = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client.App;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -11,25 +10,18 @@ public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
|
||||
{
|
||||
private readonly IBTCPayAppHubServer _hubProxy;
|
||||
private readonly ILogger _logger;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly HubConnection _connection;
|
||||
|
||||
public ExceptionWrappedHubProxy(BTCPayConnectionManager btcPayConnectionManager ,HubConnection connection, ILogger logger)
|
||||
public ExceptionWrappedHubProxy(HubConnection connection, ILogger logger)
|
||||
{
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_connection = connection;
|
||||
_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.
|
||||
// executes in thread pool
|
||||
try
|
||||
{
|
||||
return await func();
|
||||
@ -43,70 +35,69 @@ public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
|
||||
{
|
||||
_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));
|
||||
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));
|
||||
return await Wrap(async () => await _hubProxy.Pair(request));
|
||||
}
|
||||
|
||||
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.Handshake(request));
|
||||
return await Wrap(async () => await _hubProxy.Handshake(request));
|
||||
}
|
||||
|
||||
public async Task<bool> BroadcastTransaction(string tx)
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.BroadcastTransaction(tx));
|
||||
return await Wrap(async () => await _hubProxy.BroadcastTransaction(tx));
|
||||
}
|
||||
|
||||
public async Task<decimal> GetFeeRate(int blockTarget)
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.GetFeeRate(blockTarget));
|
||||
return await Wrap(async () => await _hubProxy.GetFeeRate(blockTarget));
|
||||
}
|
||||
|
||||
public async Task<BestBlockResponse> GetBestBlock()
|
||||
public async Task<BestBlockResponse?> GetBestBlock()
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.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));
|
||||
return await Wrap(async () => await _hubProxy.FetchTxsAndTheirBlockHeads(identifier, txIds, outpoints));
|
||||
}
|
||||
|
||||
public async Task<string> DeriveScript(string identifier)
|
||||
public async Task<ScriptResponse> DeriveScript(string identifier)
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.DeriveScript(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)));
|
||||
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));
|
||||
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));
|
||||
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));
|
||||
return await Wrap(async () => await _hubProxy.GetTransactions(identifiers));
|
||||
}
|
||||
|
||||
public async Task SendInvoiceUpdate(LightningInvoice lightningInvoice)
|
||||
@ -116,6 +107,6 @@ public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
|
||||
|
||||
public async Task<long?> GetCurrentMaster()
|
||||
{
|
||||
return await Wrap(async ()=> await _hubProxy.GetCurrentMaster());
|
||||
return await Wrap(async () => await _hubProxy.GetCurrentMaster());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
137
BTCPayApp.Core/BTCPayServer/IBTCPayAppHubClient.cs
Normal file
137
BTCPayApp.Core/BTCPayServer/IBTCPayAppHubClient.cs
Normal file
@ -0,0 +1,137 @@
|
||||
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,58 +0,0 @@
|
||||
using System.Net;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using VSS;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
|
||||
public class AccountAwareVssClient : IVSSAPI
|
||||
{
|
||||
private readonly IVSSAPI _inner;
|
||||
private readonly IAccountManager _accountManager;
|
||||
|
||||
public AccountAwareVssClient(IVSSAPI inner, IAccountManager accountManager)
|
||||
{
|
||||
_inner = inner;
|
||||
_accountManager = accountManager;
|
||||
}
|
||||
|
||||
private async Task<T> Wrap<T>(Func<Task<T>> func)
|
||||
{
|
||||
var retry = false;
|
||||
attemptAgain:
|
||||
try
|
||||
{
|
||||
return await func();
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized && !retry)
|
||||
{
|
||||
await _accountManager.RefreshAccess();
|
||||
retry = true;
|
||||
goto attemptAgain;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Wrap(async () => await _inner.GetObjectAsync(request, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Wrap(async () => await _inner.PutObjectAsync(request, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Wrap(async () => await _inner.DeleteObjectAsync(request, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Wrap(async () => await _inner.ListKeyVersionsAsync(request, cancellationToken));
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,7 @@ 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;
|
||||
}
|
||||
@ -43,14 +41,12 @@ public class SingleKeyDataProtector : IDataProtector
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
if(protectedData.Length == 0)
|
||||
{
|
||||
if (protectedData.Length == 0)
|
||||
return protectedData;
|
||||
}
|
||||
|
||||
var iv = protectedData.Take(16).ToArray();
|
||||
var cipherText = protectedData.Skip(16).ToArray();
|
||||
|
||||
return aes.DecryptCbc(cipherText, iv);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,58 +15,35 @@ using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
|
||||
public class SyncService : IDisposable
|
||||
public class SyncService(
|
||||
ConfigProvider configProvider,
|
||||
ILogger<SyncService> logger,
|
||||
IAccountManager accountManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
: IDisposable
|
||||
{
|
||||
private readonly ConfigProvider _configProvider;
|
||||
private readonly ILogger<SyncService> _logger;
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ISecureConfigProvider _secureConfigProvider;
|
||||
public AsyncEventHandler? EncryptionKeyChanged;
|
||||
public AsyncEventHandler<(List<Outbox> OutboxItemsProcesed, PutObjectRequest RemoteRequest)>? RemoteObjectUpdated;
|
||||
public AsyncEventHandler<string[]>? LocalUpdated;
|
||||
|
||||
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
|
||||
|
||||
public SyncService(
|
||||
ConfigProvider configProvider,
|
||||
ILogger<SyncService> logger,
|
||||
ISecureConfigProvider secureConfigProvider,
|
||||
IAccountManager accountManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
_logger = logger;
|
||||
_accountManager = accountManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_secureConfigProvider = secureConfigProvider;
|
||||
}
|
||||
|
||||
public async Task<string?> GetEncryptionKey()
|
||||
{
|
||||
return await _secureConfigProvider.Get<string>("encryptionKey");
|
||||
}
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
|
||||
private async Task<IDataProtector?> GetDataProtector()
|
||||
{
|
||||
var key = await GetEncryptionKey();
|
||||
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()
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
@ -76,6 +53,7 @@ public class SyncService : IDisposable
|
||||
|
||||
if (dataProtector is null)
|
||||
return true;
|
||||
|
||||
var decrypted = dataProtector.Unprotect(res.Value.ToByteArray());
|
||||
return "kukks" == Encoding.UTF8.GetString(decrypted);
|
||||
}
|
||||
@ -85,10 +63,9 @@ public class SyncService : IDisposable
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while checking if encryption key requires import");
|
||||
logger.LogError(e, "Error while checking if encryption key requires import");
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<bool> SetEncryptionKey(Mnemonic mnemonic)
|
||||
@ -99,17 +76,15 @@ public class SyncService : IDisposable
|
||||
|
||||
public async Task<bool> SetEncryptionKey(string key)
|
||||
{
|
||||
if (key.Contains(' '))
|
||||
{
|
||||
return await SetEncryptionKey(new Mnemonic(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()
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
@ -119,14 +94,10 @@ public class SyncService : IDisposable
|
||||
var decrypted = dataProtector.Unprotect(res.Value.Value.ToByteArray());
|
||||
if ("kukks" == Encoding.UTF8.GetString(decrypted))
|
||||
{
|
||||
await _secureConfigProvider.Set("encryptionKey", key);
|
||||
EncryptionKeyChanged?.Invoke(this);
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
|
||||
@ -134,38 +105,36 @@ public class SyncService : IDisposable
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while setting encryption key");
|
||||
logger.LogError("Error while setting encryption key: {Message}", e.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
await api.PutObjectAsync(new PutObjectRequest()
|
||||
await api.PutObjectAsync(new PutObjectRequest
|
||||
{
|
||||
GlobalVersion = await _configProvider.GetDeviceIdentifier(),
|
||||
GlobalVersion = await configProvider.GetDeviceIdentifier(),
|
||||
TransactionItems =
|
||||
{
|
||||
new KeyValue()
|
||||
new KeyValue
|
||||
{
|
||||
Key = "encryptionKeyTest",
|
||||
Value = ByteString.CopyFrom(encrypted)
|
||||
}
|
||||
},
|
||||
});
|
||||
await _secureConfigProvider.Set("encryptionKey", key);
|
||||
EncryptionKeyChanged?.Invoke(this);
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task<IVSSAPI> GetUnencryptedVSSAPI()
|
||||
{
|
||||
var account = _accountManager.GetAccount();
|
||||
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("Bearer", account.AccessToken);
|
||||
var httpClient = httpClientFactory.CreateClient("vss");
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", account.OwnerToken);
|
||||
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
|
||||
|
||||
return Task.FromResult<IVSSAPI>(new AccountAwareVssClient(vssClient, _accountManager));
|
||||
return Task.FromResult<IVSSAPI>(vssClient);
|
||||
}
|
||||
|
||||
private async Task<IVSSAPI?> GetVSSAPI()
|
||||
@ -174,19 +143,19 @@ public class SyncService : IDisposable
|
||||
return dataProtector is null ? null : new VSSApiEncryptorClient(await GetUnencryptedVSSAPI(), dataProtector);
|
||||
}
|
||||
|
||||
private async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
|
||||
private static async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
|
||||
{
|
||||
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue()
|
||||
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()
|
||||
var channels = dbContext.LightningChannels.Select(channel => new KeyValue
|
||||
{
|
||||
Key = channel.EntityKey,
|
||||
Version = channel.Version
|
||||
});
|
||||
var payments = dbContext.LightningPayments.Select(payment => new KeyValue()
|
||||
var payments = dbContext.LightningPayments.Select(payment => new KeyValue
|
||||
{
|
||||
Key = payment.EntityKey,
|
||||
Version = payment.Version
|
||||
@ -199,7 +168,7 @@ public class SyncService : IDisposable
|
||||
var backupApi = await GetVSSAPI();
|
||||
if (backupApi is null)
|
||||
return;
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var localVersions = await CreateLocalVersions(db);
|
||||
|
||||
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
|
||||
@ -224,24 +193,21 @@ public class SyncService : IDisposable
|
||||
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.Any())
|
||||
if (toDelete.Length == 0 && toUpsert.Length == 0)
|
||||
return;
|
||||
_logger.LogInformation("Syncing to local: {ToDelete} to delete, {ToUpsert} to upsert", toDelete.Length,
|
||||
toUpsert.Count());
|
||||
logger.LogInformation("Syncing to local: {ToDelete} to delete, {ToUpsert} to upsert", toDelete.Length,
|
||||
toUpsert.Length);
|
||||
|
||||
foreach (var upsertItem in toUpsert)
|
||||
{
|
||||
if (upsertItem.Value is null or {Length: 0})
|
||||
if (upsertItem.Value is not (null or { Length: 0 })) continue;
|
||||
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
|
||||
{
|
||||
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
|
||||
{
|
||||
Key = upsertItem.Key,
|
||||
}, cancellationToken);
|
||||
upsertItem.MergeFrom(item.Value);
|
||||
}
|
||||
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);
|
||||
@ -277,23 +243,21 @@ public class SyncService : IDisposable
|
||||
cancellationToken: cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.Database.CommitTransactionAsync(cancellationToken);
|
||||
_logger.LogInformation("Synced to local: {DeleteCount} deleted, {UpsertCount} upserted", deleteCount,
|
||||
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));
|
||||
.ForEach(key => configProvider.Updated?.Invoke(this, key));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await db.Database.RollbackTransactionAsync(cancellationToken);
|
||||
_logger.LogError(e, "Error while syncing to local");
|
||||
logger.LogError(e, "Error while syncing to local");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
private static async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
{
|
||||
switch (outbox.Entity)
|
||||
{
|
||||
@ -302,7 +266,7 @@ public class SyncService : IDisposable
|
||||
setting1.EntityKey == outbox.Key && setting1.Backup);
|
||||
if (setting == null)
|
||||
return null;
|
||||
return new KeyValue()
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(setting.Value),
|
||||
@ -316,7 +280,7 @@ public class SyncService : IDisposable
|
||||
return null;
|
||||
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
|
||||
|
||||
return new KeyValue()
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(val),
|
||||
@ -328,7 +292,7 @@ public class SyncService : IDisposable
|
||||
if (payment == null)
|
||||
return null;
|
||||
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
|
||||
return new KeyValue()
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(paymentBytes),
|
||||
@ -339,70 +303,66 @@ public class SyncService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private SemaphoreSlim _syncLock = new(1, 1);
|
||||
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 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 {outbox.Count} outbox items");
|
||||
|
||||
}
|
||||
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)
|
||||
var putObjectRequest = new PutObjectRequest
|
||||
{
|
||||
if (item.ActionType == OutboxAction.Delete)
|
||||
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)
|
||||
{
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
if (item.ActionType == OutboxAction.Delete)
|
||||
{
|
||||
Key = item.Key, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
{
|
||||
Key = item.Key, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
removedOutboxItems.AddRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
removedOutboxItems.AddRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
if (putObjectRequest.TransactionItems.Count == 0 && putObjectRequest.DeleteItems.Count == 0 && _syncTask is not null) return;
|
||||
|
||||
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 {putObjectRequest.TransactionItems.Count} items and deleted {putObjectRequest.DeleteItems.Count} items" +
|
||||
string.Join(", ", putObjectRequest.TransactionItems.Select(kv => kv.Key + " " + kv.Version)));
|
||||
RemoteObjectUpdated?.Invoke(this, (removedOutboxItems, putObjectRequest.Clone()));
|
||||
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
|
||||
{
|
||||
@ -414,12 +374,10 @@ public class SyncService : IDisposable
|
||||
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);
|
||||
_syncTask = (ContinuouslySync(local, cts.Token), cts, local);
|
||||
}
|
||||
|
||||
public async Task StopSync()
|
||||
@ -429,11 +387,9 @@ public class SyncService : IDisposable
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
_syncTask = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ContinuouslySync(bool local,
|
||||
CancellationToken cancellationToken = default)
|
||||
private async Task ContinuouslySync(bool local, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@ -442,18 +398,18 @@ public class SyncService : IDisposable
|
||||
if (local)
|
||||
await SyncToLocal(cancellationToken);
|
||||
else
|
||||
await SyncToRemote( cancellationToken);
|
||||
await SyncToRemote(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while syncing to {Local}", local ? "local" : "remote");
|
||||
}finally
|
||||
logger.LogError(e, "Error while syncing to {Target}", local ? "local" : "remote");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(!cancellationToken.IsCancellationRequested)
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -462,7 +418,6 @@ public class SyncService : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
RemoteObjectUpdated = null;
|
||||
EncryptionKeyChanged = null;
|
||||
LocalUpdated = null;
|
||||
}
|
||||
}
|
||||
|
||||
9
BTCPayApp.Core/Constants.cs
Normal file
9
BTCPayApp.Core/Constants.cs
Normal file
@ -0,0 +1,9 @@
|
||||
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";
|
||||
}
|
||||
@ -3,4 +3,5 @@
|
||||
public interface IDataDirectoryProvider
|
||||
{
|
||||
Task<string> GetAppDataDirectory();
|
||||
}
|
||||
Task<string> GetCacheDirectory();
|
||||
}
|
||||
|
||||
7
BTCPayApp.Core/Contracts/IEmailService.cs
Normal file
7
BTCPayApp.Core/Contracts/IEmailService.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayApp.Core.Contracts
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null);
|
||||
}
|
||||
}
|
||||
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
@ -0,0 +1,15 @@
|
||||
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,8 +1,7 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public interface ISecureConfigProvider
|
||||
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
|
||||
@ -6,20 +6,14 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
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>()
|
||||
@ -31,22 +25,22 @@ public class AppDbContext : DbContext
|
||||
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();
|
||||
@ -75,7 +69,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -88,7 +82,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -101,10 +95,10 @@ public class AppDbContext : DbContext
|
||||
// .Condition(@ref => @ref.Old.Value != @ref.New.Value)
|
||||
.Update<Setting>(
|
||||
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
|
||||
(tableRefs, setting) => new Setting() {Version = tableRefs.Old.Version + 1})
|
||||
(tableRefs, setting) => new Setting { Key = tableRefs.Old.Key, 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,
|
||||
@ -128,7 +122,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -139,7 +133,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -149,9 +143,9 @@ public class AppDbContext : DbContext
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Channel>(
|
||||
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
|
||||
(tableRefs, setting) => new Channel() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
(tableRefs, setting) => new Channel { Id = tableRefs.Old.Id, 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,
|
||||
@ -164,7 +158,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -175,7 +169,7 @@ public class AppDbContext : DbContext
|
||||
.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,
|
||||
@ -187,10 +181,10 @@ public class AppDbContext : DbContext
|
||||
|
||||
group.Update<AppLightningPayment>(
|
||||
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
|
||||
(tableRefs, setting) => new AppLightningPayment() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
(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,
|
||||
|
||||
@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,13 +5,11 @@ namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Channel:VersionedData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
public List<ChannelAlias> Aliases { get; set; }
|
||||
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();
|
||||
|
||||
@ -24,11 +22,11 @@ public class Channel:VersionedData
|
||||
|
||||
public class ChannelAlias
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public string? ChannelId { get; set; }
|
||||
[JsonIgnore]
|
||||
public Channel Channel { get; set; }
|
||||
public Channel? Channel { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -18,7 +18,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 +39,8 @@ public class LightningConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Dictionary<string, PeerInfo> Peers { get; set; } = new();
|
||||
|
||||
public bool AcceptInboundConnection{ get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
8
BTCPayApp.Core/Data/LogDbContext.cs
Normal file
8
BTCPayApp.Core/Data/LogDbContext.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogDbContext(DbContextOptions<LogDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<LogEntry> Logs { get; set; }
|
||||
}
|
||||
13
BTCPayApp.Core/Data/LogEntry.cs
Normal file
13
BTCPayApp.Core/Data/LogEntry.cs
Normal file
@ -0,0 +1,13 @@
|
||||
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; }
|
||||
}
|
||||
42
BTCPayApp.Core/Data/LoggingConfig.cs
Normal file
42
BTCPayApp.Core/Data/LoggingConfig.cs
Normal file
@ -0,0 +1,42 @@
|
||||
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 string Key { get; set; }
|
||||
public string Entity { get; set; }
|
||||
public long Version { get; set; }
|
||||
}
|
||||
public required string Key { get; set; }
|
||||
public required string Entity { get; set; }
|
||||
public required long Version { get; set; }
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ namespace BTCPayApp.Core.Data;
|
||||
public class Setting:VersionedData
|
||||
{
|
||||
[Key]
|
||||
public string Key { get; set; }
|
||||
public byte[] Value { get; set; }
|
||||
public required string Key { get; set; }
|
||||
public byte[]? Value { get; set; }
|
||||
public bool Backup { get; set; } = true;
|
||||
|
||||
public override string EntityKey
|
||||
|
||||
@ -10,42 +10,40 @@ 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 Network? NBitcoinNetwork => NBitcoin.Network.GetNetwork(Network);
|
||||
|
||||
public required BlockSnapshot Birthday { get; set; }
|
||||
|
||||
|
||||
public required CoinSnapshot CoinSnapshot { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class CoinSnapshot
|
||||
{
|
||||
public BlockSnapshot BlockSnapshot { get; set; }
|
||||
public Dictionary<string, SavedCoin[]> Coins { get; set; }
|
||||
|
||||
public required BlockSnapshot BlockSnapshot { get; set; }
|
||||
public required Dictionary<string, SavedCoin[]> Coins { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class SavedCoin
|
||||
{
|
||||
|
||||
[JsonConverter(typeof(BitcoinSerializableJsonConverterFactory))]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
public required OutPoint Outpoint { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
public KeyPath? Path { get; set; }
|
||||
}
|
||||
|
||||
public class BlockSnapshot
|
||||
{
|
||||
|
||||
public uint BlockHeight { get; set; }
|
||||
public required uint BlockHeight { get; set; }
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256 BlockHash { get; set; }
|
||||
}
|
||||
public required uint256 BlockHash { get; set; }
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
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,12 +76,11 @@
|
||||
// PendingEvents.Remove(eventData.EventId, out _);
|
||||
// return base.SaveChangesFailedAsync(eventData, cancellationToken);
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
|
||||
|
||||
using System.Text.Json;
|
||||
using AsyncKeyedLock;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
@ -91,21 +90,16 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class DatabaseConfigProvider: ConfigProvider
|
||||
public class DatabaseConfigProvider(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<DatabaseConfigProvider> logger)
|
||||
: ConfigProvider
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<DatabaseConfigProvider> _logger;
|
||||
private readonly AsyncKeyedLocker<string> _lock = new();
|
||||
|
||||
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory, ILogger<DatabaseConfigProvider> logger)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<T?> Get<T>(string key) where T : default
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
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);
|
||||
@ -115,8 +109,8 @@ public class DatabaseConfigProvider: ConfigProvider
|
||||
public override async Task Set<T>(string key, T? value, bool backup) where T : default
|
||||
{
|
||||
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
|
||||
@ -133,12 +127,11 @@ public class DatabaseConfigProvider: ConfigProvider
|
||||
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)
|
||||
{
|
||||
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,23 +1,24 @@
|
||||
#if DEBUG
|
||||
|
||||
namespace BTCPayApp.Maui;
|
||||
|
||||
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)
|
||||
public static bool ServerValidate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
|
||||
{
|
||||
return certificate.Issuer.Equals("CN=localhost") || errors == SslPolicyErrors.None;
|
||||
if (errors == SslPolicyErrors.None) return true;
|
||||
return certificate?.Subject.Equals("CN=localhost") is true;
|
||||
}
|
||||
|
||||
public static HttpClientHandler GetInsecureHandler()
|
||||
private static HttpClientHandler GetInsecureHandler()
|
||||
{
|
||||
HttpClientHandler handler = new HttpClientHandler();
|
||||
var handler = new HttpClientHandler();
|
||||
handler.ServerCertificateCustomValidationCallback = ServerValidate;
|
||||
return handler;
|
||||
}
|
||||
@ -27,8 +28,9 @@ public class DangerousHttpClientFactory : IHttpClientFactory
|
||||
return new HttpClient(GetInsecureHandler());
|
||||
}
|
||||
}
|
||||
|
||||
#if ANDROID
|
||||
public class DangerousAndroidMessageHandler :Xamarin.Android.Net.AndroidMessageHandler
|
||||
public class DangerousAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
|
||||
{
|
||||
protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
|
||||
=> new CustomHostnameVerifier();
|
||||
@ -37,30 +39,28 @@ public class DangerousAndroidMessageHandler :Xamarin.Android.Net.AndroidMessageH
|
||||
{
|
||||
public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
|
||||
{
|
||||
return session.PeerPrincipal?.Name == "CN=localhost";
|
||||
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) =>
|
||||
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();
|
||||
return new DangerousAndroidMessageHandler();
|
||||
#else
|
||||
return handler;
|
||||
#endif
|
||||
@ -73,4 +73,4 @@ public static class DebugExtensions
|
||||
return services;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@ -1,12 +1,12 @@
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Services;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client.App;
|
||||
using Laraue.EfCoreTriggers.SqlLite.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@ -14,7 +14,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
namespace BTCPayApp.Core.Extensions;
|
||||
|
||||
public static class StartupExtensions
|
||||
{
|
||||
@ -27,29 +27,31 @@ public static class StartupExtensions
|
||||
options.UseSqlLiteTriggers();
|
||||
});
|
||||
|
||||
serviceCollection.AddMemoryCache();
|
||||
// Configure logging
|
||||
LoggingConfig.ConfigureLogging(serviceCollection);
|
||||
|
||||
serviceCollection.AddHostedService<AppDatabaseMigrator>();
|
||||
serviceCollection.AddSingleton<ConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddMemoryCache();
|
||||
serviceCollection.AddHttpClient();
|
||||
serviceCollection.AddSingleton<BTCPayConnectionManager>();
|
||||
serviceCollection.AddSingleton<SyncService>();
|
||||
serviceCollection.AddSingleton<LoggingService>();
|
||||
serviceCollection.AddSingleton<LightningNodeManager>();
|
||||
serviceCollection.AddSingleton<OnChainWalletManager>();
|
||||
serviceCollection.AddSingleton<BTCPayAppServerClient>();
|
||||
serviceCollection.AddSingleton<IBTCPayAppHubClient>(provider =>
|
||||
provider.GetRequiredService<BTCPayAppServerClient>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider =>
|
||||
provider.GetRequiredService<BTCPayConnectionManager>());
|
||||
serviceCollection.AddSingleton<IBTCPayAppHubClient>(provider => provider.GetRequiredService<BTCPayAppServerClient>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<BTCPayConnectionManager>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<LightningNodeManager>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<OnChainWalletManager>());
|
||||
serviceCollection.AddSingleton<AuthStateProvider>();
|
||||
serviceCollection.AddSingleton<AuthenticationStateProvider, AuthStateProvider>(provider =>
|
||||
provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton<AuthenticationStateProvider, AuthStateProvider>(provider => provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton(sp => (IAccountManager) sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
serviceCollection.AddSingleton<ConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddSingleton(sp => (IAccountManager)sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, AuthorizationHandler>();
|
||||
serviceCollection.AddAuthorizationCore(options => options.AddPolicies());
|
||||
serviceCollection.AddLDK();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
|
||||
serviceCollection.AddAuthorizationCore(options => options.AddBTCPayPolicies());
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ using NBitcoin;
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class AsyncExtensions
|
||||
{
|
||||
{
|
||||
public static async Task RunInOtherThread(Action action)
|
||||
{
|
||||
await Task.Factory.StartNew(action);
|
||||
@ -16,8 +16,6 @@ public static class AsyncExtensions
|
||||
return await Task.Factory.StartNew(action);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static async Task RunInOtherThread(this Task task)
|
||||
{
|
||||
await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
@ -25,8 +23,7 @@ public static class AsyncExtensions
|
||||
|
||||
public static async Task<T> RunInOtherThread<T>(this Task<T> task)
|
||||
{
|
||||
|
||||
return await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
return await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
}
|
||||
/// <summary>
|
||||
/// Allows a cancellation token to be awaited.
|
||||
@ -57,7 +54,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.");
|
||||
@ -75,4 +72,4 @@ public static class AsyncExtensions
|
||||
public void UnsafeOnCompleted(Action continuation) =>
|
||||
CancellationToken.Register(continuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
@ -6,22 +7,29 @@ namespace BTCPayApp.Core.Helpers;
|
||||
// Copied from BTCPayServer
|
||||
public static class AuthorizationOptionsExtensions
|
||||
{
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
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;
|
||||
}
|
||||
|
||||
public static void AddPolicy(this AuthorizationOptions options, string policy)
|
||||
private static void AddPolicy(this AuthorizationOptions options, string policy)
|
||||
{
|
||||
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
|
||||
}
|
||||
public class CanGetRates
|
||||
|
||||
private class CanGetRates
|
||||
{
|
||||
public const string Key = "btcpay.store.cangetrates";
|
||||
}
|
||||
|
||||
@ -3,46 +3,40 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public abstract class BaseHostedService : IHostedService, IDisposable
|
||||
public abstract class BaseHostedService(ILogger logger) : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
protected CancellationTokenSource _cancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim _controlSemaphore = new(1, 1);
|
||||
private Task? _currentTask;
|
||||
protected CancellationTokenSource CancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim ControlSemaphore = new(1, 1);
|
||||
|
||||
public BaseHostedService(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
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);
|
||||
|
||||
logger.LogInformation("Stopping service");
|
||||
await CancellationTokenSource.CancelAsync();
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteStopAsync(_cancellationTokenSource.Token);
|
||||
|
||||
_logger.LogInformation("Stopped");
|
||||
await ExecuteStopAsync(CancellationTokenSource.Token);
|
||||
|
||||
logger.LogInformation("Stopped");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
ControlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,20 +45,20 @@ public abstract class BaseHostedService : IHostedService, IDisposable
|
||||
|
||||
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);
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
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,44 +26,42 @@ public static class ChannelExtensions
|
||||
add(OnEvent);
|
||||
_ = channel.ProcessChannel(processor, cancellationToken);
|
||||
|
||||
return new DisposableWrapper(async () =>
|
||||
return new DisposableWrapper(() =>
|
||||
{
|
||||
remove(OnEvent);
|
||||
channel.Writer.Complete();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task ProcessChannel<TEvent>(this Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
|
||||
private 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 TEvent item))
|
||||
while (channel.Reader.TryRead(out var 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 != null && hd.Path.ToString() != "0")
|
||||
{
|
||||
if (hd.Path is not 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();
|
||||
}
|
||||
@ -85,10 +83,10 @@ public static class ChannelExtensions
|
||||
// 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_announced_channel(false);
|
||||
// channelHandshakeConfig.set(false);
|
||||
channelHandshakeConfig.set_negotiate_anchors_zero_fee_htlc_tx(true);
|
||||
channelHandshakeConfig.set_minimum_depth(1);
|
||||
@ -99,7 +97,7 @@ public static class ChannelExtensions
|
||||
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)
|
||||
// {
|
||||
@ -177,4 +175,4 @@ public static class ChannelExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +31,16 @@ 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, Logger logger, UserConfig config, Filter filter)
|
||||
Router router, MessageRouter messageRouter, Logger logger, UserConfig config, Filter filter)
|
||||
{
|
||||
var resManager = UtilMethods.C2Tuple_ThirtyTwoBytesChannelManagerZ_read(channelManagerSerialized, entropySource,
|
||||
nodeSigner, signerProvider, feeEstimator,
|
||||
watch, txBroadcaster,
|
||||
router, logger, config, channelMonitors);
|
||||
router, messageRouter, logger, config, channelMonitors);
|
||||
if (!resManager.is_ok())
|
||||
{
|
||||
throw new SerializationException("Serialized ChannelManager was corrupt");
|
||||
@ -55,4 +54,4 @@ public static class ChannelManagerHelper
|
||||
return (resManager as Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ.
|
||||
Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ_OK)?.res.get_b();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,23 @@ 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,
|
||||
async () => RandomUtils.GetInt64(), false);
|
||||
return await configProvider.GetOrSet(ConfigDeviceIdentifierKey, () => Task.FromResult(RandomUtils.GetInt64()), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,29 +4,21 @@ namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ConfigHelpers
|
||||
{
|
||||
public static async Task<T> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key,
|
||||
Func<Task<T>> factory)
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
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)))
|
||||
{
|
||||
value = await factory();
|
||||
await configProvider.Set(key, value, backup);
|
||||
}
|
||||
|
||||
if (!Equals(value, default(T))) return value;
|
||||
value = await factory();
|
||||
await configProvider.Set(key, value, backup);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,27 @@ 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 = System.Net.Dns.GetHostAddresses(dnsEndPoint.Host);
|
||||
|
||||
var addresses = 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)
|
||||
@ -41,7 +38,7 @@ public static class EndPointParser
|
||||
throw new FormatException($"Invalid endpoint: {me}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int? Port(this EndPoint me)
|
||||
{
|
||||
var result = 0;
|
||||
@ -63,7 +60,7 @@ public static class EndPointParser
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static string ToString(this EndPoint me, int defaultPort)
|
||||
{
|
||||
string host = me.Host();
|
||||
@ -73,11 +70,9 @@ 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(System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
if (!string.IsNullOrEmpty(endPointString) && System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
{
|
||||
if (ipEndPoint.Port == 0)
|
||||
{
|
||||
@ -86,15 +81,12 @@ 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<TEventArgs>(object? sender, TEventArgs e);
|
||||
public delegate Task AsyncEventHandler<in 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;
|
||||
});
|
||||
|
||||
public static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
private static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
this Func<object, TArgs, Task> callback,
|
||||
Func<Exception, Task> errorHandler)
|
||||
where TArgs : EventArgs
|
||||
{
|
||||
return new EventHandler<TArgs>(async (object s, TArgs e) =>
|
||||
return new EventHandler<TArgs>(async void (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -41,4 +41,4 @@ public static class EventHandlers
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
@ -14,7 +14,7 @@ public static class StoreHelpers
|
||||
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)>
|
||||
GetCurrentStorePaymentMethods(this IAccountManager accountManager)
|
||||
{
|
||||
var storeId = accountManager.GetCurrentStore()?.Id;
|
||||
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);
|
||||
@ -25,14 +25,16 @@ public static class StoreHelpers
|
||||
this IAccountManager accountManager,
|
||||
OnChainWalletManager onChainWalletManager, LightningNodeManager lightningNodeService, bool applyOnchain, bool applyLighting)
|
||||
{
|
||||
var storeId = accountManager.GetCurrentStore()?.Id;
|
||||
var storeId = accountManager.CurrentStore?.Id;
|
||||
var userId = accountManager.UserInfo?.UserId;
|
||||
var config = await onChainWalletManager.GetConfig();
|
||||
if (// is a store present?
|
||||
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;
|
||||
!OnChainWalletManager.IsConfigured(config)) return null;
|
||||
// check the store's payment methods
|
||||
var (onchain, lightning) = await GetCurrentStorePaymentMethods(accountManager);
|
||||
|
||||
@ -47,15 +49,14 @@ public static class StoreHelpers
|
||||
}
|
||||
|
||||
// lightning
|
||||
if (applyLighting && lightning is null && lightningNodeService.IsActive)
|
||||
if (applyLighting && lightning is null && lightningNodeService is { IsActive: true, Node.ApiKeyManager: { } apiKeyManager })
|
||||
{
|
||||
var key = await lightningNodeService.Node.ApiKeyManager.Create("Automated BTCPay Store Setup",
|
||||
APIKeyPermission.Write);
|
||||
var key = await apiKeyManager.GetKeyForStore(storeId, APIKeyPermission.Write);
|
||||
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId,
|
||||
LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = key.ConnectionString(accountManager.GetUserInfo().UserId)
|
||||
Config = key.ConnectionString(userId)
|
||||
});
|
||||
}
|
||||
|
||||
@ -70,10 +71,8 @@ public static class StoreHelpers
|
||||
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}"))
|
||||
{
|
||||
config?.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}") is true)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -84,16 +83,15 @@ public static class StoreHelpers
|
||||
if (!string.IsNullOrEmpty(lightning?.Config.ToString()))
|
||||
{
|
||||
var node = lightningNodeManager.Node;
|
||||
if (node == null) return false;
|
||||
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))
|
||||
{
|
||||
await node!.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@ -16,7 +16,7 @@ public class BitcoinSerializableJsonConverter<T> : GenericStringJsonConverter<T>
|
||||
}
|
||||
|
||||
|
||||
public override string ToString(T? instance)
|
||||
public override string? ToString(T? instance)
|
||||
{
|
||||
return Convert.ToHexString(instance.ToBytes()).ToLowerInvariant();
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
|
||||
case JsonTokenType.Number:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
|
||||
case JsonTokenType.String:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()));
|
||||
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()!));
|
||||
}
|
||||
|
||||
throw new JsonException("Expected number or string with a unix timestamp value");
|
||||
@ -24,4 +24,4 @@ public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
writer.WriteNumberValue(value.ToUnixTimeSeconds());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ public abstract class GenericStringJsonConverter<T> : JsonConverter<T>
|
||||
writer.WriteStringValue(ToString(value));
|
||||
}
|
||||
|
||||
public virtual string ToString(T? value)
|
||||
public virtual string? ToString(T? value)
|
||||
{
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
@ -16,19 +16,13 @@ public class EndPointJsonConverter : GenericStringJsonConverter<EndPoint?>
|
||||
{
|
||||
public override EndPoint? Create(string str)
|
||||
{
|
||||
if(string.IsNullOrEmpty(str))
|
||||
return null;
|
||||
if(EndPointParser.TryParse(str, 9735, out var endpoint))
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
if (string.IsNullOrEmpty(str)) return null;
|
||||
if (EndPointParser.TryParse(str, 9735, out var endpoint)) return endpoint;
|
||||
throw new FormatException("Invalid endpoint");
|
||||
}
|
||||
|
||||
public override string ToString(EndPoint? value)
|
||||
public override string? ToString(EndPoint? value)
|
||||
{
|
||||
if (value is null)
|
||||
return null;
|
||||
return value.ToEndpointString();
|
||||
return value?.ToEndpointString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
/// <param name="Permission">Read or Write permissions, read implies being able to receive payments, write enables spending as well</param>
|
||||
public record APIKey(string Key, string Name, APIKeyPermission Permission)
|
||||
{
|
||||
public string ConnectionString(string user)
|
||||
public string ConnectionString(string userId)
|
||||
{
|
||||
return $"type=app;key={Key};user={user}";
|
||||
return $"type=app;user={userId};key={Key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum APIKeyPermission
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
/// <summary>
|
||||
/// A typed variant of <see cref="ILDKEventHandler"/> that handles a specific type of event
|
||||
/// A typed variant of <see cref="ILDKEventHandler"/> that handles a specific type of event
|
||||
/// </summary>
|
||||
/// <typeparam name="TEvent"></typeparam>
|
||||
public interface ILDKEventHandler<in TEvent>: ILDKEventHandler where TEvent : Event
|
||||
@ -18,10 +18,10 @@ public interface ILDKEventHandler
|
||||
Task Handle(Event @event)
|
||||
{
|
||||
var eventType = @event.GetType();
|
||||
|
||||
var result = this.GetType().GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILDKEventHandler<>) && i.GetGenericArguments()[0] == eventType)
|
||||
|
||||
var result = GetType().GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILDKEventHandler<>) && i.GetGenericArguments()[0] == eventType)
|
||||
?.GetMethod(nameof(ILDKEventHandler<Event>.Handle))
|
||||
?.Invoke(this, new object[] {@event});
|
||||
?.Invoke(this, [@event]);
|
||||
|
||||
if (result is Task task)
|
||||
return task;
|
||||
|
||||
@ -6,43 +6,37 @@ using org.ldk.structs;
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
// a background service that periodically checks if we have any public channels if so, publish a node announcement to the lightning network to be discoverable.
|
||||
public class LDKAnnouncementBroadcaster : IScopedHostedService, ILDKEventHandler<Event.Event_ChannelReady>
|
||||
public class LDKAnnouncementBroadcaster(
|
||||
LDKPeerHandler ldkPeerHandler,
|
||||
PeerManager peerManager,
|
||||
LDKNode ldkNode)
|
||||
: IScopedHostedService, ILDKEventHandler<Event.Event_ChannelReady>
|
||||
{
|
||||
private readonly LDKPeerHandler _ldkPeerHandler;
|
||||
private readonly PeerManager _peerManager;
|
||||
private readonly LDKNode _ldkNode;
|
||||
private CancellationTokenSource? _cts;
|
||||
private TaskCompletionSource? _tcs;
|
||||
|
||||
public LDKAnnouncementBroadcaster(LDKPeerHandler ldkPeerHandler,
|
||||
PeerManager peerManager, LDKNode ldkNode)
|
||||
{
|
||||
_ldkPeerHandler = ldkPeerHandler;
|
||||
_peerManager = peerManager;
|
||||
_ldkNode = ldkNode;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_ = RegularlyBroadcastAnnouncement(_cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private TaskCompletionSource? _tcs;
|
||||
|
||||
private async Task RegularlyBroadcastAnnouncement(CancellationToken cancellationToken)
|
||||
{
|
||||
while (cancellationToken.IsCancellationRequested == false)
|
||||
{
|
||||
var channels = (await _ldkNode.GetChannels(cancellationToken)).Where(pair => pair.Value.channelDetails is not null)
|
||||
.Select(pair => pair.Value.channelDetails!).ToList();
|
||||
var channels = (await ldkNode.GetChannels(cancellationToken) ?? [])
|
||||
.Where(pair => pair.channelDetails is not null)
|
||||
.Select(pair => pair.channelDetails!).ToList();
|
||||
|
||||
if (channels.Any(details => details.get_is_public()))
|
||||
if (channels.Any(details => details.get_is_announced()))
|
||||
{
|
||||
var endpoint = _ldkPeerHandler.Endpoint?.Endpoint();
|
||||
var config = await _ldkNode.GetConfig();
|
||||
var endpoint = ldkPeerHandler.Endpoint?.Endpoint();
|
||||
var config = await ldkNode.GetConfig();
|
||||
var alias = config.Alias;
|
||||
_peerManager.broadcast_node_announcement(config.RGB,
|
||||
Encoding.UTF8.GetBytes(alias), endpoint is null ? Array.Empty<SocketAddress>() : new[] {endpoint});
|
||||
peerManager.broadcast_node_announcement(config.RGB,
|
||||
Encoding.UTF8.GetBytes(alias), endpoint is null ? [] : [endpoint]);
|
||||
}
|
||||
|
||||
_tcs = new TaskCompletionSource();
|
||||
@ -55,8 +49,9 @@ public class LDKAnnouncementBroadcaster : IScopedHostedService, ILDKEventHandler
|
||||
await (_cts?.CancelAsync().WithCancellation(cancellationToken) ?? Task.CompletedTask);
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelReady @event)
|
||||
public Task Handle(Event.Event_ChannelReady @event)
|
||||
{
|
||||
_tcs?.TrySetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,46 +6,29 @@ namespace BTCPayApp.Core.LDK;
|
||||
/// <summary>
|
||||
/// Runs the LDK background processor which handles the main event loop for the LDK library.
|
||||
/// </summary>
|
||||
public class LDKBackgroundProcessor : IScopedHostedService
|
||||
public class LDKBackgroundProcessor(
|
||||
Persister persister,
|
||||
EventHandler eventHandler,
|
||||
ChainMonitor chainMonitor,
|
||||
ChannelManager channelManager,
|
||||
OnionMessenger onionMessenger,
|
||||
GossipSync gossipSync,
|
||||
PeerManager peerManager,
|
||||
Logger logger,
|
||||
WriteableScore scorer)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private readonly Persister _persister;
|
||||
private readonly EventHandler _eventHandler;
|
||||
private readonly ChainMonitor _chainMonitor;
|
||||
private readonly ChannelManager _channelManager;
|
||||
private readonly GossipSync _gossipSync;
|
||||
private readonly PeerManager _peerManager;
|
||||
private readonly Logger _logger;
|
||||
private readonly WriteableScore _scorer;
|
||||
private BackgroundProcessor? _processor;
|
||||
|
||||
public LDKBackgroundProcessor(Persister persister,
|
||||
EventHandler eventHandler,
|
||||
ChainMonitor chainMonitor,
|
||||
ChannelManager channelManager,
|
||||
GossipSync gossipSync,
|
||||
PeerManager peerManager,
|
||||
Logger logger,
|
||||
WriteableScore scorer)
|
||||
{
|
||||
_persister = persister;
|
||||
_eventHandler = eventHandler;
|
||||
_chainMonitor = chainMonitor;
|
||||
_channelManager = channelManager;
|
||||
_gossipSync = gossipSync;
|
||||
_peerManager = peerManager;
|
||||
_logger = logger;
|
||||
_scorer = scorer;
|
||||
}
|
||||
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await StopAsync(CancellationToken.None);
|
||||
_processor = BackgroundProcessor.start(_persister, _eventHandler, _chainMonitor, _channelManager, _gossipSync, _peerManager, _logger, Option_WriteableScoreZ.some(_scorer));
|
||||
_processor = BackgroundProcessor.start(persister, eventHandler, chainMonitor, channelManager, onionMessenger, gossipSync, peerManager, logger, Option_WriteableScoreZ.some(scorer));
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_processor?.stop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,28 +7,19 @@ namespace BTCPayApp.Core.LDK;
|
||||
/// <summary>
|
||||
/// Enables LDK to broadcast transactions through BTCPayServer.
|
||||
/// </summary>
|
||||
public class LDKBroadcaster : BroadcasterInterfaceInterface
|
||||
public class LDKBroadcaster(
|
||||
Network network,
|
||||
IEnumerable<IBroadcastGateKeeper> broadcastGateKeepers,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
: BroadcasterInterfaceInterface
|
||||
{
|
||||
private readonly Network _network;
|
||||
private readonly IEnumerable<IBroadcastGateKeeper> _broadcastGateKeepers;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKBroadcaster(
|
||||
Network network,
|
||||
IEnumerable<IBroadcastGateKeeper> broadcastGateKeepers, OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_network = network;
|
||||
_broadcastGateKeepers = broadcastGateKeepers;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
|
||||
public void broadcast_transactions(byte[][] txs)
|
||||
{
|
||||
List<Task> tasks = new();
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
var loadedTx = Transaction.Load(tx, _network);
|
||||
if(_broadcastGateKeepers.Any(gk => gk.DontBroadcast(loadedTx)))
|
||||
var loadedTx = Transaction.Load(tx, network);
|
||||
if(broadcastGateKeepers.Any(gk => gk.DontBroadcast(loadedTx)))
|
||||
continue;
|
||||
tasks.Add(Broadcast(loadedTx));
|
||||
}
|
||||
@ -37,7 +28,7 @@ public class LDKBroadcaster : BroadcasterInterfaceInterface
|
||||
|
||||
public async Task Broadcast(Transaction transaction, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _onChainWalletManager.BroadcastTransaction(transaction, cancellationToken);
|
||||
await onChainWalletManager.BroadcastTransaction(transaction, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,17 +2,11 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKBumpTransactionEventHandler: ILDKEventHandler<Event.Event_BumpTransaction>
|
||||
public class LDKBumpTransactionEventHandler(BumpTransactionEventHandler bumpTransactionEventHandler) : ILDKEventHandler<Event.Event_BumpTransaction>
|
||||
{
|
||||
private readonly BumpTransactionEventHandler _bumpTransactionEventHandler;
|
||||
|
||||
public LDKBumpTransactionEventHandler(BumpTransactionEventHandler bumpTransactionEventHandler)
|
||||
{
|
||||
_bumpTransactionEventHandler = bumpTransactionEventHandler;
|
||||
}
|
||||
public Task Handle(Event.Event_BumpTransaction @event)
|
||||
{
|
||||
_bumpTransactionEventHandler.handle_event(@event.bump_transaction);
|
||||
bumpTransactionEventHandler.handle_event(@event.bump_transaction);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,19 +4,13 @@ using org.ldk.structs;
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a bitcoin address from the main wallet when sweeping funds from closed channels
|
||||
/// Provides a bitcoin address from the main wallet when sweeping funds from closed channels
|
||||
/// </summary>
|
||||
public class LDKChangeDestinationSource:ChangeDestinationSourceInterface
|
||||
public class LDKChangeDestinationSource(LightningNodeManager lightningNodeManager) : 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());
|
||||
var s = lightningNodeManager.Node.DeriveScript().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_u8ZNoneZ.ok(s.ToBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client.App;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using org.ldk.structs;
|
||||
@ -11,58 +10,36 @@ namespace BTCPayApp.Core.LDK;
|
||||
/// <summary>
|
||||
/// a background service that keeps LDK synchronized with onchain events that it needs to know about
|
||||
/// </summary>
|
||||
public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
public class LDKChannelSync(
|
||||
IEnumerable<Confirm> confirms,
|
||||
BTCPayConnectionManager connectionManager,
|
||||
OnChainWalletManager onchainWalletManager,
|
||||
LDKNode node,
|
||||
Network network,
|
||||
Watch watch,
|
||||
LDKFilter ldkFilter,
|
||||
BTCPayAppServerClient appHubClient,
|
||||
ILogger<LDKChannelSync> logger)
|
||||
: IScopedHostedService, IDisposable
|
||||
{
|
||||
private readonly Confirm[] _confirms;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private readonly OnChainWalletManager _onchainWalletManager;
|
||||
private readonly LDKNode _node;
|
||||
private readonly Network _network;
|
||||
private readonly Watch _watch;
|
||||
private readonly LDKFilter _ldkFilter;
|
||||
private readonly BTCPayAppServerClient _appHubClient;
|
||||
private readonly ILogger<LDKChannelSync> _logger;
|
||||
private readonly List<IDisposable> _disposables = new();
|
||||
|
||||
|
||||
public LDKChannelSync(
|
||||
IEnumerable<Confirm> confirms,
|
||||
BTCPayConnectionManager connectionManager,
|
||||
OnChainWalletManager onchainWalletManager,
|
||||
LDKNode node,
|
||||
Network network,
|
||||
Watch watch,
|
||||
LDKFilter ldkFilter,
|
||||
BTCPayAppServerClient appHubClient,
|
||||
ILogger<LDKChannelSync> logger)
|
||||
{
|
||||
_confirms = confirms.ToArray();
|
||||
_connectionManager = connectionManager;
|
||||
_onchainWalletManager = onchainWalletManager;
|
||||
_node = node;
|
||||
_network = network;
|
||||
_watch = watch;
|
||||
_ldkFilter = ldkFilter;
|
||||
_appHubClient = appHubClient;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly Confirm[] _confirms = confirms.ToArray();
|
||||
private readonly List<IDisposable> _disposables = [];
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="txIds">The specific transaction ids we should check the status of. If null, we get a list of transaction ids from LDK, and also a list of utxos that we are watching </param>
|
||||
private async Task PollForTransactionUpdates(uint256[]? txIds = null)
|
||||
{
|
||||
Dictionary<uint256, (uint256 TransactionId, int Height, uint256? Block)> relevantTransactionsFromConfirms;
|
||||
List<LDKWatchedOutput> watchedOutputs = new();
|
||||
List<LDKWatchedOutput> spentWatchedOutputs = new();
|
||||
|
||||
List<LDKWatchedOutput> watchedOutputs = [];
|
||||
List<LDKWatchedOutput> spentWatchedOutputs = [];
|
||||
|
||||
if (txIds is null)
|
||||
{
|
||||
txIds = [];
|
||||
|
||||
watchedOutputs = await _ldkFilter.GetWatchedOutputs();
|
||||
watchedOutputs = await ldkFilter.GetWatchedOutputs();
|
||||
|
||||
relevantTransactionsFromConfirms = _confirms.SelectMany(confirm => confirm.get_relevant_txids().Select(zz =>
|
||||
(TransactionId: new uint256(zz.get_a()),
|
||||
@ -82,23 +59,20 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
(uint256 TransactionId, int Height, uint256? Block) (uint256) => (uint256, 0, null));
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Fetching {relevantTransactionsFromConfirms.Count} transactions");
|
||||
logger.LogInformation("Fetching {Count} transactions", relevantTransactionsFromConfirms.Count);
|
||||
var txIdsToQuery = relevantTransactionsFromConfirms.Select(zz => zz.Key.ToString()).ToArray();
|
||||
var outpoints = watchedOutputs.Select(zz => zz.Outpoint.ToString()).ToArray();
|
||||
var lnIdentifier = await _node.Identifier;
|
||||
var lnIdentifier = await node.Identifier;
|
||||
var result =
|
||||
await _connectionManager.HubProxy.FetchTxsAndTheirBlockHeads(lnIdentifier, txIdsToQuery, outpoints);
|
||||
await connectionManager.HubProxy.FetchTxsAndTheirBlockHeads(lnIdentifier, txIdsToQuery, outpoints);
|
||||
var blockHeaders =
|
||||
result.BlockHeaders.ToDictionary(zz => new uint256(zz.Key), zz => BlockHeader.Parse(zz.Value, _network));
|
||||
result.BlockHeaders.ToDictionary(zz => new uint256(zz.Key), zz => BlockHeader.Parse(zz.Value, network));
|
||||
var txs = result.Txs.ToDictionary(zz => new uint256(zz.Key),
|
||||
zz => Transaction.Parse(zz.Value.Transaction, _network));
|
||||
zz => Transaction.Parse(zz.Value.Transaction, network));
|
||||
|
||||
|
||||
_logger.LogInformation($"Fetched {result.Txs.Count} transactions");
|
||||
logger.LogInformation("Fetched {Count} transactions", result.Txs.Count);
|
||||
Dictionary<uint256, List<TwoTuple_usizeTransactionZ>> blockToTxList = new();
|
||||
|
||||
|
||||
// Dictionary<uint256, List<TwoTuple_usizeTransactionZ>> confirmedTxList = new();
|
||||
foreach (var transactionResult in result.Txs)
|
||||
{
|
||||
var tx = txs[new uint256(transactionResult.Key)];
|
||||
@ -123,17 +97,15 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
list.Add(TwoTuple_usizeTransactionZ.of(0, tx.ToBytes()));
|
||||
break;
|
||||
}
|
||||
case { } when transactionResult.Value.BlockHash is not null &&
|
||||
tx1.Block != uint256.Parse(transactionResult.Value.BlockHash):
|
||||
|
||||
case not null when transactionResult.Value.BlockHash is not null &&
|
||||
tx1.Block != uint256.Parse(transactionResult.Value.BlockHash):
|
||||
{
|
||||
foreach (var confirm in _confirms)
|
||||
{
|
||||
confirm.transaction_unconfirmed(tx1.TransactionId.ToBytes());
|
||||
}
|
||||
|
||||
blockToTxList.TryAdd(new uint256(transactionResult.Value.BlockHash),
|
||||
new List<TwoTuple_usizeTransactionZ>());
|
||||
blockToTxList.TryAdd(new uint256(transactionResult.Value.BlockHash), []);
|
||||
|
||||
var list = blockToTxList[new uint256(transactionResult.Value.BlockHash)];
|
||||
list.Add(TwoTuple_usizeTransactionZ.of(0, tx.ToBytes()));
|
||||
@ -145,8 +117,7 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
transactionResult.Value.BlockHash is not null)
|
||||
{
|
||||
var watchedOutput = watchedOutputs.First(zz => tx.Inputs.Any(zzz => zzz.PrevOut == zz.Outpoint));
|
||||
blockToTxList.TryAdd(new uint256(transactionResult.Value.BlockHash),
|
||||
new List<TwoTuple_usizeTransactionZ>());
|
||||
blockToTxList.TryAdd(new uint256(transactionResult.Value.BlockHash), []);
|
||||
var list = blockToTxList[new uint256(transactionResult.Value.BlockHash)];
|
||||
list.Add(TwoTuple_usizeTransactionZ.of(0, tx.ToBytes()));
|
||||
|
||||
@ -157,7 +128,7 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
foreach (var block in blockToTxList)
|
||||
{
|
||||
var header = blockHeaders[block.Key];
|
||||
var height = result.BlockHeghts[block.Key.ToString()];
|
||||
var height = result.BlockHeights[block.Key.ToString()];
|
||||
var headerBytes = header.ToBytes();
|
||||
// if(block.Key.ToString() == "00000000000000086130942075335f4937cd89cb183d69cce612eb780c838f7c")
|
||||
// continue;
|
||||
@ -167,54 +138,51 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
await _ldkFilter.OutputsSpent(spentWatchedOutputs);
|
||||
await ldkFilter.OutputsSpent(spentWatchedOutputs);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_disposables.Clear();
|
||||
var monitors = await _node.GetInitialChannelMonitors();
|
||||
var monitors = await node.GetInitialChannelMonitors();
|
||||
foreach (var channelMonitor in monitors)
|
||||
{
|
||||
_watch.watch_channel(channelMonitor.get_funding_txo().get_a(), channelMonitor);
|
||||
watch.watch_channel(channelMonitor.get_funding_txo().get_a(), channelMonitor);
|
||||
}
|
||||
|
||||
await PollForTransactionUpdates();
|
||||
|
||||
var bb = await _onchainWalletManager.GetBestBlock();
|
||||
var bbHeader = BlockHeader.Parse(bb.BlockHeader, _network).ToBytes();
|
||||
var bb = await onchainWalletManager.GetBestBlock();
|
||||
var bbHeader = BlockHeader.Parse(bb.BlockHeader, network).ToBytes();
|
||||
foreach (var confirm in _confirms)
|
||||
{
|
||||
confirm.best_block_updated(bbHeader, bb.BlockHeight);
|
||||
}
|
||||
|
||||
|
||||
_disposables.Add(ChannelExtensions.SubscribeToEventWithChannelQueue<string>(
|
||||
action => _appHubClient.OnNewBlock += action,
|
||||
action => _appHubClient.OnNewBlock -= action, OnNewBlock,
|
||||
action => appHubClient.OnNewBlock += action,
|
||||
action => appHubClient.OnNewBlock -= action, OnNewBlock,
|
||||
cancellationToken));
|
||||
|
||||
_disposables.Add(ChannelExtensions.SubscribeToEventWithChannelQueue<TransactionDetectedRequest>(
|
||||
action => _appHubClient.OnTransactionDetected += action,
|
||||
action => _appHubClient.OnTransactionDetected -= action, OnTransactionUpdate,
|
||||
action => appHubClient.OnTransactionDetected += action,
|
||||
action => appHubClient.OnTransactionDetected -= action, OnTransactionUpdate,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
|
||||
private async Task OnTransactionUpdate(TransactionDetectedRequest txUpdate, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"Transaction update {txUpdate.TxId}");
|
||||
|
||||
logger.LogInformation("Transaction update {TxId}", txUpdate.TxId);
|
||||
await PollForTransactionUpdates([new uint256(txUpdate.TxId)]);
|
||||
_logger.LogInformation($"Transaction update {txUpdate.TxId} processed");
|
||||
logger.LogInformation("Transaction update {TxId} processed", txUpdate.TxId);
|
||||
}
|
||||
|
||||
private async Task OnNewBlock(string e, CancellationToken arg2)
|
||||
private async Task OnNewBlock(string block, CancellationToken arg2)
|
||||
{
|
||||
_logger.LogInformation($"New block {e}");
|
||||
logger.LogInformation("New block {Block}", block);
|
||||
|
||||
var blockHeaderResponse = await _onchainWalletManager.GetBestBlock();
|
||||
var header = BlockHeader.Parse(blockHeaderResponse.BlockHeader, _network);
|
||||
var blockHeaderResponse = await onchainWalletManager.GetBestBlock();
|
||||
var header = BlockHeader.Parse(blockHeaderResponse.BlockHeader, network);
|
||||
var headerBytes = header.ToBytes();
|
||||
foreach (var confirm in _confirms)
|
||||
{
|
||||
@ -222,16 +190,16 @@ public class LDKChannelSync : IScopedHostedService, IDisposable
|
||||
}
|
||||
|
||||
await PollForTransactionUpdates();
|
||||
_logger.LogInformation($"New block {e} processed");
|
||||
logger.LogInformation("New block {Block} processed", block);
|
||||
}
|
||||
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposables.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,8 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKCoinSelector : CoinSelectionSourceInterface
|
||||
public class LDKCoinSelector(OnChainWalletManager onChainWalletManager) : CoinSelectionSourceInterface
|
||||
{
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKCoinSelector(OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
public Result_CoinSelectionNoneZ select_confirmed_utxos(byte[] claim_id, Input[] must_spend,
|
||||
org.ldk.structs.TxOut[] must_pay_to,
|
||||
int target_feerate_sat_per_1000_weight)
|
||||
@ -21,30 +15,26 @@ public class LDKCoinSelector : CoinSelectionSourceInterface
|
||||
var coins = must_spend.Select(x => x.Coin()).ToList();
|
||||
try
|
||||
{
|
||||
var tx = _onChainWalletManager.CreateTransaction(
|
||||
var tx = onChainWalletManager.CreateTransaction(
|
||||
txouts, feerate,
|
||||
coins).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
var changeTxOut = tx.Tx.Outputs.FirstOrDefault(@out => @out.ScriptPubKey == tx.Change.ScriptPubKey);
|
||||
|
||||
|
||||
var utxos = tx.SpentCoins.Select(x => Utxo.of(x.Outpoint.Outpoint(), x.TxOut.TxOut(), tx.Tx.Inputs.First(@in => @in.PrevOut == x.Outpoint).GetSerializedSize())).ToArray();
|
||||
return Result_CoinSelectionNoneZ.ok(CoinSelection.of(utxos, changeTxOut is null ? Option_TxOutZ.none() : Option_TxOutZ.some(changeTxOut.TxOut())));
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return Result_CoinSelectionNoneZ.err();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public Result_TransactionNoneZ sign_psbt(byte[] psbtBytes)
|
||||
{
|
||||
var signedPsbt = _onChainWalletManager.SignTransaction(psbtBytes).GetAwaiter().GetResult();
|
||||
var signedPsbt = onChainWalletManager.SignTransaction(psbtBytes).GetAwaiter().GetResult();
|
||||
return signedPsbt is null
|
||||
? Result_TransactionNoneZ.err()
|
||||
: Result_TransactionNoneZ.ok(signedPsbt.ExtractTransaction().ToBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,30 +3,22 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKEventHandler : EventHandlerInterface
|
||||
public class LDKEventHandler(IEnumerable<ILDKEventHandler> eventHandlers, LDKWalletLogger ldkWalletLogger) : EventHandlerInterface
|
||||
{
|
||||
private readonly IEnumerable<ILDKEventHandler> _eventHandlers;
|
||||
private readonly LDKWalletLogger _ldkWalletLogger;
|
||||
|
||||
public LDKEventHandler(IEnumerable<ILDKEventHandler> eventHandlers, LDKWalletLogger ldkWalletLogger)
|
||||
public Result_NoneReplayEventZ handle_event(Event @event)
|
||||
{
|
||||
_eventHandlers = eventHandlers;
|
||||
_ldkWalletLogger = ldkWalletLogger;
|
||||
}
|
||||
|
||||
public void handle_event(Event _event)
|
||||
{
|
||||
_ldkWalletLogger.LogInformation($"Received event {_event.GetType()}");
|
||||
_eventHandlers.AsParallel().ForAll(async handler =>
|
||||
ldkWalletLogger.LogInformation("Received event {Type}", @event.GetType());
|
||||
eventHandlers.AsParallel().ForAll(async void (handler) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.Handle(_event);
|
||||
await handler.Handle(@event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ldkWalletLogger.LogError(ex, $"Error handling event {_event.GetType()} with handler {handler.GetType()}");
|
||||
ldkWalletLogger.LogError(ex, "Error handling event {EventType} with handler {HandlerType}", @event.GetType(), handler.GetType());
|
||||
}
|
||||
});
|
||||
return Result_NoneReplayEventZ.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,6 @@ public static class LDKExtensions
|
||||
|
||||
if (remote is null)
|
||||
return Option_SocketAddressZ.none();
|
||||
var ipe = ((IPEndPoint) socket.RemoteEndPoint);
|
||||
|
||||
// return Option_SocketAddressZ.some(SocketAddress.tcp_ip_v4(ipe.Address.GetAddressBytes(), (short) ipe.Port));
|
||||
var socketAddress = SocketAddress.from_str(remote);
|
||||
@ -64,7 +63,7 @@ public static class LDKExtensions
|
||||
{
|
||||
return SocketAddress.from_str(endPoint.ToString()) switch
|
||||
{
|
||||
org.ldk.structs.Result_SocketAddressSocketAddressParseErrorZ.Result_SocketAddressSocketAddressParseErrorZ_OK
|
||||
Result_SocketAddressSocketAddressParseErrorZ.Result_SocketAddressSocketAddressParseErrorZ_OK
|
||||
ok => ok.res,
|
||||
_ => null
|
||||
};
|
||||
@ -98,6 +97,7 @@ public static class LDKExtensions
|
||||
var watch = provider.GetRequiredService<Watch>();
|
||||
var broadcasterInterface = provider.GetRequiredService<BroadcasterInterface>();
|
||||
var router = provider.GetRequiredService<Router>();
|
||||
var messageRouter = provider.GetRequiredService<MessageRouter>();
|
||||
var logger = provider.GetRequiredService<Logger>();
|
||||
var signerProvider = provider.GetRequiredService<SignerProvider>();
|
||||
var userConfig = provider.GetRequiredService<UserConfig>();
|
||||
@ -115,10 +115,10 @@ public static class LDKExtensions
|
||||
{
|
||||
return ChannelManagerHelper.Load(channelManagerSerialized.Value.channelMonitors,
|
||||
channelManagerSerialized.Value.serializedChannelManager, entropySource, signerProvider, nodeSigner,
|
||||
feeEstimator, watch, broadcasterInterface, router, logger, userConfig, filter);
|
||||
feeEstimator, watch, broadcasterInterface, router, messageRouter, logger, userConfig, filter);
|
||||
}
|
||||
|
||||
return ChannelManager.of(feeEstimator, watch, broadcasterInterface, router, logger, entropySource,
|
||||
return ChannelManager.of(feeEstimator, watch, broadcasterInterface, router, messageRouter, logger, entropySource,
|
||||
nodeSigner, signerProvider, userConfig, chainParameters,
|
||||
(int) DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||
});
|
||||
@ -132,9 +132,12 @@ public static class LDKExtensions
|
||||
provider.GetRequiredService<EntropySource>()));
|
||||
services.AddScoped(provider => provider.GetRequiredService<P2PGossipSync>().as_RoutingMessageHandler());
|
||||
services.AddScoped(provider => provider.GetRequiredService<DefaultMessageRouter>().as_MessageRouter());
|
||||
services.AddScoped(provider => IgnoringMessageHandler.of().as_CustomOnionMessageHandler());
|
||||
services.AddScoped(provider => IgnoringMessageHandler.of().as_CustomMessageHandler());
|
||||
services.AddScoped<NodeIdLookUp>(provider => EmptyNodeIdLookUp.of().as_NodeIdLookUp());
|
||||
services.AddScoped(_ => IgnoringMessageHandler.of().as_CustomOnionMessageHandler());
|
||||
services.AddScoped(_ => IgnoringMessageHandler.of().as_CustomMessageHandler());
|
||||
services.AddScoped(_ => IgnoringMessageHandler.of().as_DNSResolverMessageHandler());
|
||||
services.AddScoped(_ => IgnoringMessageHandler.of().as_AsyncPaymentsMessageHandler());
|
||||
services.AddScoped(_ => EmptyNodeIdLookUp.of().as_NodeIdLookUp());
|
||||
|
||||
services.AddScoped<OnionMessenger>(provider =>
|
||||
OnionMessenger.of(
|
||||
provider.GetRequiredService<EntropySource>(),
|
||||
@ -143,9 +146,10 @@ public static class LDKExtensions
|
||||
provider.GetRequiredService<NodeIdLookUp>(),
|
||||
provider.GetRequiredService<MessageRouter>(),
|
||||
provider.GetRequiredService<OffersMessageHandler>(),
|
||||
provider.GetRequiredService<AsyncPaymentsMessageHandler>(),
|
||||
provider.GetRequiredService<DNSResolverMessageHandler>(),
|
||||
provider.GetRequiredService<CustomOnionMessageHandler>()));
|
||||
|
||||
|
||||
services.AddScoped(provider => provider.GetRequiredService<OnionMessenger>().as_OnionMessageHandler());
|
||||
services.AddScoped<LDKBroadcaster>();
|
||||
services.AddScoped<PeerManager>(provider => PeerManager.of(
|
||||
@ -230,9 +234,9 @@ public static class LDKExtensions
|
||||
services.AddScoped<OutputSweeper>(provider =>
|
||||
{
|
||||
var onchainWalletManager = provider.GetRequiredService<OnChainWalletManager>();
|
||||
var resp = onchainWalletManager.GetBestBlock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var hash = uint256.Parse(resp.BlockHash).ToBytes();
|
||||
var bestBlock = BestBlock.of(hash, (int) resp.BlockHeight);
|
||||
var res = onchainWalletManager.GetBestBlock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (res is null) throw new ApplicationException("Could not get best block to instantiate OutputSweeper");
|
||||
var bestBlock = BestBlock.of(uint256.Parse(res.BlockHash).ToBytes(), res.BlockHeight);
|
||||
return OutputSweeper.of(bestBlock,
|
||||
provider.GetRequiredService<BroadcasterInterface>(),
|
||||
provider.GetRequiredService<FeeEstimator>(),
|
||||
@ -247,10 +251,9 @@ public static class LDKExtensions
|
||||
services.AddScoped<ChainParameters>(provider =>
|
||||
{
|
||||
var onchainWalletManager = provider.GetRequiredService<OnChainWalletManager>();
|
||||
var resp = onchainWalletManager.GetBestBlock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var hash = uint256.Parse(resp.BlockHash).ToBytes();
|
||||
|
||||
var bestBlock = BestBlock.of(hash, (int) resp.BlockHeight);
|
||||
var res = onchainWalletManager.GetBestBlock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (res is null) throw new ApplicationException("Could not get best block to instantiate ChainParameters");
|
||||
var bestBlock = BestBlock.of(uint256.Parse(res.BlockHash).ToBytes(), res.BlockHeight);
|
||||
return ChainParameters.of(provider.GetRequiredService<Network>().GetLdkNetwork(), bestBlock);
|
||||
});
|
||||
services.AddScoped<Score>(provider => provider.GetRequiredService<ProbabilisticScorer>().as_Score());
|
||||
@ -260,11 +263,8 @@ public static class LDKExtensions
|
||||
provider.GetRequiredService<MultiThreadedLockableScore>().as_LockableScore());
|
||||
services.AddScoped<WriteableScore>(provider =>
|
||||
provider.GetRequiredService<MultiThreadedLockableScore>().as_WriteableScore());
|
||||
|
||||
services.AddScoped<LDKWalletLogger>();
|
||||
services.AddScoped<Logger>(provider => Logger.new_impl(provider.GetRequiredService<LDKWalletLogger>()));
|
||||
|
||||
|
||||
services.AddScoped<LDKEntropySource>();
|
||||
services.AddScoped<EntropySource>(provider =>
|
||||
EntropySource.new_impl(provider.GetRequiredService<LDKEntropySource>()));
|
||||
@ -307,19 +307,17 @@ public static class LDKExtensions
|
||||
provider.GetRequiredService<LockableScore>(),
|
||||
ProbabilisticScoringFeeParameters.with_default()));
|
||||
services.AddScoped<Router>(provider => provider.GetRequiredService<DefaultRouter>().as_Router());
|
||||
|
||||
|
||||
services.AddScoped<VoltageFlow2Jit>();
|
||||
//services.AddScoped<VoltageFlow2Jit>();
|
||||
services.AddScoped<OlympusFlow2Jit>();
|
||||
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
|
||||
//services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
|
||||
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<OlympusFlow2Jit>());
|
||||
services.AddScoped<IJITService, VoltageFlow2Jit>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
|
||||
//services.AddScoped<IJITService, VoltageFlow2Jit>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
|
||||
services.AddScoped<IJITService, OlympusFlow2Jit>(provider => provider.GetRequiredService<OlympusFlow2Jit>());
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddLDKEventHandler<T>(this IServiceCollection services)
|
||||
private static IServiceCollection AddLDKEventHandler<T>(this IServiceCollection services)
|
||||
where T : class, ILDKEventHandler
|
||||
{
|
||||
services.TryAddScoped<T>();
|
||||
@ -327,8 +325,7 @@ public static class LDKExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static org.ldk.enums.Network GetLdkNetwork(this Network network)
|
||||
private static org.ldk.enums.Network GetLdkNetwork(this Network network)
|
||||
{
|
||||
return network.ChainName switch
|
||||
{
|
||||
@ -386,9 +383,8 @@ public static class LDKExtensions
|
||||
var reason = evt.reason.GetType().Name;
|
||||
switch (evt.reason)
|
||||
{
|
||||
|
||||
case ClosureReason.ClosureReason_CounterpartyForceClosed closureReasonCounterpartyForceClosed:
|
||||
reason += " with msg from peer: " +closureReasonCounterpartyForceClosed.peer_msg.get_a();
|
||||
reason += " with msg from peer: " + closureReasonCounterpartyForceClosed.peer_msg.get_a();
|
||||
break;
|
||||
case ClosureReason.ClosureReason_ProcessingError closureReasonProcessingError:
|
||||
reason += " " + closureReasonProcessingError.err;
|
||||
@ -396,7 +392,6 @@ public static class LDKExtensions
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
|
||||
|
||||
public static byte[]? GetPreimage(this PaymentPurpose purpose, out byte[]? secret)
|
||||
{
|
||||
@ -427,4 +422,4 @@ public static class LDKExtensions
|
||||
throw new ArgumentOutOfRangeException(nameof(purpose));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using NBitcoin;
|
||||
using org.ldk.enums;
|
||||
using org.ldk.structs;
|
||||
using Network = NBitcoin.Network;
|
||||
@ -17,32 +16,33 @@ public class LDKFeeEstimator : FeeEstimatorInterface
|
||||
_network = network;
|
||||
}
|
||||
|
||||
public int get_est_sat_per_1000_weight(ConfirmationTarget confirmation_target)
|
||||
public int get_est_sat_per_1000_weight(ConfirmationTarget confirmationTarget)
|
||||
{
|
||||
var targetBlocks = confirmation_target switch
|
||||
{
|
||||
ConfirmationTarget.LDKConfirmationTarget_OnChainSweep => 30, // High priority (10-50 blocks)
|
||||
// ConfirmationTarget
|
||||
// .LDKConfirmationTarget_MaxAllowedNonAnchorChannelRemoteFee =>
|
||||
// 20, // Moderate to high priority (small multiple of high-priority estimate)
|
||||
ConfirmationTarget
|
||||
.LDKConfirmationTarget_MinAllowedAnchorChannelRemoteFee =>
|
||||
12, // Moderate priority (long-term mempool minimum or medium-priority)
|
||||
ConfirmationTarget
|
||||
.LDKConfirmationTarget_MinAllowedNonAnchorChannelRemoteFee =>
|
||||
12, // Moderate priority (medium-priority feerate)
|
||||
ConfirmationTarget.LDKConfirmationTarget_AnchorChannelFee => 6, // Lower priority (can be bumped later)
|
||||
ConfirmationTarget
|
||||
.LDKConfirmationTarget_NonAnchorChannelFee => 20, // Moderate to high priority (high-priority feerate)
|
||||
ConfirmationTarget.LDKConfirmationTarget_ChannelCloseMinimum => 144, // Within a day or so (144-250 blocks)
|
||||
// Try fixing this to 1sat/vByte, see also
|
||||
// https://github.com/lightningdevkit/rust-lightning/blob/master/lightning/src/chain/chaininterface.rs#L183
|
||||
// https://github.com/MutinyWallet/mutiny-node/blob/master/mutiny-core/src/fees.rs#L193
|
||||
if (confirmationTarget == ConfirmationTarget.LDKConfirmationTarget_MinAllowedAnchorChannelRemoteFee)
|
||||
return 253;
|
||||
|
||||
ConfirmationTarget.LDKConfirmationTarget_OutputSpendingFee => 144,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(confirmation_target), confirmation_target, null)
|
||||
// https://docs.rs/lightning/latest/lightning/chain/chaininterface/enum.ConfirmationTarget.html
|
||||
// https://github.com/lightningdevkit/ldk-node/blob/main/src/fee_estimator.rs#L87
|
||||
var targetBlocks = confirmationTarget switch
|
||||
{
|
||||
ConfirmationTarget.LDKConfirmationTarget_MaximumFeeEstimate => 1,
|
||||
ConfirmationTarget.LDKConfirmationTarget_UrgentOnChainSweep => 6,
|
||||
//ConfirmationTarget.LDKConfirmationTarget_MinAllowedAnchorChannelRemoteFee => 1008,
|
||||
ConfirmationTarget.LDKConfirmationTarget_MinAllowedNonAnchorChannelRemoteFee => 144,
|
||||
ConfirmationTarget.LDKConfirmationTarget_AnchorChannelFee => 1008,
|
||||
ConfirmationTarget.LDKConfirmationTarget_NonAnchorChannelFee => 12,
|
||||
ConfirmationTarget.LDKConfirmationTarget_ChannelCloseMinimum => 144,
|
||||
ConfirmationTarget.LDKConfirmationTarget_OutputSpendingFee => 12,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(confirmationTarget), confirmationTarget, null)
|
||||
};
|
||||
|
||||
if (_network == Network.TestNet && targetBlocks >= 12)
|
||||
targetBlocks = 144;
|
||||
|
||||
return (int) _onChainWalletManager.GetFeeRate(targetBlocks).ConfigureAwait(false).GetAwaiter().GetResult().FeePerK.Satoshi;
|
||||
if (_network == Network.TestNet && targetBlocks >= 12)
|
||||
targetBlocks = 144;
|
||||
|
||||
var feeRate = _onChainWalletManager.GetFeeRate(targetBlocks, confirmationTarget.ToString()).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return (int)feeRate.FeePerK.Satoshi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,9 @@ using Script = NBitcoin.Script;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKFilter : FilterInterface
|
||||
public class LDKFilter(LDKNode ldkNode, ConfigProvider configProvider) : FilterInterface
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
public LDKFilter(LDKNode ldkNode, ConfigProvider configProvider)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public void register_tx(byte[] txid, byte[] script_pubkey)
|
||||
{
|
||||
@ -32,10 +25,13 @@ public class LDKFilter : FilterInterface
|
||||
|
||||
public async Task<List<LDKWatchedOutput>> GetWatchedOutputs()
|
||||
{
|
||||
return await _configProvider.Get<List<LDKWatchedOutput>?>("ln:watchedOutputs")?? [];
|
||||
return await GetWatchedOutputs(configProvider);
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
public static async Task<List<LDKWatchedOutput>> GetWatchedOutputs(ConfigProvider configProvider)
|
||||
{
|
||||
return await configProvider.Get<List<LDKWatchedOutput>?>("ln:watchedOutputs") ?? [];
|
||||
}
|
||||
|
||||
private async Task AddOrUpdateWatchedOutput(LDKWatchedOutput output)
|
||||
{
|
||||
@ -50,7 +46,7 @@ public class LDKFilter : FilterInterface
|
||||
}
|
||||
|
||||
watchedOutputs.Add(output);
|
||||
await _configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
await configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -58,16 +54,15 @@ public class LDKFilter : FilterInterface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void Track(Script script)
|
||||
{
|
||||
_ = _ldkNode.TrackScripts([script]);
|
||||
_ = ldkNode.TrackScripts([script]);
|
||||
}
|
||||
|
||||
public async Task OutputsSpent(List<LDKWatchedOutput> spentWatchedOutputs)
|
||||
{
|
||||
var watchedOutputs = await GetWatchedOutputs();
|
||||
watchedOutputs.RemoveAll(w => spentWatchedOutputs.Any(s => s.Outpoint == w.Outpoint));
|
||||
await _configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
await configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,8 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKKVStore:KVStoreInterface
|
||||
public class LDKKVStore(ConfigProvider configProvider) : KVStoreInterface
|
||||
{
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
public LDKKVStore(ConfigProvider configProvider)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
private string CombineKey(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var str = "ln:";
|
||||
@ -35,28 +28,28 @@ public class LDKKVStore: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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,9 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKLogger : LoggerInterface, ILogger
|
||||
public class LDKLogger(ILoggerFactory loggerFactory) : LoggerInterface, ILogger
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _baseLogger;
|
||||
|
||||
public LDKLogger(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_baseLogger = loggerFactory.CreateLogger("");
|
||||
}
|
||||
private readonly ILogger _baseLogger = loggerFactory.CreateLogger("");
|
||||
|
||||
public virtual void log(Record record)
|
||||
{
|
||||
@ -26,7 +19,7 @@ public class LDKLogger : LoggerInterface, ILogger
|
||||
Level.LDKLevel_Error => LogLevel.Error,
|
||||
Level.LDKLevel_Gossip => LogLevel.Trace,
|
||||
};
|
||||
_loggerFactory.CreateLogger(record.get_module_path()).Log(level, "{Args}", record.get_args());
|
||||
loggerFactory.CreateLogger(record.get_module_path()).Log(level, "{Args}", record.get_args());
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
@ -44,4 +37,4 @@ public class LDKLogger : LoggerInterface, ILogger
|
||||
{
|
||||
_baseLogger.Log(logLevel, eventId, state, exception, formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using AsyncKeyedLock;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
@ -22,16 +22,16 @@ public partial class LDKNode :
|
||||
ILDKEventHandler<Event.Event_ChannelPending>,
|
||||
ILDKEventHandler<Event.Event_ChannelReady>
|
||||
{
|
||||
public async Task<Dictionary<string, (Channel channel, ChannelDetails? channelDetails)>?> GetChannels(CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<(Channel channel, ChannelDetails? channelDetails)>?> GetChannels(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return (await _memoryCache.GetOrCreateAsync(nameof(GetChannels), async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var dbChannels = dbContext.LightningChannels.AsNoTracking().Include(channel => channel.Aliases).AsAsyncEnumerable();
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var dbChannels = dbContext.LightningChannels.AsNoTracking()
|
||||
.Include(channel => channel.Aliases).AsAsyncEnumerable();
|
||||
var channels = ServiceProvider.GetRequiredService<ChannelManager>().list_channels();
|
||||
|
||||
var result = new Dictionary<string, (Channel channel, ChannelDetails? channelDetails)>();
|
||||
var result = new List<(Channel channel, ChannelDetails? channelDetails)>();
|
||||
await foreach (var dbChannel in dbChannels)
|
||||
{
|
||||
var channel = channels.FirstOrDefault(channel =>
|
||||
@ -39,18 +39,16 @@ public partial class LDKNode :
|
||||
var id = Convert.ToHexString(channel.get_channel_id().get_a()).ToLowerInvariant();
|
||||
return id == dbChannel.Id || dbChannel.Aliases.Any(alias => alias.Id == id);
|
||||
});
|
||||
result.Add(dbChannel.Id, (dbChannel, channel));
|
||||
result.Add((dbChannel, channel));
|
||||
}
|
||||
return result;
|
||||
}).WithCancellation(cancellationToken))!;
|
||||
}
|
||||
|
||||
|
||||
public async Task Handle(Event.Event_ChannelClosed evt)
|
||||
{
|
||||
|
||||
_logger.LogInformation($"Channel {Convert.ToHexString(evt.channel_id.get_a()).ToLowerInvariant()} closed: {evt.GetReason()}");
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
_logger.LogInformation("Channel {ChannelId} closed: {Reason}", Convert.ToHexString(evt.channel_id.get_a()).ToLowerInvariant(), evt.GetReason());
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
{
|
||||
{"CloseReason", JsonSerializer.SerializeToElement(evt.reason.write())},
|
||||
{"CloseReasonHuman", JsonSerializer.SerializeToElement(evt.GetReason())},
|
||||
@ -61,17 +59,16 @@ public partial class LDKNode :
|
||||
|
||||
public async Task Handle(Event.Event_ChannelPending evt)
|
||||
{
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
{
|
||||
{"PendingTimestamp", JsonSerializer.SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())}
|
||||
});
|
||||
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelReady evt)
|
||||
{
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
{
|
||||
{"ReadyTimestamp", JsonSerializer.SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())}
|
||||
});
|
||||
@ -82,17 +79,12 @@ public partial class LDKNode :
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(nameof(GetPeers), async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
return ServiceProvider.GetRequiredService<PeerManager>().list_peers();
|
||||
}).WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public void PeersChanged()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
}
|
||||
|
||||
private void InvalidateCache()
|
||||
public void InvalidateCache()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
@ -100,66 +92,48 @@ public partial class LDKNode :
|
||||
|
||||
public async Task<Result_ChannelIdAPIErrorZ> OpenChannel(Money amount, PubKey nodeId)
|
||||
{
|
||||
_logger.LogInformation("Opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
_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>().clone();
|
||||
|
||||
|
||||
var temporaryChannelId = ChannelId.temporary_from_entropy_source(entropySource);
|
||||
|
||||
var userChannelId = new UInt128(temporaryChannelId.get_a().Take(16).ToArray());
|
||||
try
|
||||
{
|
||||
return await AsyncExtensions.RunInOtherThread(() => channelManager.create_channel(nodeId.ToBytes(),
|
||||
amount.Satoshi,
|
||||
0,
|
||||
userChannelId,
|
||||
temporaryChannelId,
|
||||
userConfig));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("finished (trying to) opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
}
|
||||
var result = await AsyncExtensions.RunInOtherThread(() => channelManager.create_channel(nodeId.ToBytes(),
|
||||
amount.Satoshi,
|
||||
0,
|
||||
userChannelId,
|
||||
temporaryChannelId,
|
||||
userConfig));
|
||||
|
||||
if (result.is_ok())
|
||||
_logger.LogInformation("Opened channel with {NodeId} for {Amount}", nodeId, amount);
|
||||
else if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_Err e && e.err.GetError() is var message)
|
||||
_logger.LogError("Opening channel with {NodeId} for {Amount} failed: {Message}", nodeId, amount, message);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async Task CloseChannel(ChannelId channelId, PubKey counterparty, bool force)
|
||||
public async Task<Result_NoneAPIErrorZ> CloseChannel(ChannelId channelId, PubKey counterparty, bool force)
|
||||
{
|
||||
var chanId = Convert.ToHexString(channelId.get_a()).ToLowerInvariant();
|
||||
_logger.LogInformation("{Action} channel {ChannelId} with {Counterparty}", force ? "Force-closing" : "Closing", chanId, counterparty);
|
||||
|
||||
var channelManager = ServiceProvider.GetRequiredService<ChannelManager>();
|
||||
Result_NoneAPIErrorZ? result = null;
|
||||
if (force)
|
||||
{
|
||||
result = channelManager.force_close_broadcasting_latest_txn(channelId, counterparty.ToBytes());
|
||||
}
|
||||
else
|
||||
{
|
||||
result = channelManager.close_channel(channelId, counterparty.ToBytes());
|
||||
|
||||
}
|
||||
if(result.is_ok())
|
||||
{
|
||||
_logger.LogInformation($"Channel {Convert.ToHexString(channelId.get_a()).ToLowerInvariant()} closed");
|
||||
return;
|
||||
}
|
||||
if(result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_Err e && e.err.GetError() is var message)
|
||||
{
|
||||
_logger.LogError($"Error closing channel {Convert.ToHexString(channelId.get_a()).ToLowerInvariant()}: {message}");
|
||||
}
|
||||
|
||||
var result = await AsyncExtensions.RunInOtherThread(() => force
|
||||
? channelManager.force_close_broadcasting_latest_txn(channelId, counterparty.ToBytes(), "User-initiated force-close in unconnected channel state")
|
||||
: channelManager.close_channel(channelId, counterparty.ToBytes()));
|
||||
if (result.is_ok())
|
||||
_logger.LogInformation("Channel {ChannelId} {Action} with {Counterparty}", chanId, counterparty, force ? "force closed" : "closed");
|
||||
if (result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_Err e && e.err.GetError() is var message)
|
||||
_logger.LogError("{Action} channel {ChannelId} with {Counterparty} failed: {Message}", force ? "Force-closing" : "Closing", chanId, counterparty, message);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IJITService?> GetJITLSPService()
|
||||
{
|
||||
var config = await GetConfig();
|
||||
var lsp = config.JITLSP;
|
||||
if (lsp is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (string.IsNullOrEmpty(lsp)) return null;
|
||||
|
||||
var jits = ServiceProvider.GetServices<IJITService>();
|
||||
return jits.FirstOrDefault(jit => jit.ProviderName == lsp);
|
||||
@ -197,7 +171,6 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
private TaskCompletionSource? _started;
|
||||
private readonly SemaphoreSlim _semaphore = new(1);
|
||||
|
||||
|
||||
public IServiceProvider GetServiceProvider() => ServiceProvider;
|
||||
|
||||
public Network Network => ServiceProvider.GetRequiredService<Network>();
|
||||
@ -222,28 +195,23 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidateCache();
|
||||
var walletConfig = await _onChainWalletManager.GetConfig();
|
||||
var lightningConfig = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key) ?? new LightningConfig();
|
||||
|
||||
var keyPath = KeyPath.Parse(lightningConfig.LightningDerivationPath);
|
||||
|
||||
|
||||
Seed = new Mnemonic(walletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey
|
||||
.ToBytes();
|
||||
_logger.LogInformation($"Node {walletConfig.Mnemonic} SEED: {Convert.ToHexString(Seed)}");
|
||||
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");
|
||||
}
|
||||
|
||||
InvalidateCache();
|
||||
var walletConfig = await _onChainWalletManager.GetConfig();
|
||||
var lightningConfig = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key) ?? new LightningConfig();
|
||||
var keyPath = KeyPath.Parse(lightningConfig.LightningDerivationPath);
|
||||
Seed = new Mnemonic(walletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
|
||||
_logger.LogInformation("Starting LDKNode services");
|
||||
foreach (var service in services)
|
||||
{
|
||||
_logger.LogInformation($"Starting {service.GetType().Name}");
|
||||
_logger.LogInformation("Starting {Name}", service.GetType().Name);
|
||||
await service.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@ -270,9 +238,9 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
return await _configProvider.Get<LightningConfig>(LightningConfig.Key) ?? new LightningConfig();
|
||||
}
|
||||
|
||||
public async Task<string[]?> GetJITLSPs()
|
||||
public Task<string[]> GetJITLSPs()
|
||||
{
|
||||
return ServiceProvider.GetServices<IJITService>().Select(jit => jit.ProviderName).ToArray();
|
||||
return Task.FromResult(ServiceProvider.GetServices<IJITService>().Select(jit => jit.ProviderName).ToArray());
|
||||
}
|
||||
|
||||
public async Task UpdateConfig(Func<LightningConfig, Task<(LightningConfig, bool)>> config)
|
||||
@ -283,25 +251,23 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
{
|
||||
var current = await GetConfig();
|
||||
var updated = await config(current);
|
||||
|
||||
|
||||
if (!updated.Item2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _configProvider.Set(LightningConfig.Key, updated.Item1, true);
|
||||
|
||||
|
||||
|
||||
ConfigUpdated?.Invoke(this, updated.Item1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public AsyncEventHandler<LightningConfig>? ConfigUpdated;
|
||||
|
||||
public byte[] Seed { get; private set; }
|
||||
@ -309,9 +275,8 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
public PaymentsManager PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
|
||||
public LightningAPIKeyManager ApiKeyManager => ServiceProvider.GetRequiredService<LightningAPIKeyManager>();
|
||||
public LDKPeerHandler PeerHandler => ServiceProvider.GetRequiredService<LDKPeerHandler>();
|
||||
|
||||
public PubKey NodeId => new(ServiceProvider.GetRequiredService<ChannelManager>().get_our_node_id());
|
||||
|
||||
public Balance[] ClaimableBalances => ServiceProvider.GetRequiredService<ChainMonitor>().get_claimable_balances([]);
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -330,14 +295,13 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
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}");
|
||||
_logger.LogInformation("Stopping {Name}", service.GetType().Name);
|
||||
await service.StopAsync(cancellationToken);
|
||||
_logger.LogInformation($"Stopped {service.GetType().Name}");
|
||||
_logger.LogInformation("Stopped {Name}", service.GetType().Name);
|
||||
}).ToArray();
|
||||
await Task.WhenAll(tasks);
|
||||
// _configProvider.Updated -= Updated;
|
||||
@ -351,7 +315,6 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
// await StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
private readonly TaskCompletionSource<ChannelMonitor[]?> icm = new();
|
||||
// private LightningConfig? _config;
|
||||
|
||||
@ -365,7 +328,9 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
{
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var data = await db.LightningChannels.Where(channel => !channel.Archived).Select(channel => channel.Data)
|
||||
var data = await db.LightningChannels
|
||||
.Where(channel => !channel.Archived && channel.Data != null && channel.Data.Length > 0)
|
||||
.Select(channel => channel.Data!)
|
||||
.ToArrayAsync();
|
||||
|
||||
var channels = ChannelManagerHelper.GetInitialMonitors(data, entropySource, signerProvider);
|
||||
@ -384,7 +349,6 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
await _configProvider.Set("ln:ChannelManager", serializedChannelManager.write(), true);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
|
||||
{
|
||||
await _configProvider.Set("ln:NetworkGraph", networkGraph.write(), true);
|
||||
@ -395,7 +359,6 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
await _configProvider.Set("ln:Score", score.write(), true);
|
||||
}
|
||||
|
||||
|
||||
public async Task<(byte[] serializedChannelManager, ChannelMonitor[] channelMonitors)?> GetSerializedChannelManager(
|
||||
EntropySource entropySource, SignerProvider signerProvider)
|
||||
{
|
||||
@ -417,28 +380,28 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
}
|
||||
|
||||
public Task<string?> Identifier => _onChainWalletManager.GetConfig().ContinueWith(task => task.Result?.Derivations[WalletDerivation.LightningScripts].Identifier);
|
||||
|
||||
|
||||
|
||||
public async Task TrackScripts(Script[] scripts, string derivation = WalletDerivation.LightningScripts)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
_logger.LogDebug("Tracking scripts {Scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
var config = await _onChainWalletManager.GetConfig();
|
||||
var identifier = config.Derivations[derivation].Identifier;
|
||||
|
||||
await _connectionManager.HubProxy.TrackScripts(identifier,
|
||||
scripts.Select(script => script.ToHex()).ToArray()).RunInOtherThread();
|
||||
_logger.LogDebug("Tracked scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
_logger.LogDebug("Tracked scripts {Scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error tracking scripts {scripts}",
|
||||
_logger.LogError(e, "Error tracking scripts {Scripts}",
|
||||
string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
}
|
||||
AsyncKeyedLocker<string> channelLocker = new();
|
||||
|
||||
|
||||
public async Task UpdateChannel(List<ChannelAlias> identifiers, byte[] write, long checkpoint)
|
||||
{
|
||||
using var releaser = await channelLocker.LockAsync(identifiers.First().Id);
|
||||
@ -464,7 +427,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = new Channel()
|
||||
channel = new Channel
|
||||
{
|
||||
Id = ids.First(),
|
||||
Data = write,
|
||||
@ -479,27 +442,20 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Updating channel {channel.Id} with checkpoint {checkpoint}");
|
||||
_logger.LogDebug("Updating channel {ChannelId} with checkpoint {Checkpoint}", channel.Id, checkpoint);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task Peer(PubKey key, PeerInfo? value)
|
||||
{
|
||||
var toString = key.ToString().ToLowerInvariant();
|
||||
await UpdateConfig(async config =>
|
||||
await UpdateConfig(config =>
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
if (config.Peers.Remove(toString))
|
||||
{
|
||||
|
||||
return (config, true);
|
||||
}
|
||||
}
|
||||
if (value is null && config.Peers.Remove(toString))
|
||||
return Task.FromResult((config, true));
|
||||
|
||||
config.Peers.AddOrReplace(toString, value);
|
||||
return (config, true);
|
||||
config.Peers!.AddOrReplace(toString, value);
|
||||
return Task.FromResult((config, true));
|
||||
});
|
||||
}
|
||||
|
||||
@ -519,7 +475,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddChannelData(ChannelId id, Dictionary<string, JsonElement> data)
|
||||
private async Task AddChannelData(ChannelId id, Dictionary<string, JsonElement> data)
|
||||
{
|
||||
var channelId = Convert.ToHexString(id.get_a()).ToLowerInvariant();
|
||||
using var releaser = await channelLocker.LockAsync(channelId);
|
||||
@ -528,7 +484,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
.FirstOrDefaultAsync(channel => !channel.Archived && (channel.Id == channelId || channel.Aliases.Any(alias => alias.Id == channelId)));
|
||||
if (channel is null)
|
||||
{
|
||||
channel = new Channel()
|
||||
channel = new Channel
|
||||
{
|
||||
Id = channelId,
|
||||
Archived = false,
|
||||
@ -536,15 +492,10 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
Checkpoint = 0
|
||||
};
|
||||
|
||||
|
||||
await context.LightningChannels.AddAsync(channel);
|
||||
}
|
||||
|
||||
|
||||
channel.AdditionalData = JsonSerializer.SerializeToNode(channel.AdditionalData)!.MergeDictionary(data).Deserialize<Dictionary<string, JsonElement>>();
|
||||
|
||||
channel.AdditionalData = JsonSerializer.SerializeToNode(channel.AdditionalData)!.MergeDictionary(data).Deserialize<Dictionary<string, JsonElement>>();
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,52 +4,40 @@ using UInt128 = org.ldk.util.UInt128;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
|
||||
|
||||
public class LDKOpenChannelRequestEventHandler: ILDKEventHandler<Event.Event_OpenChannelRequest>
|
||||
public class LDKOpenChannelRequestEventHandler(ChannelManager channelManager, LDKNode node)
|
||||
: ILDKEventHandler<Event.Event_OpenChannelRequest>
|
||||
{
|
||||
private readonly ChannelManager _channelManager;
|
||||
private readonly LDKNode _node;
|
||||
|
||||
public LDKOpenChannelRequestEventHandler(ChannelManager channelManager, LDKNode node)
|
||||
{
|
||||
_channelManager = channelManager;
|
||||
_node = node;
|
||||
}
|
||||
public async Task Handle(Event.Event_OpenChannelRequest eventOpenChannelRequest)
|
||||
{
|
||||
|
||||
var userChannelId = new UInt128(eventOpenChannelRequest.temporary_channel_id.get_a().Take(16).ToArray());
|
||||
|
||||
|
||||
if (eventOpenChannelRequest.channel_type.supports_zero_conf())
|
||||
{
|
||||
var nodeId = Convert.ToHexString(eventOpenChannelRequest.counterparty_node_id).ToLower();
|
||||
|
||||
var config = await _node.GetConfig();
|
||||
if(config.Peers.TryGetValue(nodeId, out var peer) && peer.Trusted)
|
||||
|
||||
var config = await node.GetConfig();
|
||||
if (config.Peers.TryGetValue(nodeId, out var peer) && peer.Trusted)
|
||||
{
|
||||
var result = _channelManager.accept_inbound_channel_from_trusted_peer_0conf(
|
||||
var result = channelManager.accept_inbound_channel_from_trusted_peer_0conf(
|
||||
eventOpenChannelRequest.temporary_channel_id,
|
||||
eventOpenChannelRequest.counterparty_node_id,
|
||||
userChannelId
|
||||
);
|
||||
if(result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_OK)
|
||||
if (result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_OK)
|
||||
AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_channelManager.accept_inbound_channel(
|
||||
|
||||
channelManager.accept_inbound_channel(
|
||||
eventOpenChannelRequest.temporary_channel_id,
|
||||
eventOpenChannelRequest.counterparty_node_id,
|
||||
userChannelId);
|
||||
|
||||
|
||||
AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
|
||||
//TODO: if we want to reject the channel, we can call reject_channel
|
||||
//_channelManager.force_close_without_broadcasting_txn(eventOpenChannelRequest.temporary_channel_id, eventOpenChannelRequest.counterparty_node_id);
|
||||
|
||||
}
|
||||
|
||||
public AsyncEventHandler<Event.Event_OpenChannelRequest>? AcceptedChannel;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,60 +12,51 @@ using NodeInfo = BTCPayServer.Lightning.NodeInfo;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKPeerHandler : IScopedHostedService
|
||||
public class LDKPeerHandler(
|
||||
PeerManager peerManager,
|
||||
LDKWalletLoggerFactory logger,
|
||||
ChannelManager channelManager,
|
||||
LDKNode node,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
BTCPayAppServerClient btcPayAppServerClient)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private readonly ILogger<LDKPeerHandler> _logger;
|
||||
private readonly PeerManager _peerManager;
|
||||
private readonly ChannelManager _channelManager;
|
||||
private readonly LDKNode _node;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly ILogger<LDKPeerHandler> _logger = logger.CreateLogger<LDKPeerHandler>();
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
|
||||
private TaskCompletionSource? _configTcs;
|
||||
private readonly ObservableConcurrentDictionary<string, LDKTcpDescriptor> _descriptors = new();
|
||||
private readonly ConcurrentDictionary<string, Task<LDKTcpDescriptor?>> _connectionTasks = new();
|
||||
public EndPoint? Endpoint { get; set; }
|
||||
|
||||
public LDKPeerHandler(PeerManager peerManager, LDKWalletLoggerFactory logger, ChannelManager channelManager,
|
||||
LDKNode node,
|
||||
BTCPayConnectionManager btcPayConnectionManager, BTCPayAppServerClient btcPayAppServerClient)
|
||||
{
|
||||
_peerManager = peerManager;
|
||||
_channelManager = channelManager;
|
||||
_node = node;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_logger = logger.CreateLogger<LDKPeerHandler>();
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_node.ConfigUpdated += ConfigUpdated;
|
||||
node.ConfigUpdated += ConfigUpdated;
|
||||
_descriptors.CollectionChanged += DescriptorsOnCollectionChanged;
|
||||
_ = ListenForInboundConnections(_cts.Token);
|
||||
_ = ContinuouslyAttemptToConnectToPersistentPeers(_cts.Token);
|
||||
_ = PeriodicTicker(_cts.Token, 1000, () => _peerManager.process_events());
|
||||
_btcPayAppServerClient.OnServerNodeInfo += BtcPayAppServerClientOnOnServerNodeInfo;
|
||||
if (!string.IsNullOrEmpty(_btcPayConnectionManager.ReportedNodeInfo))
|
||||
{
|
||||
_ = BtcPayAppServerClientOnOnServerNodeInfo(null, _btcPayConnectionManager.ReportedNodeInfo);
|
||||
}
|
||||
_ = PeriodicTicker(_cts.Token, 1000, peerManager.process_events);
|
||||
|
||||
btcPayAppServerClient.OnServerNodeInfo += PeerBtcPayServerHost;
|
||||
if (!string.IsNullOrEmpty(btcPayConnectionManager.ReportedNodeInfo))
|
||||
_ = PeerBtcPayServerHost(null, btcPayConnectionManager.ReportedNodeInfo);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void DescriptorsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
_node.PeersChanged();
|
||||
|
||||
node.InvalidateCache();
|
||||
}
|
||||
|
||||
private async Task BtcPayAppServerClientOnOnServerNodeInfo(object? sender, string e)
|
||||
private async Task PeerBtcPayServerHost(object? sender, string? e)
|
||||
{
|
||||
var nodeInfo = NodeInfo.Parse(e);
|
||||
var config = await _node.GetConfig();
|
||||
if (config.Peers.ContainsKey(nodeInfo.NodeId.ToString()))
|
||||
return;
|
||||
var config = await node.GetConfig();
|
||||
if (config.Peers.ContainsKey(nodeInfo.NodeId.ToString())) return;
|
||||
|
||||
var endpoint = new IPEndPoint(IPAddress.Parse(nodeInfo.Host), nodeInfo.Port);
|
||||
await _node.Peer(nodeInfo.NodeId, new PeerInfo()
|
||||
await node.Peer(nodeInfo.NodeId, new PeerInfo
|
||||
{
|
||||
Label = "BTCPay Server Node",
|
||||
Endpoint = endpoint,
|
||||
@ -74,11 +65,10 @@ public class LDKPeerHandler : IScopedHostedService
|
||||
});
|
||||
}
|
||||
|
||||
private TaskCompletionSource? _configTcs;
|
||||
|
||||
private async Task ConfigUpdated(object? sender, LightningConfig e)
|
||||
private Task ConfigUpdated(object? sender, LightningConfig e)
|
||||
{
|
||||
_configTcs?.TrySetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ContinuouslyAttemptToConnectToPersistentPeers(CancellationToken ctsToken)
|
||||
@ -87,42 +77,44 @@ public class LDKPeerHandler : IScopedHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
var connected = peerManager.list_peers().Select(p => Convert.ToHexString(p.get_counterparty_node_id()).ToLower());
|
||||
var chans = await node.GetChannels(ctsToken);
|
||||
var channels = chans?.Where(pair => pair.channelDetails is not null)
|
||||
.Select(pair => pair.channelDetails!).ToList() ?? [];
|
||||
var channelPeers = channels
|
||||
.Select(details => Convert.ToHexString(details.get_counterparty().get_node_id()).ToLower()).Distinct();
|
||||
var config = await node.GetConfig();
|
||||
var missingConnections = config.Peers
|
||||
.Where(pair => pair.Value.Persistent || channelPeers.Contains(pair.Key)).Select(pair => pair.Key)
|
||||
.Except(connected, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||
|
||||
var connected = _peerManager.list_peers().Select(p => Convert.ToHexString(p.get_counterparty_node_id()).ToLower());
|
||||
var channels = (await _node.GetChannels(ctsToken)).Where(pair => pair.Value.channelDetails is not null)
|
||||
.Select(pair => pair.Value.channelDetails!).ToList();
|
||||
|
||||
var channelPeers = channels
|
||||
.Select(details => Convert.ToHexString(details.get_counterparty().get_node_id()).ToLower()).Distinct();
|
||||
var config = await _node.GetConfig();
|
||||
var missingConnections = config.Peers
|
||||
.Where(pair => pair.Value.Persistent || channelPeers.Contains(pair.Key)).Select(pair => pair.Key)
|
||||
.Except(connected, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var persistentPeer in missingConnections)
|
||||
{
|
||||
var kv = config.Peers[persistentPeer];
|
||||
var nodeid = new PubKey(persistentPeer);
|
||||
if (kv.Endpoint is {} endpoint)
|
||||
var tasks = new List<Task>();
|
||||
foreach (var persistentPeer in missingConnections)
|
||||
{
|
||||
var kv = config.Peers[persistentPeer];
|
||||
if (kv.Endpoint is not { } endpoint) continue;
|
||||
var nodeId = new PubKey(persistentPeer);
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsToken);
|
||||
cts.CancelAfter(10000);
|
||||
tasks.Add(ConnectAsync(nodeid, endpoint, cts.Token));
|
||||
tasks.Add(ConnectAsync(nodeId, endpoint, cts.Token));
|
||||
}
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.Delay(5000, ctsToken);
|
||||
catch (Exception e) when (e is { InnerException: SocketException })
|
||||
{
|
||||
_logger.LogError(e.Message);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(e, "Error while attempting to connect to persistent peers");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.Delay(5000, ctsToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task PeriodicTicker(CancellationToken cancellationToken, int ms, Action action)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
@ -140,13 +132,12 @@ public class LDKPeerHandler : IScopedHostedService
|
||||
await Task.Run(() =>
|
||||
{
|
||||
_logger.LogInformation("Stopping, disconnecting all peers");
|
||||
_peerManager.disconnect_all_peers();
|
||||
peerManager.disconnect_all_peers();
|
||||
|
||||
}, cancellationToken);
|
||||
_node.ConfigUpdated -= ConfigUpdated;
|
||||
|
||||
_btcPayAppServerClient.OnServerNodeInfo -= BtcPayAppServerClientOnOnServerNodeInfo;
|
||||
|
||||
node.ConfigUpdated -= ConfigUpdated;
|
||||
btcPayAppServerClient.OnServerNodeInfo -= PeerBtcPayServerHost;
|
||||
_descriptors.CollectionChanged -= DescriptorsOnCollectionChanged;
|
||||
}
|
||||
|
||||
@ -154,11 +145,10 @@ public class LDKPeerHandler : IScopedHostedService
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
var config = await _node.GetConfig();
|
||||
var config = await node.GetConfig();
|
||||
if (!config.AcceptInboundConnection)
|
||||
{
|
||||
_configTcs ??= new();
|
||||
_configTcs ??= new TaskCompletionSource();
|
||||
await _configTcs.Task.WaitAsync(cancellationToken);
|
||||
_configTcs = null;
|
||||
continue;
|
||||
@ -167,109 +157,120 @@ public class LDKPeerHandler : IScopedHostedService
|
||||
using var listener = new TcpListener(new IPEndPoint(IPAddress.Any, 0));
|
||||
listener.Start();
|
||||
var ip = listener.LocalEndpoint;
|
||||
Endpoint = new IPEndPoint(IPAddress.Loopback, (int) ip.Port());
|
||||
Endpoint = new IPEndPoint(IPAddress.Loopback, (int)ip.Port());
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = LDKTcpDescriptor.Inbound(_peerManager,
|
||||
var result = LDKTcpDescriptor.Inbound(peerManager,
|
||||
await listener.AcceptTcpClientAsync(cancellationToken),
|
||||
_logger, _descriptors);
|
||||
if (result is not null)
|
||||
{
|
||||
_descriptors.TryAdd(result.Id, result);
|
||||
_peerManager.process_events();
|
||||
peerManager.process_events();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EndPoint? Endpoint { get; set; }
|
||||
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(NodeInfo nodeInfo,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var remote = IPEndPoint.Parse(nodeInfo.Host + ":" + nodeInfo.Port);
|
||||
return await ConnectAsync(nodeInfo.NodeId, remote, cancellationToken);
|
||||
var remote = nodeInfo.Host + ":" + nodeInfo.Port;
|
||||
if (EndPointParser.TryParse(remote, 9735, out var endpoint))
|
||||
return await ConnectAsync(nodeInfo.NodeId, endpoint, cancellationToken);
|
||||
throw new ArgumentException($"Invalid endpoint: {remote}", nameof(nodeInfo));
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task<LDKTcpDescriptor?>> _connectionTasks = new();
|
||||
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey peerNodeId,PeerInfo peerInfo, CancellationToken cancellationToken = default)
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey peerNodeId, PeerInfo peerInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (peerInfo.Endpoint is {} endpoint)
|
||||
{
|
||||
if(peerInfo.Label is not null)
|
||||
_logger.LogInformation($"Attempting to connect to {peerNodeId} at {endpoint} ({peerInfo.Label})");
|
||||
return await ConnectAsync(peerNodeId, endpoint, cancellationToken);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (peerInfo.Endpoint is not { } endpoint) return null;
|
||||
if (peerInfo.Label is not null)
|
||||
_logger.LogInformation("Attempting to connect to {NodeId} at {Endpoint} ({Label})", peerNodeId, endpoint.ToEndpointString(), peerInfo.Label);
|
||||
return await ConnectAsync(peerNodeId, endpoint, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey theirNodeId, EndPoint remote,
|
||||
CancellationToken cancellationToken = default)
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey theirNodeId, EndPoint remote, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//cache this task so that we dont have multiple attempts to connect to the same place
|
||||
|
||||
if (_connectionTasks.TryGetValue(theirNodeId.ToString(), out var task))
|
||||
var nodeId = theirNodeId.ToString();
|
||||
//cache this task so that we don't have multiple attempts to connect to the same place
|
||||
if (_connectionTasks.TryGetValue(nodeId, out var task))
|
||||
{
|
||||
_logger.LogInformation($"Already attempting to connect to {theirNodeId}");
|
||||
_logger.LogInformation("Already attempting to connect to {NodeId}", nodeId);
|
||||
return await task.WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<LDKTcpDescriptor?>();
|
||||
try
|
||||
{
|
||||
if (!_connectionTasks.TryAdd(theirNodeId.ToString(), tcs.Task))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_channelManager.get_our_node_id().SequenceEqual(theirNodeId.ToBytes()))
|
||||
if (!_connectionTasks.TryAdd(nodeId, tcs.Task))
|
||||
return null;
|
||||
|
||||
if (channelManager.get_our_node_id().SequenceEqual(theirNodeId.ToBytes()))
|
||||
return null;
|
||||
|
||||
var ipEndpoint = remote.IPEndPoint();
|
||||
var client = new TcpClient();
|
||||
await client.ConnectAsync(remote.IPEndPoint(), cancellationToken);
|
||||
await client.ConnectAsync(ipEndpoint, cancellationToken);
|
||||
|
||||
|
||||
var result = LDKTcpDescriptor.Outbound(_peerManager, client, _logger, theirNodeId, _descriptors);
|
||||
var result = LDKTcpDescriptor.Outbound(peerManager, client, _logger, theirNodeId, _descriptors);
|
||||
if (result is not null)
|
||||
{
|
||||
_descriptors.TryAdd(result.Id, result);
|
||||
_peerManager.process_events();
|
||||
|
||||
var config = await _node.GetConfig();
|
||||
if (!config.Peers.TryGetValue(theirNodeId.ToString(), out var peer))
|
||||
var needsUpdate = false;
|
||||
var config = await node.GetConfig();
|
||||
if (!config.Peers.TryGetValue(nodeId, out var peer))
|
||||
{
|
||||
peer = new PeerInfo();
|
||||
peer = new PeerInfo { Trusted = true };
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (!peer.Persistent)
|
||||
{
|
||||
peer.Persistent = true;
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (peer.Endpoint?.ToEndpointString() != remote.ToString())
|
||||
{
|
||||
peer.Endpoint = remote;
|
||||
await _node.Peer(theirNodeId, peer);
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (needsUpdate)
|
||||
await node.Peer(theirNodeId, peer);
|
||||
|
||||
_descriptors.TryAdd(result.Id, result);
|
||||
peerManager.process_events();
|
||||
}
|
||||
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// wrap the original exception to report on the failing node URI
|
||||
tcs.TrySetException(new Exception($"Socket connection to {nodeId}@{remote} failed: {e.Message}", e));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.TrySetException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionTasks.TryRemove(theirNodeId.ToString(), out _);
|
||||
_connectionTasks.TryRemove(nodeId, out _);
|
||||
}
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
|
||||
public async Task DisconnectAsync(PubKey id)
|
||||
{
|
||||
_logger.LogInformation($"Disconnecting from {id}");
|
||||
_peerManager.disconnect_by_node_id(id.ToBytes());
|
||||
_logger.LogInformation($"Disconnected from {id}");
|
||||
_logger.LogInformation("Disconnecting from {NodeId}", id);
|
||||
peerManager.disconnect_by_node_id(id.ToBytes());
|
||||
|
||||
var config = await node.GetConfig();
|
||||
if (config.Peers.TryGetValue(id.ToString(), out var peer) && peer.Persistent)
|
||||
{
|
||||
peer.Persistent = false;
|
||||
await node.Peer(id, peer);
|
||||
}
|
||||
|
||||
peerManager.process_events();
|
||||
_logger.LogInformation("Disconnected from {NodeId}", id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,38 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using org.ldk.structs;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class LDKPendingHTLCsForwardableEventHandler : IScopedHostedService, ILDKEventHandler<Event.Event_PendingHTLCsForwardable>
|
||||
public class LDKPendingHTLCsForwardableEventHandler(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<LDKPendingHTLCsForwardableEventHandler> logger)
|
||||
: IScopedHostedService, ILDKEventHandler<Event.Event_PendingHTLCsForwardable>
|
||||
{
|
||||
private readonly ConcurrentQueue<DateTimeOffset> _scheduledTimes;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<LDKPendingHTLCsForwardableEventHandler> _logger;
|
||||
private readonly ConcurrentQueue<DateTimeOffset> _scheduledTimes = new();
|
||||
|
||||
public LDKPendingHTLCsForwardableEventHandler(IServiceProvider serviceProvider, ILogger<LDKPendingHTLCsForwardableEventHandler> logger)
|
||||
{
|
||||
_scheduledTimes = new ConcurrentQueue<DateTimeOffset>();
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public Task Handle(Event.Event_PendingHTLCsForwardable eventPendingHtlCsForwardable)
|
||||
{
|
||||
var time = Random.Shared.NextInt64(eventPendingHtlCsForwardable.time_forwardable,
|
||||
5 * eventPendingHtlCsForwardable.time_forwardable);
|
||||
_logger.LogDebug($"Scheduling processing of pending HTLC forwards in {time} seconds");
|
||||
logger.LogDebug("Scheduling processing of pending HTLC forwards in {Time} seconds", time);
|
||||
_scheduledTimes.Enqueue(DateTimeOffset.UtcNow.AddSeconds(time));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationTokenSource= new CancellationTokenSource();
|
||||
_ = ExecuteAsync(_cancellationTokenSource.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@ -45,23 +44,15 @@ public class LDKPendingHTLCsForwardableEventHandler : IScopedHostedService, ILDK
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Processing pending HTLC forwards");
|
||||
|
||||
_ = Task.Run(() => _serviceProvider.GetRequiredService<ChannelManager>().process_pending_htlc_forwards(), stoppingToken);
|
||||
logger.LogDebug("Processing pending HTLC forwards");
|
||||
|
||||
_ = Task.Run(() => serviceProvider.GetRequiredService<ChannelManager>().process_pending_htlc_forwards(), stoppingToken);
|
||||
}
|
||||
|
||||
await Task.Delay(1000, stoppingToken); // Polling delay
|
||||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationTokenSource= new CancellationTokenSource();
|
||||
_ = ExecuteAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync().WithCancellation(cancellationToken);
|
||||
|
||||
@ -9,6 +9,7 @@ using NBitcoin;
|
||||
using org.ldk.enums;
|
||||
using org.ldk.structs;
|
||||
using VSSProto;
|
||||
using Exception = System.Exception;
|
||||
using OutPoint = org.ldk.structs.OutPoint;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
@ -21,8 +22,6 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
private readonly SyncService _syncService;
|
||||
private readonly ConcurrentDictionary<long, Task> updateTasks = new();
|
||||
private readonly ConcurrentDictionary<long, TaskCompletionSource> _updateTaskCompletionSources = new();
|
||||
private Dictionary<long,long> _updateIds = new();
|
||||
// private readonly ChainMonitor _chainMonitor;
|
||||
|
||||
public LDKPersistInterface(LDKNode node, ILogger<LDKPersistInterface> logger, IServiceProvider serviceProvider, SyncService syncService )
|
||||
{
|
||||
@ -35,7 +34,6 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
|
||||
private Task RemoteObjectUpdated(object? sender, (List<Outbox> OutboxItemsProcesed, PutObjectRequest RemoteRequest) e)
|
||||
{
|
||||
|
||||
var channelUpdates = e.RemoteRequest.TransactionItems.Where(x => x.Key.StartsWith("Channel_")).Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!).ToArray();
|
||||
foreach (var channelUpdate in channelUpdates)
|
||||
{
|
||||
@ -45,18 +43,12 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public ChannelMonitorUpdateStatus persist_new_channel(OutPoint channel_funding_outpoint, ChannelMonitor data,
|
||||
MonitorUpdateId update_id)
|
||||
public ChannelMonitorUpdateStatus persist_new_channel(OutPoint channelFundingOutpoint, ChannelMonitor data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(
|
||||
$"Persisting new channel, outpoint: {channel_funding_outpoint.Outpoint()}, updateid: {update_id.hash()}");
|
||||
|
||||
var updateId = update_id.hash();
|
||||
var updateId = data.get_latest_update_id();
|
||||
_logger.LogDebug("Persisting new channel, outpoint: {Outpoint}, updateId: {UpdateId}", channelFundingOutpoint.Outpoint(), updateId);
|
||||
|
||||
var taskResult = updateTasks.GetOrAdd(updateId, async l =>
|
||||
{
|
||||
@ -65,31 +57,33 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
// .SelectMany(zzzz => zzzz.get_b().Select(zz => Script.FromBytesUnsafe(zz.get_b()))).ToArray();
|
||||
|
||||
_updateTaskCompletionSources.TryAdd(updateId, new TaskCompletionSource());
|
||||
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower();
|
||||
|
||||
var identifiers = new List<ChannelAlias>();
|
||||
identifiers.Add(new ChannelAlias()
|
||||
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channelFundingOutpoint).get_a()).ToLower();
|
||||
|
||||
var identifiers = new List<ChannelAlias>
|
||||
{
|
||||
Id = fundingId,
|
||||
Type = "funding_outpoint"
|
||||
});
|
||||
new()
|
||||
{
|
||||
Id = fundingId,
|
||||
Type = "funding_outpoint"
|
||||
}
|
||||
};
|
||||
var otherId = data.channel_id().is_zero()? null: Convert.ToHexString(data.channel_id().get_a()).ToLower();
|
||||
if(otherId == fundingId)
|
||||
if (otherId == fundingId)
|
||||
{
|
||||
otherId = null;
|
||||
|
||||
|
||||
}
|
||||
if(otherId != null)
|
||||
if (otherId != null)
|
||||
{
|
||||
identifiers.Add(new ChannelAlias()
|
||||
identifiers.Add(new ChannelAlias
|
||||
{
|
||||
Id = otherId,
|
||||
Type = "arbitrary_id"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// var trackTask = _node.TrackScripts(outs).ContinueWith(task => _logger.LogDebug($"Tracking scripts finished for updateid: {update_id.hash()}"));;
|
||||
var updateTask = _node.UpdateChannel(identifiers, data.write(), updateId).ContinueWith(task => _logger.LogDebug($"Updating channel finished for updateid: {update_id.hash()}"));;
|
||||
var updateTask = _node.UpdateChannel(identifiers, data.write(), updateId).ContinueWith(task => _logger.LogDebug("Updating channel finished for updateId: {UpdateId}", updateId));
|
||||
await updateTask;
|
||||
// _logger.LogDebug("channel updated to local, waiting for remote sync to finish");
|
||||
// await _updateTaskCompletionSources[updateId].Task;
|
||||
@ -98,23 +92,17 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
_logger.LogDebug(
|
||||
$"Calling channel_monitor_updated, outpoint: {channel_funding_outpoint.Outpoint()}, updateid: {update_id.hash()}");
|
||||
|
||||
_serviceProvider.GetRequiredService<ChainMonitor>()
|
||||
.channel_monitor_updated(channel_funding_outpoint, update_id);
|
||||
_logger.LogDebug(
|
||||
$"Persisted new channel, outpoint: {channel_funding_outpoint.Outpoint()}, updateid: {update_id.hash()}");
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
_logger.LogDebug("Calling channel_monitor_updated, outpoint: {Outpoint}, updateId: {UpdateId}", channelFundingOutpoint.Outpoint(), updateId);
|
||||
_serviceProvider.GetRequiredService<ChainMonitor>().channel_monitor_updated(channelFundingOutpoint, updateId);
|
||||
_logger.LogDebug("Persisted new channel, outpoint: {Outpoint}, updateId: {UpdateId}", channelFundingOutpoint.Outpoint(), updateId);
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error calling channel_monitor_updated for new channel ");
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
// _chainMonitor.channel_monitor_updated(channel_funding_outpoint, update_id);
|
||||
});
|
||||
|
||||
@ -127,57 +115,48 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
|
||||
if (taskResult.IsCompleted)
|
||||
{
|
||||
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed;
|
||||
|
||||
}
|
||||
|
||||
|
||||
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_InProgress;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_UnrecoverableError;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public ChannelMonitorUpdateStatus update_persisted_channel(OutPoint channel_funding_outpoint, ChannelMonitorUpdate update,
|
||||
ChannelMonitor data, MonitorUpdateId update_id)
|
||||
public ChannelMonitorUpdateStatus update_persisted_channel(OutPoint channelFundingOutpoint, ChannelMonitorUpdate? update, ChannelMonitor data)
|
||||
{
|
||||
|
||||
var updateId = update_id.hash();
|
||||
var updateId = update?.get_update_id() ?? data.get_latest_update_id();
|
||||
|
||||
_updateTaskCompletionSources.TryAdd(updateId, new TaskCompletionSource());
|
||||
var taskResult = updateTasks.GetOrAdd(updateId, async l =>
|
||||
{
|
||||
|
||||
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower();
|
||||
|
||||
var identifiers = new List<ChannelAlias>();
|
||||
identifiers.Add(new ChannelAlias()
|
||||
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channelFundingOutpoint).get_a()).ToLower();
|
||||
var identifiers = new List<ChannelAlias>
|
||||
{
|
||||
Id = fundingId,
|
||||
Type = "funding_outpoint"
|
||||
});
|
||||
new()
|
||||
{
|
||||
Id = fundingId,
|
||||
Type = "funding_outpoint"
|
||||
}
|
||||
};
|
||||
var otherId = data.channel_id().is_zero()? null: Convert.ToHexString(data.channel_id().get_a()).ToLower();
|
||||
if(otherId == fundingId)
|
||||
if (otherId == fundingId)
|
||||
{
|
||||
otherId = null;
|
||||
|
||||
}
|
||||
if(otherId != null)
|
||||
if (otherId != null)
|
||||
{
|
||||
identifiers.Add(new ChannelAlias()
|
||||
identifiers.Add(new ChannelAlias
|
||||
{
|
||||
Id = otherId,
|
||||
Type = "arbitrary_id"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await _node.UpdateChannel(identifiers, data.write(), updateId);
|
||||
_logger.LogDebug("channel updated to local, waiting for remote sync to finish");
|
||||
// await _updateTaskCompletionSources[updateId].Task;
|
||||
@ -185,16 +164,11 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
|
||||
await AsyncExtensions.RunInOtherThread(() =>
|
||||
{
|
||||
_logger.LogDebug(
|
||||
$"Calling channel_monitor_updated, outpoint: {channel_funding_outpoint.Outpoint()}, updateid: {update_id.hash()}");
|
||||
|
||||
_serviceProvider.GetRequiredService<ChainMonitor>()
|
||||
.channel_monitor_updated(channel_funding_outpoint, update_id);
|
||||
_logger.LogDebug(
|
||||
$"Persisted update channel, outpoint: {channel_funding_outpoint.Outpoint()}, updateid: {update_id.hash()}");
|
||||
_logger.LogDebug("Calling channel_monitor_updated, outpoint: {Outpoint}, updateId: {UpdateId}", channelFundingOutpoint.Outpoint(), updateId);
|
||||
_serviceProvider.GetRequiredService<ChainMonitor>().channel_monitor_updated(channelFundingOutpoint, updateId);
|
||||
_logger.LogDebug("Persisted update channel, outpoint: {Outpoint}, updateId: {UpdateId}", channelFundingOutpoint.Outpoint(), updateId);
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (taskResult.IsFaulted)
|
||||
@ -206,37 +180,28 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
|
||||
if (taskResult.IsCompleted)
|
||||
{
|
||||
|
||||
updateTasks.TryRemove(updateId, out _);
|
||||
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed;
|
||||
|
||||
}
|
||||
|
||||
|
||||
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_InProgress;
|
||||
}
|
||||
|
||||
public void archive_persisted_channel(OutPoint channel_funding_outpoint)
|
||||
public void archive_persisted_channel(OutPoint channelFundingOutpoint)
|
||||
{
|
||||
_logger.LogInformation($"Archiving channel, outpoint: {channel_funding_outpoint.Outpoint()}");
|
||||
_logger.LogInformation("Archiving channel, outpoint: {Outpoint}", channelFundingOutpoint.Outpoint());
|
||||
AsyncExtensions.RunInOtherThread(() =>
|
||||
_node.ArchiveChannel(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint))).GetAwaiter().GetResult();
|
||||
}
|
||||
//
|
||||
// public async Task StartAsync(CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
//
|
||||
// public async Task StopAsync(CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_node.ArchiveChannel(ChannelId.v1_from_funding_outpoint(channelFundingOutpoint))).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_syncService.RemoteObjectUpdated -= RemoteObjectUpdated;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,23 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKPersister : PersisterInterface
|
||||
public class LDKPersister(LDKNode ldkNode) : PersisterInterface
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
|
||||
public LDKPersister(LDKNode ldkNode)
|
||||
public Result_NoneIOErrorZ persist_manager(ChannelManager channelManager)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_manager(ChannelManager channel_manager)
|
||||
{
|
||||
_ldkNode.UpdateChannelManager(channel_manager).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
ldkNode.UpdateChannelManager(channelManager).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_graph(NetworkGraph network_graph)
|
||||
public Result_NoneIOErrorZ persist_graph(NetworkGraph networkGraph)
|
||||
{
|
||||
_ldkNode.UpdateNetworkGraph(network_graph).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
ldkNode.UpdateNetworkGraph(networkGraph).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_scorer(WriteableScore scorer)
|
||||
{
|
||||
_ldkNode.UpdateScore(scorer).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
ldkNode.UpdateScore(scorer).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,33 +6,23 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
public class LDKRapidGossipSyncer(
|
||||
LDKNode ldkNode,
|
||||
RapidGossipSync rapidGossipSync,
|
||||
NetworkGraph networkGraph,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<LDKRapidGossipSyncer> logger)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly RapidGossipSync _rapidGossipSync;
|
||||
private readonly NetworkGraph _networkGraph;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<LDKRapidGossipSyncer> _logger;
|
||||
private CancellationTokenSource? _cts;
|
||||
private TaskCompletionSource _configUpdated = new();
|
||||
|
||||
public LDKRapidGossipSyncer(LDKNode ldkNode,
|
||||
RapidGossipSync rapidGossipSync,
|
||||
NetworkGraph networkGraph,
|
||||
IHttpClientFactory httpClientFactory, ILogger<LDKRapidGossipSyncer> logger)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_rapidGossipSync = rapidGossipSync;
|
||||
_networkGraph = networkGraph;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_ldkNode.ConfigUpdated += OnConfigUpdated;
|
||||
ldkNode.ConfigUpdated += OnConfigUpdated;
|
||||
_ = UpdateNetworkGraph();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnConfigUpdated(object? sender, LightningConfig e)
|
||||
@ -48,7 +38,7 @@ public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
try
|
||||
{
|
||||
_configUpdated = new();
|
||||
var config = await _ldkNode.GetConfig();
|
||||
var config = await ldkNode.GetConfig();
|
||||
if (config.RapidGossipSyncUrl is null)
|
||||
{
|
||||
try
|
||||
@ -63,30 +53,30 @@ public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
// wait until config is updated or _cts is cancelled
|
||||
}
|
||||
|
||||
var timestamp = _networkGraph.get_last_rapid_gossip_sync_timestamp() is Option_u32Z.Option_u32Z_Some some
|
||||
var timestamp = networkGraph.get_last_rapid_gossip_sync_timestamp() is Option_u32Z.Option_u32Z_Some some
|
||||
? some.some : 0;
|
||||
var uri = new Uri(config.RapidGossipSyncUrl, $"/snapshot/{timestamp}");
|
||||
var response = await _httpClientFactory.CreateClient("rgs").GetAsync(uri, _cts.Token);
|
||||
var response = await httpClientFactory.CreateClient("rgs").GetAsync(uri, _cts.Token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("Failed to download snapshot from {uri}", uri);
|
||||
logger.LogError("Failed to download snapshot from {uri}", uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var snapshot = await response.Content.ReadAsByteArrayAsync();
|
||||
var result =
|
||||
_rapidGossipSync.update_network_graph_no_std(snapshot,
|
||||
rapidGossipSync.update_network_graph_no_std(snapshot,
|
||||
Option_u64Z.some(DateTime.Now.ToUnixTimestamp()));
|
||||
if (result is Result_u32GraphSyncErrorZ.Result_u32GraphSyncErrorZ_Err err)
|
||||
{
|
||||
switch (err.err)
|
||||
{
|
||||
case GraphSyncError.GraphSyncError_DecodeError graphSyncErrorDecodeError:
|
||||
_logger.LogError(
|
||||
logger.LogError(
|
||||
$"Failed to decode snapshot from {uri} with error {graphSyncErrorDecodeError.decode_error.GetType().Name}");
|
||||
break;
|
||||
case GraphSyncError.GraphSyncError_LightningError graphSyncErrorLightningError:
|
||||
_logger.LogError(
|
||||
logger.LogError(
|
||||
$"Failed to update network graph with error {graphSyncErrorLightningError.lightning_error.get_err()}");
|
||||
// config = await _ldkNode.GetConfig();
|
||||
// await _ldkNode.UpdateConfig(config);
|
||||
@ -105,7 +95,7 @@ public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while updating network graph");
|
||||
logger.LogError(e, "Error while updating network graph");
|
||||
await Task.Delay(10000, _cts.Token);
|
||||
}
|
||||
}
|
||||
@ -113,11 +103,11 @@ public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ldkNode.ConfigUpdated -= OnConfigUpdated;
|
||||
ldkNode.ConfigUpdated -= OnConfigUpdated;
|
||||
if (_cts is not null)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,52 +5,42 @@ using UInt128 = org.ldk.util.UInt128;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKSignerProvider : SignerProviderInterface
|
||||
public class LDKSignerProvider(KeysManager innerSigner, LDKNode ldkNode) : SignerProviderInterface
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly SignerProvider _innerSigner;
|
||||
|
||||
public LDKSignerProvider(KeysManager innerSigner, LDKNode ldkNode)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_innerSigner = innerSigner.as_SignerProvider();
|
||||
}
|
||||
private readonly SignerProvider _innerSigner = innerSigner.as_SignerProvider();
|
||||
|
||||
public byte[] generate_channel_keys_id(bool inbound, long channel_value_satoshis, UInt128 user_channel_id)
|
||||
{
|
||||
return _innerSigner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id);
|
||||
}
|
||||
|
||||
public WriteableEcdsaChannelSigner derive_channel_signer(long channel_value_satoshis, byte[] channel_keys_id)
|
||||
public EcdsaChannelSigner derive_channel_signer(long channel_value_satoshis, byte[] channel_keys_id)
|
||||
{
|
||||
return _innerSigner.derive_channel_signer(channel_value_satoshis, channel_keys_id);
|
||||
}
|
||||
|
||||
public Result_WriteableEcdsaChannelSignerDecodeErrorZ read_chan_signer(byte[] reader)
|
||||
public Result_EcdsaChannelSignerDecodeErrorZ read_chan_signer(byte[] reader)
|
||||
{
|
||||
return _innerSigner.read_chan_signer(reader);
|
||||
}
|
||||
|
||||
public Result_CVec_u8ZNoneZ get_destination_script(byte[] channel_keys_id)
|
||||
{
|
||||
var script = _ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
var script = ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
return Result_CVec_u8ZNoneZ.ok(script.ToBytes());
|
||||
}
|
||||
|
||||
public Result_ShutdownScriptNoneZ get_shutdown_scriptpubkey()
|
||||
{
|
||||
var script = _ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
|
||||
var script = ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
|
||||
if (!script.IsScriptType(ScriptType.Witness))
|
||||
{ throw new NotSupportedException("Generated a non witness script."); }
|
||||
|
||||
throw new NotSupportedException("Generated a non witness script.");
|
||||
|
||||
var witnessParams = PayToWitTemplate.Instance.ExtractScriptPubKeyParameters2(script);
|
||||
var result = ShutdownScript.new_witness_program(new WitnessProgram(witnessParams.Program,
|
||||
new WitnessVersion((byte) witnessParams.Version)));
|
||||
if(result is Result_ShutdownScriptInvalidShutdownScriptZ.Result_ShutdownScriptInvalidShutdownScriptZ_OK ok)
|
||||
return Result_ShutdownScriptNoneZ.ok(ok.res);
|
||||
return Result_ShutdownScriptNoneZ.err();
|
||||
var result = ShutdownScript.new_witness_program(new WitnessProgram(witnessParams.Program, new WitnessVersion((byte) witnessParams.Version)));
|
||||
return result is Result_ShutdownScriptInvalidShutdownScriptZ.Result_ShutdownScriptInvalidShutdownScriptZ_OK ok
|
||||
? Result_ShutdownScriptNoneZ.ok(ok.res)
|
||||
: Result_ShutdownScriptNoneZ.err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,14 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_SpendableOutputs>
|
||||
public class LDKSpendableOutputEventHandler(OutputSweeper outputSweeper)
|
||||
: ILDKEventHandler<Event.Event_SpendableOutputs>
|
||||
{
|
||||
private readonly OutputSweeper _outputSweeper;
|
||||
|
||||
public LDKSpendableOutputEventHandler(OutputSweeper outputSweeper)
|
||||
public Task Handle(Event.Event_SpendableOutputs eventSpendableOutputs)
|
||||
{
|
||||
_outputSweeper = outputSweeper;
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_SpendableOutputs eventSpendableOutputs)
|
||||
{
|
||||
_outputSweeper.track_spendable_outputs(eventSpendableOutputs.outputs, eventSpendableOutputs.channel_id, true,
|
||||
outputSweeper.track_spendable_outputs(eventSpendableOutputs.outputs, eventSpendableOutputs.channel_id, true,
|
||||
Option_u32Z.none());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
// }
|
||||
@ -30,8 +25,8 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// public LDKSpendableOutputEventHandler(
|
||||
// BTCPayConnectionManager connectionManager,
|
||||
// OnChainWalletManager onChainWalletManager,
|
||||
// BTCPayAppServerClient appServerClient,
|
||||
// LDKNode node,
|
||||
// BTCPayAppServerClient appServerClient,
|
||||
// LDKNode node,
|
||||
// IDbContextFactory<AppDbContext> dbContextFactory, OutputSweeper outputSweeper)
|
||||
// {
|
||||
// _connectionManager = connectionManager;
|
||||
@ -50,13 +45,13 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// }
|
||||
// //
|
||||
// // var toPersist = eventSpendableOutputs.outputs.Where(descriptor => descriptor is not SpendableOutputDescriptor.SpendableOutputDescriptor_StaticOutput);
|
||||
// //
|
||||
// //
|
||||
// // await PersistSpendableOutputs(toPersist);
|
||||
// // }
|
||||
// //
|
||||
// // private async Task PersistSpendableOutputs(IEnumerable<SpendableOutputDescriptor> toPersist)
|
||||
// // {
|
||||
// //
|
||||
// //
|
||||
// // await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
// // List<Script> scripts = new();
|
||||
// // var spendableOutputs = toPersist.Select(descriptor =>
|
||||
@ -73,7 +68,7 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// // Outpoint = outpoint,
|
||||
// // Script = txout.ScriptPubKey.ToHex(),
|
||||
// // Data = descriptor.write()
|
||||
// //
|
||||
// //
|
||||
// // };
|
||||
// // }
|
||||
// // case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticPaymentOutput staticPaymentOutput:
|
||||
@ -126,7 +121,7 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// // }
|
||||
// // // }else if(_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.SpendableOutputs].Identifier != e.identifier)
|
||||
// // // return;
|
||||
// // //
|
||||
// // //
|
||||
// // //
|
||||
// // // await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
// // // var spendableCoins = await context.SpendableCoins
|
||||
@ -142,8 +137,8 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// // //
|
||||
// // // var spendableOutputDescriptors = spendableCoins.Select(coin => (coin, SpendableOutputDescriptor.read(coin.Data))).ToArray();
|
||||
// // //
|
||||
// //
|
||||
// //
|
||||
// //
|
||||
// //
|
||||
// // }
|
||||
// //
|
||||
// // public async Task StartAsync(CancellationToken cancellationToken)
|
||||
@ -159,4 +154,4 @@ public class LDKSpendableOutputEventHandler : ILDKEventHandler<Event.Event_Spend
|
||||
// // _appServerClient.OnTransactionDetected -= AppServerClientOnTransactionDetected;
|
||||
// // _appServerClient.OnNewBlock -= AppServerClientOnOnNewBlock;
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -15,67 +15,11 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
private readonly NetworkStream _stream;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
|
||||
public SocketDescriptor SocketDescriptor { get; set; }
|
||||
private SocketDescriptor SocketDescriptor { get; set; }
|
||||
public string Id { get; set; }
|
||||
readonly SemaphoreSlim _readSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _readSemaphore = new(1, 1);
|
||||
private readonly TaskCompletionSource _tcs;
|
||||
|
||||
public static LDKTcpDescriptor? Inbound(PeerManager peerManager, TcpClient tcpClient, ILogger logger, ObservableConcurrentDictionary<string, LDKTcpDescriptor> descriptors)
|
||||
{
|
||||
var descriptor = new LDKTcpDescriptor(peerManager, tcpClient, logger,s => descriptors.TryRemove(s, out _));
|
||||
var result = peerManager.new_inbound_connection(descriptor.SocketDescriptor, tcpClient.Client.GetSocketAddress());
|
||||
if (result.is_ok())
|
||||
{
|
||||
logger.LogInformation("New inbound connection accepted");
|
||||
descriptor.Start();
|
||||
return descriptor;
|
||||
}
|
||||
else if(result is Result_NonePeerHandleErrorZ.Result_NonePeerHandleErrorZ_Err err)
|
||||
{
|
||||
logger.LogError($"Failed to create inbound connection");
|
||||
tcpClient.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static LDKTcpDescriptor? Outbound(PeerManager peerManager, TcpClient tcpClient, ILogger logger,
|
||||
PubKey pubKey, ObservableConcurrentDictionary<string, LDKTcpDescriptor> descriptors)
|
||||
{
|
||||
var descriptor = new LDKTcpDescriptor(peerManager, tcpClient, logger, s => descriptors.TryRemove(s, out _));
|
||||
var saSocketAddress = tcpClient.Client?.GetSocketAddress();
|
||||
if(saSocketAddress is null)
|
||||
{
|
||||
logger.LogWarning("Failed to get tcp client or socket address so cannot create outbound connection");
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.LogInformation($"Connected to {pubKey} at {((Option_SocketAddressZ.Option_SocketAddressZ_Some)saSocketAddress).some.to_str()}");
|
||||
descriptor.Start();
|
||||
var result = peerManager.new_outbound_connection(pubKey.ToBytes(), descriptor.SocketDescriptor,saSocketAddress);
|
||||
if (result is Result_CVec_u8ZPeerHandleErrorZ.Result_CVec_u8ZPeerHandleErrorZ_OK ok)
|
||||
{
|
||||
descriptor.send_data(ok.res, true);
|
||||
}
|
||||
|
||||
if (result.is_ok())
|
||||
{
|
||||
logger.LogInformation("New outbound connection accepted");
|
||||
|
||||
return descriptor;
|
||||
}else if(result is Result_CVec_u8ZPeerHandleErrorZ.Result_CVec_u8ZPeerHandleErrorZ_Err err)
|
||||
{
|
||||
logger.LogError($"Failed to create outbound connection: {err.err}");
|
||||
tcpClient.Dispose();
|
||||
return null;
|
||||
}
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
private LDKTcpDescriptor(PeerManager peerManager, TcpClient tcpClient, ILogger logger, Action<string> onDisconnect)
|
||||
{
|
||||
_peerManager = peerManager;
|
||||
@ -92,6 +36,62 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
_ = ReadEvents(_cts.Token);
|
||||
}
|
||||
|
||||
public static LDKTcpDescriptor? Inbound(PeerManager peerManager, TcpClient tcpClient, ILogger logger, ObservableConcurrentDictionary<string, LDKTcpDescriptor> descriptors)
|
||||
{
|
||||
var descriptor = new LDKTcpDescriptor(peerManager, tcpClient, logger,s => descriptors.TryRemove(s, out _));
|
||||
var result = peerManager.new_inbound_connection(descriptor.SocketDescriptor, tcpClient.Client.GetSocketAddress());
|
||||
if (result.is_ok())
|
||||
{
|
||||
logger.LogInformation("New inbound connection accepted");
|
||||
descriptor.Start();
|
||||
return descriptor;
|
||||
}
|
||||
if (result is Result_NonePeerHandleErrorZ.Result_NonePeerHandleErrorZ_Err)
|
||||
{
|
||||
logger.LogError("Failed to create inbound connection");
|
||||
tcpClient.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static LDKTcpDescriptor? Outbound(PeerManager peerManager, TcpClient tcpClient, ILogger logger,
|
||||
PubKey pubKey, ObservableConcurrentDictionary<string, LDKTcpDescriptor> descriptors)
|
||||
{
|
||||
var descriptor = new LDKTcpDescriptor(peerManager, tcpClient, logger, s => descriptors.TryRemove(s, out _));
|
||||
var saSocketAddress = tcpClient.Client.GetSocketAddress();
|
||||
if (saSocketAddress is Option_SocketAddressZ.Option_SocketAddressZ_None)
|
||||
{
|
||||
logger.LogWarning("Failed to get TCP client socket address, cannot create outbound connection to {PubKey}", pubKey);
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
var saStr = ((Option_SocketAddressZ.Option_SocketAddressZ_Some)saSocketAddress).some.to_str();
|
||||
logger.LogInformation("Connecting to {PubKey}@{Str}", pubKey, saStr);
|
||||
descriptor.Start();
|
||||
var result = peerManager.new_outbound_connection(pubKey.ToBytes(), descriptor.SocketDescriptor, saSocketAddress);
|
||||
if (result is Result_CVec_u8ZPeerHandleErrorZ.Result_CVec_u8ZPeerHandleErrorZ_OK ok)
|
||||
{
|
||||
descriptor.send_data(ok.res, true);
|
||||
}
|
||||
if (result.is_ok())
|
||||
{
|
||||
logger.LogInformation("Connection to {PubKey}@{Str} accepted", pubKey, saStr);
|
||||
return descriptor;
|
||||
}
|
||||
if (result is Result_CVec_u8ZPeerHandleErrorZ.Result_CVec_u8ZPeerHandleErrorZ_Err errResult)
|
||||
{
|
||||
logger.LogError("Connecting to {PubKey}@{Str} failed: {Error}", pubKey, saStr, errResult.err is { } peerError ? peerError.ToString() : errResult.ToString());
|
||||
tcpClient.Dispose();
|
||||
return null;
|
||||
}
|
||||
descriptor.disconnect_socket();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_tcs.TrySetResult();
|
||||
@ -116,23 +116,21 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
private async Task ReadEvents(CancellationToken cancellationToken)
|
||||
{
|
||||
await _tcs.Task.WaitAsync(cancellationToken);
|
||||
//max 4kib
|
||||
var bufSz = 4096;
|
||||
const int bufSz = 4096; // max 4kib
|
||||
var buffer = new byte[bufSz];
|
||||
while (_tcpClient.Connected && !_cts.IsCancellationRequested)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer,cancellationToken);
|
||||
|
||||
var read = await _stream.ReadAsync(buffer,cancellationToken);
|
||||
if (read == 0)
|
||||
{
|
||||
_logger.LogWarning("Read 0 bytes of data from peer");
|
||||
disconnect_socket();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var data = buffer[..read];
|
||||
_logger.LogTrace($"Read {read} bytes of data from peer" );
|
||||
switch ( _peerManager.read_event(SocketDescriptor, data) )
|
||||
_logger.LogTrace("Read {Read} bytes of data from peer", read);
|
||||
switch ( _peerManager.read_event(SocketDescriptor, data))
|
||||
{
|
||||
case Result_boolPeerHandleErrorZ.Result_boolPeerHandleErrorZ_OK ok:
|
||||
if (ok.res)
|
||||
@ -146,44 +144,23 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
disconnect_socket();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
_peerManager.process_events();
|
||||
}
|
||||
}
|
||||
|
||||
private void Resume()
|
||||
public long send_data(byte[] data, bool resumeRead)
|
||||
{
|
||||
try
|
||||
{
|
||||
_readSemaphore.Release();
|
||||
|
||||
_logger.LogInformation("resuming read");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public long send_data(byte[] data, bool resume_read)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("sending {Bytes} bytes of data to peer", data.Length);
|
||||
|
||||
_logger.LogTrace("Sending {Bytes} bytes of data to peer", data.Length);
|
||||
var result = _tcpClient.Client.Send(data);
|
||||
_logger.LogTrace("Sent {Bytes} bytes of data to peer", result);
|
||||
if (resume_read)
|
||||
{
|
||||
Resume();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e,"Failed to send data");
|
||||
_logger.LogError("Failed to send {Bytes} bytes of data to peer: {Error} - disconnecting socket", data.Length, e.Message);
|
||||
disconnect_socket();
|
||||
return 0;
|
||||
}
|
||||
@ -191,10 +168,7 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
|
||||
public void disconnect_socket()
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_cts.IsCancellationRequested) return;
|
||||
|
||||
_logger.LogInformation("Disconnecting socket");
|
||||
_cts.Cancel();
|
||||
@ -203,9 +177,9 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
_onDisconnect(Id);
|
||||
}
|
||||
|
||||
public bool eq(SocketDescriptor other_arg)
|
||||
public bool eq(SocketDescriptor otherArg)
|
||||
{
|
||||
return hash() == other_arg.hash();
|
||||
return hash() == otherArg.hash();
|
||||
}
|
||||
|
||||
public long hash()
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKWalletLogger : LDKLogger
|
||||
{
|
||||
public LDKWalletLogger(LDKWalletLoggerFactory ldkWalletLoggerFactory) : base(ldkWalletLoggerFactory)
|
||||
{
|
||||
}
|
||||
}
|
||||
public class LDKWalletLogger(LDKWalletLoggerFactory ldkWalletLoggerFactory) : LDKLogger(ldkWalletLoggerFactory);
|
||||
|
||||
@ -2,15 +2,8 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKWalletLoggerFactory : ILoggerFactory
|
||||
public class LDKWalletLoggerFactory(ILoggerFactory loggerFactory) : ILoggerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _inner;
|
||||
|
||||
public LDKWalletLoggerFactory(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_inner = loggerFactory;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//ignore as this is scoped
|
||||
@ -18,7 +11,7 @@ public class LDKWalletLoggerFactory : ILoggerFactory
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
_inner.AddProvider(provider);
|
||||
loggerFactory.AddProvider(provider);
|
||||
}
|
||||
|
||||
public List<string> Logs { get; } = new List<string>();
|
||||
@ -26,11 +19,11 @@ public class LDKWalletLoggerFactory : ILoggerFactory
|
||||
public ILogger CreateLogger(string category)
|
||||
{
|
||||
var categoryName = string.IsNullOrWhiteSpace(category) ? "LDK" : $"LDK.{category}";
|
||||
LoggerWrapper logger = new LoggerWrapper(_inner.CreateLogger(categoryName));
|
||||
LoggerWrapper logger = new LoggerWrapper(loggerFactory.CreateLogger(categoryName));
|
||||
|
||||
logger.LogEvent += (sender, message) =>
|
||||
Logs.Add(DateTime.Now.ToShortTimeString() + " " + categoryName + message);
|
||||
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,12 +15,17 @@ public class LDKWatchedOutput
|
||||
public uint256? BlockHash { get; set; }
|
||||
|
||||
[JsonConverter(typeof(BitcoinSerializableJsonConverterFactory))]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
public OutPoint? Outpoint { get; set; }
|
||||
|
||||
public LDKWatchedOutput()
|
||||
{
|
||||
}
|
||||
|
||||
public LDKWatchedOutput(Script script)
|
||||
{
|
||||
Script = script;
|
||||
}
|
||||
|
||||
public LDKWatchedOutput(WatchedOutput watchedOutput)
|
||||
{
|
||||
Script = Script.FromBytesUnsafe(watchedOutput.get_script_pubkey());
|
||||
@ -29,4 +34,4 @@ public class LDKWatchedOutput
|
||||
: null;
|
||||
Outpoint = watchedOutput.get_outpoint().Outpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,42 +2,51 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LightningAPIKeyManager
|
||||
public class LightningAPIKeyManager(ConfigProvider configProvider)
|
||||
{
|
||||
private const string LightningAPIKeyConfigKey = "LightningAPIKeys";
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
|
||||
public LightningAPIKeyManager(ConfigProvider configProvider)
|
||||
public async Task<APIKey> GetKeyForStore(string storeId, APIKeyPermission permission)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
public async Task<List<APIKey>> List()
|
||||
{
|
||||
var keys = await _configProvider.Get<List<APIKey>>(LightningAPIKeyConfigKey) ?? [];
|
||||
return keys;
|
||||
return await GetOrCreate($"BTCPay Store {storeId}", permission);
|
||||
}
|
||||
|
||||
public async Task Revoke(string key)
|
||||
{
|
||||
var keys = await List();
|
||||
if(keys.RemoveAll(k => k.Key == key)>0)
|
||||
await _configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
if (keys.RemoveAll(k => k.Key == key)>0)
|
||||
await configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
}
|
||||
|
||||
public async Task<APIKey> Create(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
var newKey = new APIKey(Guid.NewGuid().ToString(), name, permission);
|
||||
keys.Add(newKey);
|
||||
await _configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
return newKey;
|
||||
|
||||
}
|
||||
public async Task<bool> CheckPermission(string key, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
return keys.Any(k => k.Key == key && k.Permission >= permission);
|
||||
}
|
||||
|
||||
}
|
||||
private async Task<List<APIKey>> List()
|
||||
{
|
||||
var keys = await configProvider.Get<List<APIKey>>(LightningAPIKeyConfigKey) ?? [];
|
||||
return keys;
|
||||
}
|
||||
|
||||
private async Task<APIKey?> Get(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
return keys.FirstOrDefault(k => k.Name == name && k.Permission == permission);
|
||||
}
|
||||
|
||||
private async Task<APIKey> Create(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
var newKey = new APIKey(Guid.NewGuid().ToString(), name, permission);
|
||||
keys.Add(newKey);
|
||||
await configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
return newKey;
|
||||
}
|
||||
|
||||
private async Task<APIKey> GetOrCreate(string name, APIKeyPermission permission)
|
||||
{
|
||||
return await Get(name, permission) ?? await Create(name, permission);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,31 +2,24 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LoggerWrapper : ILogger
|
||||
public class LoggerWrapper(ILogger inner) : ILogger
|
||||
{
|
||||
private readonly ILogger _inner;
|
||||
|
||||
public LoggerWrapper(ILogger inner)
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
return _inner.BeginScope(state);
|
||||
return inner.BeginScope(state);
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return _inner.IsEnabled(logLevel);
|
||||
return inner.IsEnabled(logLevel);
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
_inner.Log(logLevel, eventId, state, exception, formatter);
|
||||
inner.Log(logLevel, eventId, state, exception, formatter);
|
||||
LogEvent?.Invoke(this, formatter(state, exception));
|
||||
}
|
||||
|
||||
public event EventHandler<string>? LogEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,10 +75,8 @@ public class PaymentsManager :
|
||||
public async Task<AppLightningPayment> RequestPayment(LightMoney amount, TimeSpan expiry, uint256 descriptionHash)
|
||||
{
|
||||
var amt = amount == LightMoney.Zero ? Option_u64Z.none() : Option_u64Z.some(amount.MilliSatoshi);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var epoch = now.ToUnixTimeSeconds();
|
||||
|
||||
var descHashBytes = Sha256.from_bytes(descriptionHash.ToBytes());
|
||||
var lsp = await _ldkNode.GetJITLSPService();
|
||||
|
||||
@ -101,33 +99,23 @@ public class PaymentsManager :
|
||||
lsp = null;
|
||||
}
|
||||
|
||||
var result = await Task.Run(() =>
|
||||
org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
|
||||
_channelManager, _nodeSigner, _logger,
|
||||
_network.GetLdkCurrency(), amt, descHashBytes, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
|
||||
Option_u16Z.none()));
|
||||
var result = await Task.Run(() => org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash(_channelManager, amt, descHashBytes, (int) Math.Ceiling(expiry.TotalSeconds), Option_u16Z.none()));
|
||||
if (result is Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_Err err)
|
||||
{
|
||||
throw new Exception(err.err.to_str());
|
||||
}
|
||||
|
||||
var originalInvoice =
|
||||
((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
|
||||
.res;
|
||||
|
||||
|
||||
var preimageResult =
|
||||
_channelManager.get_payment_preimage(originalInvoice.payment_hash(), originalInvoice.payment_secret());
|
||||
var originalInvoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result).res;
|
||||
var preimageResult = _channelManager.get_payment_preimage(originalInvoice.payment_hash(), originalInvoice.payment_secret());
|
||||
var preimage = preimageResult switch
|
||||
{
|
||||
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx => throw new Exception(
|
||||
errx.err.GetError()),
|
||||
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx => throw new Exception(errx.err.GetError()),
|
||||
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_OK ok => ok.res,
|
||||
_ => throw new Exception("Unknown error retrieving preimage")
|
||||
};
|
||||
|
||||
var parsedOriginalInvoice = BOLT11PaymentRequest.Parse(originalInvoice.to_str(), _network);
|
||||
var lp = new AppLightningPayment()
|
||||
var lp = new AppLightningPayment
|
||||
{
|
||||
Inbound = true,
|
||||
PaymentId = "default",
|
||||
@ -252,7 +240,7 @@ public class PaymentsManager :
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
outbound.Status = LightningPaymentStatus.Failed;
|
||||
@ -267,11 +255,11 @@ public class PaymentsManager :
|
||||
{
|
||||
if (lightningPayment.Inbound)
|
||||
{
|
||||
await CancelInbound(lightningPayment.PaymentHash);
|
||||
await CancelInbound(lightningPayment.PaymentHash!);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CancelOutbound(lightningPayment.PaymentId);
|
||||
await CancelOutbound(lightningPayment.PaymentId!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +356,7 @@ public class PaymentsManager :
|
||||
_channelManager.claim_funds(preimage);
|
||||
return;
|
||||
}
|
||||
if (accept.AdditionalData.TryGetValue(VoltageFlow2Jit.LightningPaymentLSPKey, out var lspDoc) &&
|
||||
if (accept.AdditionalData.TryGetValue(Flow2Jit.LightningPaymentLSPKey, out var lspDoc) &&
|
||||
lspDoc.Deserialize<string>() is { } lsp &&
|
||||
await _ldkNode.GetJITLSPService() is { } lspService && lspService.ProviderName == lsp &&
|
||||
await lspService.IsAcceptable(accept!, eventPaymentClaimable))
|
||||
@ -393,22 +381,20 @@ public class PaymentsManager :
|
||||
preimage is null ? null : Convert.ToHexString(preimage).ToLower());
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_PaymentFailed @eventPaymentFailed)
|
||||
public async Task Handle(Event.Event_PaymentFailed eventPaymentFailed)
|
||||
{
|
||||
await PaymentUpdate(new uint256(eventPaymentFailed.payment_hash), false,
|
||||
var paymentHash = uint256.Parse(Convert.ToHexString(((Option_ThirtyTwoBytesZ.Option_ThirtyTwoBytesZ_Some)eventPaymentFailed.payment_hash).some).ToLower());
|
||||
await PaymentUpdate(paymentHash, false,
|
||||
Convert.ToHexString(eventPaymentFailed.payment_id).ToLower(), true, null);
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_PaymentSent eventPaymentSent)
|
||||
{
|
||||
|
||||
var paymentHash = uint256.Parse(Convert.ToHexString(eventPaymentSent.payment_hash).ToLower());
|
||||
await PaymentUpdate(paymentHash, false,
|
||||
Convert.ToHexString(
|
||||
((Option_ThirtyTwoBytesZ.Option_ThirtyTwoBytesZ_Some) eventPaymentSent.payment_id).some).ToLower(),
|
||||
((Option_ThirtyTwoBytesZ.Option_ThirtyTwoBytesZ_Some)eventPaymentSent.payment_id).some).ToLower(),
|
||||
false,
|
||||
Convert.ToHexString(eventPaymentSent.payment_preimage).ToLower());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class PeersChangedEventArgs : EventArgs
|
||||
{
|
||||
public List<NodeInfo> PeerNodeIds { get; set; }
|
||||
|
||||
public PeersChangedEventArgs(List<NodeInfo> peerNodeIds)
|
||||
{
|
||||
PeerNodeIds = peerNodeIds;
|
||||
}
|
||||
}
|
||||
@ -14,34 +14,33 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
namespace BTCPayApp.Core.LSP.JIT;
|
||||
|
||||
/// <summary>
|
||||
/// https://docs.voltage.cloud/flow/flow-2.0
|
||||
/// https://www.voltage.cloud/blog/introducing-flow-v2
|
||||
/// https://www.voltage.cloud/blog/deprecating-flow-2-0---paving-the-way-for-a-superior-solution
|
||||
/// </summary>
|
||||
public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandler<Event.Event_ChannelPending>
|
||||
|
||||
public abstract class Flow2Jit : IJITService, IScopedHostedService, ILDKEventHandler<Event.Event_ChannelPending>
|
||||
{
|
||||
private const string LightningPaymentOriginalPaymentRequest = "OriginalPaymentRequest";
|
||||
private const string LightningPaymentJITFeeKey = "JITFeeKey";
|
||||
public const string LightningPaymentLSPKey = "LSP";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Network _network;
|
||||
private readonly LDKNode _node;
|
||||
private readonly ChannelManager _channelManager;
|
||||
private readonly ILogger<VoltageFlow2Jit> _logger;
|
||||
private readonly ILogger<Flow2Jit> _logger;
|
||||
private readonly LDKOpenChannelRequestEventHandler _openChannelRequestEventHandler;
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
private readonly ConcurrentDictionary<long, Event.Event_OpenChannelRequest> _acceptedChannels = new();
|
||||
public bool Active { get; }
|
||||
|
||||
public virtual string ProviderName => "Abstract Flow 2.0 Provider";
|
||||
protected virtual LightMoney NonChannelOpenFee => LightMoney.Zero;
|
||||
|
||||
public virtual Uri? BaseAddress(Network network)
|
||||
{
|
||||
return network switch
|
||||
{
|
||||
not null when network == Network.Main => new Uri("https://lsp.voltageapi.com"),
|
||||
not null when network == Network.TestNet => new Uri("https://testnet-lsp.voltageapi.com"),
|
||||
// not null when network == Network.RegTest => new Uri("https://localhost:5001/jit-lsp"),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
private FlowInfoResponse? _info;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LDKNode node,
|
||||
ChannelManager channelManager, ILogger<VoltageFlow2Jit> logger,
|
||||
protected Flow2Jit(IHttpClientFactory httpClientFactory, Network network, LDKNode node,
|
||||
ChannelManager channelManager, ILogger<Flow2Jit> logger,
|
||||
LDKOpenChannelRequestEventHandler openChannelRequestEventHandler)
|
||||
{
|
||||
var httpClientInstance = httpClientFactory.CreateClient("VoltageFlow2JIT");
|
||||
@ -56,14 +55,26 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
_openChannelRequestEventHandler = openChannelRequestEventHandler;
|
||||
}
|
||||
|
||||
public async Task<FlowInfoResponse> GetInfo(CancellationToken cancellationToken = default)
|
||||
protected virtual Uri? BaseAddress(Network network)
|
||||
{
|
||||
var path = "/api/v1/info";
|
||||
return network switch
|
||||
{
|
||||
not null when network == Network.Main => new Uri("https://lsp.voltageapi.com"),
|
||||
not null when network == Network.TestNet => new Uri("https://testnet-lsp.voltageapi.com"),
|
||||
// not null when network == Network.RegTest => new Uri("https://localhost:5001/jit-lsp"),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<FlowInfoResponse> GetInfo(CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string path = "/api/v1/info";
|
||||
var response = await _httpClient.GetAsync(path, cancellationToken);
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<FlowInfoResponse>(cancellationToken);
|
||||
var res = await response.Content.ReadFromJsonAsync<FlowInfoResponse>(cancellationToken);
|
||||
return res!;
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
@ -72,16 +83,17 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FlowFeeResponse> GetFee(LightMoney amount, PubKey pubkey,
|
||||
private async Task<FlowFeeResponse> GetFee(LightMoney amount, PubKey pubkey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = "/api/v1/fee";
|
||||
const string path = "/api/v1/fee";
|
||||
var request = new FlowFeeRequest(amount, pubkey);
|
||||
var response = await _httpClient.PostAsJsonAsync(path, request, cancellationToken);
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<FlowFeeResponse>(cancellationToken);
|
||||
var res = await response.Content.ReadFromJsonAsync<FlowFeeResponse>(cancellationToken);
|
||||
return res!;
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
@ -90,11 +102,11 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BOLT11PaymentRequest> GetProposal(BOLT11PaymentRequest bolt11PaymentRequest,
|
||||
private async Task<BOLT11PaymentRequest> GetProposal(BOLT11PaymentRequest bolt11PaymentRequest,
|
||||
EndPoint? endPoint = null, string? feeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = "/api/v1/proposal";
|
||||
var request = new FlowProposalRequest()
|
||||
const string path = "/api/v1/proposal";
|
||||
var request = new FlowProposalRequest
|
||||
{
|
||||
Bolt11 = bolt11PaymentRequest.ToString(),
|
||||
Host = endPoint?.Host(),
|
||||
@ -118,17 +130,15 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string ProviderName => "Voltage";
|
||||
|
||||
public async Task<JITFeeResponse?> CalculateInvoiceAmount(LightMoney expectedAmount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fee = await GetFee(expectedAmount, _node.NodeId, cancellationToken);
|
||||
var amtToGenerate = expectedAmount - fee.Amount;
|
||||
if(amtToGenerate.MilliSatoshi <= 0)
|
||||
return null;
|
||||
return new JITFeeResponse(expectedAmount, amtToGenerate, fee.Amount, fee.Id, ProviderName);
|
||||
return amtToGenerate.MilliSatoshi <= 0
|
||||
? null
|
||||
: new JITFeeResponse(expectedAmount, amtToGenerate, fee.Amount, fee.Id, ProviderName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -137,10 +147,6 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
}
|
||||
}
|
||||
|
||||
public const string LightningPaymentJITFeeKey = "JITFeeKey";
|
||||
public const string LightningPaymentLSPKey = "LSP";
|
||||
public const string LightningPaymentOriginalPaymentRequest = "OriginalPaymentRequest";
|
||||
|
||||
public async Task<bool> WrapInvoice(AppLightningPayment lightningPayment, JITFeeResponse? fee, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
@ -148,13 +154,11 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
if (lightningPayment.AdditionalData?.ContainsKey(LightningPaymentLSPKey) is true)
|
||||
return false;
|
||||
|
||||
|
||||
fee ??= await CalculateInvoiceAmount(new LightMoney(lightningPayment.Value), cancellationToken);
|
||||
|
||||
if (fee is null)
|
||||
return false;
|
||||
var invoice = lightningPayment.PaymentRequest;
|
||||
|
||||
var invoice = lightningPayment.PaymentRequest!;
|
||||
var proposal = await GetProposal(invoice, null, fee!.FeeIdentifier, cancellationToken);
|
||||
if (proposal.MinimumAmount != fee.AmountToRequestPayer || proposal.PaymentHash != invoice.PaymentHash)
|
||||
return false;
|
||||
@ -170,45 +174,30 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
{
|
||||
_logger.LogError(e, "Error while wrapping invoice");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> IsAcceptable(AppLightningPayment lightningPayment,
|
||||
public virtual Task<bool> IsAcceptable(AppLightningPayment lightningPayment,
|
||||
Event.Event_PaymentClaimable paymentClaimable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!lightningPayment.AdditionalData.TryGetValue(LightningPaymentLSPKey, out var lsp) ||
|
||||
lsp.GetString() != ProviderName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!lightningPayment.AdditionalData.TryGetValue(LightningPaymentLSPKey, out var lsp) || lsp.GetString() != ProviderName)
|
||||
return Task.FromResult(false);
|
||||
|
||||
if (!lightningPayment.AdditionalData.TryGetValue(LightningPaymentJITFeeKey, out var feeRaw) ||
|
||||
feeRaw.Deserialize<JITFeeResponse>() is not { } fee)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!lightningPayment.AdditionalData.TryGetValue(LightningPaymentJITFeeKey, out var feeRaw) || feeRaw.Deserialize<JITFeeResponse>() is not { } fee)
|
||||
return Task.FromResult(false);
|
||||
|
||||
if (_acceptedChannels.TryRemove(paymentClaimable.via_channel_id.hash(), out var channelRequest) &&
|
||||
paymentClaimable.counterparty_skimmed_fee_msat == fee.LSPFee.MilliSatoshi)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_acceptedChannels.TryRemove(paymentClaimable.via_channel_id.hash(), out _) && paymentClaimable.counterparty_skimmed_fee_msat == fee.LSPFee.MilliSatoshi)
|
||||
return Task.FromResult(true);
|
||||
|
||||
return paymentClaimable.counterparty_skimmed_fee_msat == NonChannelOpenFee.MilliSatoshi || paymentClaimable.amount_msat == (lightningPayment.Value - NonChannelOpenFee );
|
||||
return Task.FromResult(paymentClaimable.counterparty_skimmed_fee_msat == NonChannelOpenFee.MilliSatoshi || paymentClaimable.amount_msat == (lightningPayment.Value - NonChannelOpenFee ));
|
||||
}
|
||||
|
||||
protected virtual LightMoney NonChannelOpenFee => LightMoney.Zero;
|
||||
|
||||
public bool Active { get; }
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_node.ConfigUpdated += ConfigUpdated;
|
||||
_openChannelRequestEventHandler.AcceptedChannel += AcceptedChannel;
|
||||
_ = ConfigUpdated(this, await _node.GetConfig()).WithCancellation(_cts.Token);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (_cts.Token.IsCancellationRequested == false)
|
||||
@ -217,30 +206,17 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
await Task.Delay(10000, _cts.Token);
|
||||
}
|
||||
}, _cts.Token);
|
||||
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<long, Event.Event_OpenChannelRequest> _acceptedChannels = new();
|
||||
|
||||
private Task AcceptedChannel(object? sender, Event.Event_OpenChannelRequest e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_info?.PubKey) && new PubKey(_info.PubKey) == new PubKey(e.counterparty_node_id))
|
||||
{
|
||||
_acceptedChannels.TryAdd(e.temporary_channel_id.hash(), e);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
private FlowInfoResponse? _info;
|
||||
|
||||
public VoltageFlow2Jit(bool active)
|
||||
{
|
||||
Active = active;
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private async Task ConfigUpdated(object? sender, LightningConfig e)
|
||||
{
|
||||
try
|
||||
@ -250,13 +226,11 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
{
|
||||
_info = await GetInfo();
|
||||
|
||||
|
||||
var ni = _info.ToNodeInfo();
|
||||
var configPeers = await _node.GetConfig();
|
||||
var pubkey = new PubKey(_info.PubKey);
|
||||
if (configPeers.Peers.TryGetValue(_info.PubKey, out var peer))
|
||||
{
|
||||
//check if the endpoint matches any of the info ones
|
||||
//check if the endpoint matches any of the info ones
|
||||
if (!_info.ConnectionMethods.Any(a =>
|
||||
a.ToEndpoint().ToEndpointString().Equals(peer.Endpoint.ToEndpointString(), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
@ -297,7 +271,6 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
@ -307,7 +280,7 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
await _cts.CancelAsync();
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelPending @event)
|
||||
public Task Handle(Event.Event_ChannelPending @event)
|
||||
{
|
||||
var nodeId = new PubKey(@event.counterparty_node_id);
|
||||
if (nodeId.ToString() == _info?.PubKey)
|
||||
@ -316,11 +289,12 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
|
||||
.list_channels_with_counterparty(@event.counterparty_node_id)
|
||||
.FirstOrDefault(a => a.get_channel_id().eq(@event.channel_id));
|
||||
if (channel is null)
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
var channelConfig = channel.get_config();
|
||||
channelConfig.set_accept_underpaying_htlcs(true);
|
||||
_channelManager.update_channel_config(@event.counterparty_node_id, new[] {@event.channel_id},
|
||||
_channelManager.update_channel_config(@event.counterparty_node_id, [@event.channel_id],
|
||||
channelConfig);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,10 +6,12 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.LSP.JIT;
|
||||
|
||||
public class FlowInfoResponse
|
||||
public abstract class FlowInfoResponse
|
||||
{
|
||||
[JsonPropertyName("connection_methods")] public ConnectionMethod[] ConnectionMethods { get; set; }
|
||||
[JsonPropertyName("pubkey")] public required string PubKey { get; set; }
|
||||
[JsonPropertyName("connection_methods")]
|
||||
public ConnectionMethod[] ConnectionMethods { get; set; } = [];
|
||||
[JsonPropertyName("pubkey")]
|
||||
public required string PubKey { get; set; }
|
||||
|
||||
public NodeInfo[] ToNodeInfo()
|
||||
{
|
||||
@ -17,15 +19,18 @@ public class FlowInfoResponse
|
||||
return ConnectionMethods.Select(method => new NodeInfo(pubkey, method.Address, method.Port)).ToArray();
|
||||
}
|
||||
|
||||
public class ConnectionMethod
|
||||
public abstract class ConnectionMethod
|
||||
{
|
||||
[JsonPropertyName("address")] public string Address { get; set; }
|
||||
[JsonPropertyName("port")] public int Port { get; set; }
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
[JsonPropertyName("address")]
|
||||
public required string Address { get; set; }
|
||||
[JsonPropertyName("port")]
|
||||
public required int Port { get; set; }
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
public EndPoint? ToEndpoint()
|
||||
{
|
||||
return EndPointParser.TryParse($"{Address}:{Port}", 9735, out var endpoint) ? endpoint : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user