Compare commits

...

295 Commits

Author SHA1 Message Date
Pavlenex
395495fef5
Merge pull request #236 from TChukwuleta/bg/centre_charge_button 2025-07-18 19:35:28 +05:00
Chukwuleta Tobechi
aee8c5f032 Fix charge button position on Bitcoinize POS 2025-07-18 15:23:16 +01:00
Ghander
0785a1ab39
Update build-test.yml
change workflow trigger from pull_request to pull_request_target, which runs in the context of the base repository and can access secrets.
2025-07-01 16:57:04 -05:00
Ghander
ae06fd4404
Merge pull request #225 from TChukwuleta/ft/Include_archive
Include receipt and archive CTA
2025-06-30 11:50:51 -05:00
Ghander
e2d9b4e87c
clean before build-test.yml 2025-06-29 21:28:51 -05:00
Ghander
621adc1804 fixed ambiguous class references for invoice request 2025-06-28 21:49:25 -05:00
Pavlenex
831f1e2c5b
Merge pull request #220 from TChukwuleta/feat/include_supporters
Include supporters to the login and create account view
2025-06-27 11:34:43 +05:00
Pavlenex
9f56451b41
Merge pull request #227 from NicolasDorier/fix-double-charge
Fix: The keypad is charging double of what it should be
2025-06-27 10:50:04 +05:00
nicolas.dorier
427ba10f5e
Fix instructions in README.md 2025-06-26 18:24:36 +09:00
nicolas.dorier
409822d1a2
Fix: The keypad is charging double of what it should be 2025-06-26 17:53:42 +09:00
Chukwuleta Tobechi
1e84841af1 fixing sshd 2025-06-24 13:41:14 +01:00
Chukwuleta Tobechi
29136fe4c4 Include receipt and archive CTA 2025-06-23 19:18:23 +01:00
rockstardev
1b462e81da
Improving NFC scan processing 2025-06-19 11:04:55 +02:00
Chukwuleta Tobechi
e75d011eb4 Include supporters to the login and create account view 2025-06-19 10:04:31 +01:00
Ghander
ef534215f9
Merge pull request #219 from btcpayserver/fix-store-build
Fix: Unable to build on the plugin builder
2025-06-15 16:07:34 -05:00
Ghander
b147ddba3f Running the app in the background is causing the app to lock up on restart. Turning this off for now until we can find a long term fix. 2025-06-13 23:05:23 -05:00
Ghander
fdd3e0fc33 prevent multiple simultaneous nfc scans 2025-06-13 22:49:45 -05:00
Ghander
797e7a33c7 added invoice paid with successful nfc payment since this happens outside the iframe. 2025-06-12 19:47:07 -05:00
Ghander
f4f988b113 lnurl nfc attempt #3 2025-06-11 20:44:28 -05:00
nicolas.dorier
b9000dfc75
Fix: Unable to build on the plugin builder 2025-06-10 22:30:26 +09:00
Ghander
9d44aee152 format INfcInterface 2025-06-09 17:27:01 -05:00
Ghander
698afcd033 nfc iframe checkout attempt 3 2025-06-09 16:23:08 -05:00
Ghander
ac98a426c0 enable web-nfc for checkout iframe 2025-06-08 15:28:14 -05:00
Ghander
d232fbc68c initialize nfc plugin 2025-06-08 15:00:27 -05:00
Ghander
c9d6f5d9d7 NFC permissions enabled for compatible devices 2025-06-08 14:41:05 -05:00
Ghander
336daad4c6 Merge branch 'master' of https://github.com/btcpayserver/app 2025-06-06 09:07:06 -05:00
Ghander
4dbab4038c login user with pos qr code and set to cashier mode - issue #212 2025-06-06 08:55:27 -05:00
Andrew Camilleri (Kukks)
5b86d4f28f
Provide global settings that lock wallet gen + disbale auto generation of wallets 2025-06-06 12:37:13 +02:00
Andrew Camilleri (Kukks)
f64768a383
add a ps1 setup variant 2025-06-06 11:21:04 +02:00
Ghander
ccd160c4e0 Merge branch 'master' of https://github.com/btcpayserver/app 2025-05-30 15:06:39 -05:00
Dennis Reimann
5bd52b6f86
Update submodule 2025-05-30 13:35:56 +02:00
Dennis Reimann
c2e7a79bb8
Withdraw: Make Next button more prominent 2025-05-30 13:31:02 +02:00
Dennis Reimann
4a0bedd7b0
Fix missing exception and stack trace in error logs 2025-05-30 13:08:40 +02:00
Dennis Reimann
7f917a2a92
Refactor EmailService for use in desktop/server apps 2025-05-30 09:34:33 +02:00
Ghander
957da11d89 Merge branch 'master' of https://github.com/btcpayserver/app 2025-05-29 11:50:47 -05:00
d11n
b489cd0ec7
Show Withdraw button regardless of price 2025-05-29 08:17:00 +02:00
Dennis Reimann
f86b996303
Withdraw: Add review/confirmation for sending Lightning payments 2025-05-28 11:47:49 +02:00
Dennis Reimann
5f59fa7987
Add closing amount to offchain balance API 2025-05-27 15:51:13 +02:00
Dennis Reimann
48368cecbb
Fix version mismatch with server 2025-05-27 12:46:02 +02:00
Dennis Reimann
df637acb54
UI: Fix channel messages 2025-05-27 12:44:02 +02:00
Dennis Reimann
9b8890d04e
Update submodule 2025-05-27 12:43:35 +02:00
Ghander
b5ce32c67b Merge branch 'master' of https://github.com/btcpayserver/app 2025-05-26 08:50:09 -05:00
Dennis Reimann
ad870cae50
UI: Withdraw page updates
Context: #162
2025-05-26 14:18:39 +02:00
Dennis Reimann
612ef699b7
UI: Withdraw page updates
Context: #162
2025-05-23 18:01:31 +02:00
Dennis Reimann
186e995c41
UI: Extract recent transactions to separate page 2025-05-23 12:45:05 +02:00
Dennis Reimann
489d78bc40
Minor channel-related updates 2025-05-23 11:12:23 +02:00
Dennis Reimann
74ec312c31
Peer Handler: Improve handling of failed socket connections to persistent peers 2025-05-23 10:28:19 +02:00
Ghander
796a512b50 undo breaking changes 2025-05-22 16:14:52 -05:00
Dennis Reimann
fd3bbf68e5
Use vector icons for modern Android versions 2025-05-22 22:13:30 +02:00
Dennis Reimann
2061e93db3
Update submodule 2025-05-22 22:12:58 +02:00
Ghander
3834ef4c4f Merge branch 'master' of https://github.com/btcpayserver/app 2025-05-21 17:18:32 -05:00
Ghander
cdf6c78b0a reverted LDK Resume changes. 2025-05-21 17:18:13 -05:00
Ghander
b0d1e83bda fixed merge 2025-05-21 17:00:27 -05:00
Ghander
a678ad4d9e reverted the breaking changes. 2025-05-21 16:59:27 -05:00
Ghander
855046c161 created powershell script for windows to mimic the lightning setup shell script 2025-05-21 16:28:37 -05:00
Ghander
f55c394c0c fixed exception cuased by releasing a non-existant semaphore and various store page errors preventing build 2025-05-21 16:27:28 -05:00
Dennis Reimann
f5486a087b
Android: Add foreground service to keep hub connection alive 2025-05-21 11:47:12 +02:00
Dennis Reimann
0b04dd03d4
Improve app state logging 2025-05-21 11:28:40 +02:00
Dennis Reimann
85ca2d9658
BTCPayServer.Plugins.App v0.3.1.0 2025-05-20 12:19:13 +02:00
Dennis Reimann
72885feea7
VSS: Extend logging
Context: #208
2025-05-20 12:17:56 +02:00
Dennis Reimann
0aa42e93a8
Update submodule 2025-05-20 11:21:30 +02:00
Dennis Reimann
ef7faed0d8
UI: Show Withdraw link regardless of balance 2025-05-20 11:20:46 +02:00
Dennis Reimann
d63ef814ca
BTCPayServer.Plugins.App v0.3.0.0 2025-05-19 12:00:11 +02:00
Dennis Reimann
4868cf9b1f
Add login info endpoint
Redirect to password reset if password is missing. Closes #184.
2025-05-19 11:57:34 +02:00
Dennis Reimann
d076afbd85
Update submodule 2025-05-19 07:49:55 +02:00
Ghander
3e6db4d9e9 add sendEmail service to web app 2025-05-18 22:45:05 -05:00
Dennis Reimann
22f1da33f3
UI: Fix withdraw amount input field display and validation
Fixes #192.
2025-05-16 17:30:06 +02:00
Dennis Reimann
d9dc06eb01
UI: Form validation fixes 2025-05-16 17:29:06 +02:00
Dennis Reimann
f94a1aa262
Minor cleanups 2025-05-16 16:04:08 +02:00
Dennis Reimann
4e047acc8e
UI: Channels list improvements 2025-05-16 12:51:10 +02:00
Dennis Reimann
e4e68c1cf4
UI: Add device ID to debug section 2025-05-16 12:47:12 +02:00
Dennis Reimann
28af1bfecb
Update submodule 2025-05-15 13:55:19 +02:00
Dennis Reimann
4e54057447
UI: Add per-word feedback for verifying recovery phase
Closes #206.
2025-05-15 13:53:53 +02:00
Dennis Reimann
25eadbd822
UI: Redirect to dashboard after verifying recovery phrase
Closes #205.
2025-05-15 12:35:47 +02:00
Dennis Reimann
e87131338b
UI: Channels list improvements 2025-05-15 11:33:03 +02:00
Dennis Reimann
9ff0c0166b
Add labelling support for peers 2025-05-14 16:19:17 +02:00
Dennis Reimann
ea8344f537
Add exception handling to LN node accessor 2025-05-14 15:05:54 +02:00
Dennis Reimann
6698d81efa
Update submodule and adapt to store template feature 2025-05-14 14:14:20 +02:00
Dennis Reimann
426f50e1cd
Add non-truncated NodeID for testing 2025-05-14 11:55:46 +02:00
Dennis Reimann
42af818d85
Re-add LN connection string and LN store payment method button 2025-05-14 10:56:59 +02:00
Dennis Reimann
8babd11c1e
Channels list fixes 2025-05-13 14:10:41 +02:00
Dennis Reimann
244682f390
Re-add LNURL channel request feature 2025-05-13 12:15:54 +02:00
Ghander
dfeb073c3c added default request for REQUEST_IGNORE_BATTERY_OPTIMIZATIONS to see if we can get access to running in background. 2025-05-12 23:28:09 -05:00
Ghander
8dd2a33d16 added button to send logs via email. 2025-05-12 23:06:10 -05:00
Ghander
3b3d82f873 merged send email branch with master 2025-05-12 22:51:26 -05:00
Dennis Reimann
d24e971a09
UI: Improve channels list 2025-05-12 15:09:23 +02:00
Dennis Reimann
206b569fd4
UI: Don't show superfluous Top Items tile
Hides the tile if only keypad amounts were used. Closes #203.
2025-05-09 16:41:49 +02:00
Dennis Reimann
cad7b343e4
UI: Fix crash on settings page when toggling primary/secondary device 2025-05-09 16:28:09 +02:00
Dennis Reimann
18acd479f1
Minor channel list fix 2025-05-09 16:12:15 +02:00
Dennis Reimann
553948a38c
UI: Fix threading issue when adding a new peer
Fixes #197.
2025-05-09 16:07:14 +02:00
Dennis Reimann
375a320fb0
Cleanups 2025-05-09 09:14:45 +02:00
Dennis Reimann
e00b9b8d7b
UI: Fix onchain and lightning settings for additional/secondary devices 2025-05-08 17:39:17 +02:00
Dennis Reimann
fe33ec8769
UI: Remove QR code scan logs to not leak data 2025-05-08 15:44:23 +02:00
Dennis Reimann
df790001c0
MAUI: Set PublishTrimmed to false for Android release 2025-05-08 13:46:05 +02:00
Dennis Reimann
83b567d02c
Create pre-releases for master only 2025-05-08 11:33:02 +02:00
Dennis Reimann
712f2a5e5c Use MAUI compatible SQLite logging lib 2025-05-08 11:29:06 +02:00
Dennis Reimann
128ee5b744 Log to SQLite
Unfortunately this doesn't build for the MAUI project. See saleem-mirza/serilog-sinks-sqlite#41.
2025-05-08 11:29:06 +02:00
Dennis Reimann
ab0bb4156a
MAUI: Remove unnecessary settings from csproj 2025-05-08 11:27:10 +02:00
Ghander
c5446985f9 send logs in body of email 2025-05-06 22:48:59 -05:00
Dennis Reimann
ff1489fba7
UI: Signed out pages improvements 2025-05-06 10:30:50 +02:00
Dennis Reimann
7cf70000f3
UI: Fix store select redirect
Fixes #186.
2025-05-06 09:59:46 +02:00
Dennis Reimann
e0b07afc80
Syntax cleanup for LDK classes 2025-05-05 17:10:19 +02:00
Dennis Reimann
7f4e12dd2c
LN Node: Fix startup order to prevent trying to start without best block 2025-05-05 15:50:04 +02:00
Dennis Reimann
c7f2c4781a
Fix setting LSP to none
Fixes #190.
2025-05-05 14:47:07 +02:00
Dennis Reimann
903869d04a
Download logs
WIP for downloading the logs instead of copying them. This is needed as the logs output might be too large to be handled by the clipboard.

The current solution works in the browser, but does not work on Android as the file doesn't get saved to the download folder.

According to [this bug report](https://github.com/dotnet/maui/issues/15493), though the code is mostly adapted from the [official sample](https://learn.microsoft.com/en-us/aspnet/core/blazor/file-downloads?view=aspnetcore-8.0).
2025-05-05 12:03:02 +02:00
Dennis Reimann
3a8727ade2
Remove unused file 2025-05-05 11:57:52 +02:00
Dennis Reimann
71a9bf4cc5
AppState: Log node info error message 2025-05-05 09:40:14 +02:00
Dennis Reimann
1375719773
AppState: Fix use of deprecated ParseOutputDescriptor method 2025-05-05 09:39:52 +02:00
Dennis Reimann
90bd8f0f6f
Update submodule 2025-05-05 09:22:57 +02:00
Ghander
6bcc0547d6 close modal when exiting qr code scanner mode with back button. 2025-05-04 23:36:18 -05:00
Ghander
80076556e1 Merge branch 'master' of https://github.com/btcpayserver/app 2025-05-03 15:48:42 -05:00
Ghander
13ffd0371b updated app logs icon to nav-server-settings 2025-05-03 15:48:38 -05:00
Dennis Reimann
75c87e96ed Fix build and test 2025-05-02 10:19:09 +02:00
Dennis Reimann
cb6f10999a Optimize file logging 2025-05-02 10:19:09 +02:00
Dennis Reimann
63235396af Logging update 2025-05-02 10:19:09 +02:00
Dennis Reimann
7247bbc409 Add toggle to enable verbose logs
Closes #189.
2025-05-02 10:19:09 +02:00
Israel Ulelu
49d5ef1e47 impl: copy invoice id to clipboard on invoice detail page (https://github.com/btcpayserver/app/issues/53) 2025-05-02 10:18:51 +02:00
Ghander
ef9a3cd062 updated log icon in settings list 2025-05-01 23:25:29 -05:00
Ghander
c4aa16992c fix for scanning a QR problem #172 2025-04-30 23:25:06 -05:00
Dennis Reimann
46c4eead09
UI: Minor spacing improvement 2025-04-29 13:47:17 +02:00
Dennis Reimann
3de73adb19
Refactor Flow 2 JIT into an abstract class, clean up references 2025-04-29 13:46:33 +02:00
Dennis Reimann
ccd80fad4c
Try fixing MinAllowedAnchorChannelRemoteFee to 1sat/vB 2025-04-29 11:07:09 +02:00
Dennis Reimann
34f28284a0
Fix an exception in the LDK persist interface 2025-04-29 10:55:06 +02:00
Dennis Reimann
a3e9a36dbe
Refactor channel opening/closing 2025-04-29 10:42:58 +02:00
Dennis Reimann
9faf94b12c
Update submodule 2025-04-28 16:39:10 +02:00
Dennis Reimann
747e9bd1db
UI: Refactor channels list 2025-04-28 16:38:02 +02:00
Dennis Reimann
5c8453c3e3
More channels page improvements, fix connecting existing peer 2025-04-25 16:03:52 +02:00
Dennis Reimann
96cbcb5f88
Channels page improvements, fix adding new peer 2025-04-25 13:54:58 +02:00
Dennis Reimann
afe395c9ff
Update submodule 2025-04-25 07:55:36 +02:00
Dennis Reimann
4efb05744b
UI: QR scanner improvements 2025-04-24 17:02:26 +02:00
Dennis Reimann
386f9fa774
UI: Fix huge splash logo 2025-04-24 14:55:11 +02:00
Dennis Reimann
a4a5d9fbdc
UI: More potential CSS fixes 2025-04-24 14:54:59 +02:00
Dennis Reimann
e3412b1433
Potential CSS fixes for older webviews 2025-04-24 12:14:11 +02:00
Dennis Reimann
ea55664f93
README: Add note about Sunmi V2s device 2025-04-24 10:34:33 +02:00
Dennis Reimann
866532332e
LDK TCP Descriptor fix 2025-04-23 11:51:35 +02:00
Dennis Reimann
f93b676f40
Hide fees on settings page 2025-04-23 07:52:13 +02:00
Dennis Reimann
c3fc4fc82b
Adapt fee estiimates to what LDK Node uses
https://github.com/lightningdevkit/ldk-node/blob/main/src/fee_estimator.rs#L87
2025-04-23 07:51:42 +02:00
Dennis Reimann
2da390c091
Display current fees in debug section 2025-04-22 14:37:35 +02:00
Dennis Reimann
b76e9392a6
Adjust fee estimation values 2025-04-22 13:11:17 +02:00
Dennis Reimann
3c2295f744
Fix deserialization of ChannelMonitor 2025-04-22 10:49:16 +02:00
Dennis Reimann
ca50033d21
Update submodule and NBitcoin 2025-04-22 10:48:44 +02:00
Ghander
5faf6b21c5 update alllowed profile img size from 0.5 mb to 2mb 2025-04-19 16:04:58 -05:00
Dennis Reimann
4aef5c078c
Cleanups 2025-04-17 17:08:07 +02:00
Dennis Reimann
c6c1b49cd2
UI: TruncateCenter fix 2025-04-17 17:07:20 +02:00
Dennis Reimann
4c3860edf6
Support connecting endpoints specified via hostname
Closes #176.
2025-04-17 15:14:34 +02:00
Dennis Reimann
7adeb99e66
Remove Voltage Flow 2 LSP option
See https://www.voltage.cloud/blog/deprecating-flow-2-0---paving-the-way-for-a-superior-solution
2025-04-16 16:30:07 +02:00
Dennis Reimann
401fc3f1a4
BTCPayServer.Plugins.App v0.2.0.0 2025-04-16 10:35:21 +02:00
Dennis Reimann
c38ad051e4
Add script for building the plugin 2025-04-16 10:27:53 +02:00
Dennis Reimann
6d7d708e71
Update submodule 2025-04-16 10:26:03 +02:00
Dennis Reimann
f573916392
Update LDK
Closes #144.
2025-04-15 14:18:55 +02:00
Dennis Reimann
ffeb9fbea8
Upgrade dependencies
Fix build warnings
2025-04-14 11:14:02 +02:00
Dennis Reimann
01586fd857
Update submodule 2025-04-14 07:43:18 +02:00
Dennis Reimann
81daa88618
UI: Fix validation problem with role select 2025-04-09 13:39:10 +02:00
Dennis Reimann
ef093a480b
Update submodule 2025-04-09 09:59:37 +02:00
Dennis Reimann
ee8b8b991b
Hide user management buttons on server user page for currently active user 2025-04-08 13:10:25 +02:00
Dennis Reimann
11f621ad9e
Store-level invite for users 2025-04-08 13:05:34 +02:00
Ghander
0bedbc5cb2 profile img upload fix 2025-04-06 19:07:32 -05:00
Dennis Reimann
be8f7ad5cc
View unifications and improvements 2025-04-04 14:37:43 +02:00
Dennis Reimann
7db3b597ea
Add server user invite, improve users overview and details 2025-04-04 13:03:19 +02:00
Dennis Reimann
b03987cb87
Remove user from store; add modal for confirming actions 2025-04-01 15:41:47 +02:00
Dennis Reimann
f0584a032e
CTA at bottom improvements for pages without navigation bar 2025-04-01 11:56:42 +02:00
Dennis Reimann
44bda68311
Add server users overview 2025-03-31 19:39:29 +02:00
Dennis Reimann
5d20274f4c
Add store users overview and ability to change a user's role 2025-03-28 10:13:41 +01:00
Dennis Reimann
1d4797d1da
Clarify Store Settings navigation in app settings view
Closes #168.
2025-03-27 12:21:17 +01:00
Dennis Reimann
abc7713131
Update terminology
Related to #170.
2025-03-27 12:11:08 +01:00
Dennis Reimann
2fdf6394a3
Select store fix 2025-03-26 15:05:25 +01:00
Dennis Reimann
4caf6baedd
UI: Position primary CTA at the bottom
Closes #165.
2025-03-25 17:47:27 +01:00
Dennis Reimann
e29d57ab80
Reduce request count on store change 2025-03-25 11:08:03 +01:00
Dennis Reimann
85d89ceeb6
Update submodule 2025-03-25 11:07:29 +01:00
Dennis Reimann
1eb87753f3
Throw exception when not able to get best block 2025-03-24 16:09:27 +01:00
Dennis Reimann
e5ccf2f579
Update submodule 2025-03-24 16:09:04 +01:00
Dennis Reimann
f92294e4e7
Keep console logging output 2025-03-24 16:08:37 +01:00
Dennis Reimann
42d3e36eb9
Improve app logs page and add copy logs button 2025-03-21 15:01:46 +01:00
Dennis Reimann
52c6f0ed6d
Less verbose logging of hub connection errors 2025-03-21 14:57:09 +01:00
Dennis Reimann
b5810cb433
Logging unifications 2025-03-21 14:05:53 +01:00
Dennis Reimann
9c4836fbb6
Reestablish connection and refresh after returning from the background
Closes #161.
2025-03-21 10:40:08 +01:00
Ghander
a32866ba9c Merge branch 'master' of https://github.com/btcpayserver/app 2025-03-20 01:47:07 -07:00
Ghander
4e53925020 moved serilog packages into the core library 2025-03-20 01:46:59 -07:00
Dennis Reimann
e36d83d54e
Update submodule 2025-03-19 10:28:34 +01:00
Dennis Reimann
a5421a92e6
Add account on secondary device when scanning the pairing code
Enhances the encryption key with account info, similar to the login codes. Prepares the account when scnaning the pairing code on the secondary device. This way, only the passwords needs to be provided, afterwards everything is set up automatically.

Closes #150.
2025-03-19 10:27:45 +01:00
Ghander
a13156c1d3 merge latest with logging updates 2025-03-18 18:38:51 -07:00
Ghander
b5c6ae88f3 added v1 logs to settings 2025-03-18 18:37:31 -07:00
Dennis Reimann
921340f15a
Tiny improvements 2025-03-18 14:11:37 +01:00
Dennis Reimann
e1c60245a8
UI: Fix missing error state for already claimed invitation
Fixes #160.
2025-03-18 11:06:17 +01:00
Dennis Reimann
7623a9e6ce
Update submodule 2025-03-18 09:27:24 +01:00
Dennis Reimann
7e4871bda0
UI: Make store default currency the primary displayed currency
Closes #155.
2025-03-14 13:12:49 +01:00
Dennis Reimann
0e974b4848
Update submodule 2025-03-13 16:09:18 +01:00
Dennis Reimann
99d5b33756
New code from various tries to fix the file upload issue on device
Context: https://github.com/btcpayserver/app/issues/156#issuecomment-2721580131
2025-03-13 16:07:32 +01:00
Dennis Reimann
4ad3a9b333
Minor fixes 2025-03-13 10:26:01 +01:00
Dennis Reimann
1625f8817e
Try updating dev release process 2025-03-12 18:40:58 +01:00
Dennis Reimann
1e081c4f0a
UI: Improve menu bar responsiveness
Fixes #157.
2025-03-12 12:43:05 +01:00
Dennis Reimann
ab79da7ba4
UI: Auto-prefix URLs with https:// on signed out pages
Closes #163.
2025-03-12 12:35:25 +01:00
Dennis Reimann
730e355b0f
Docs: Add GrapheneOS development hint in troubleshooting section
Now I can finally run the app in dev mode with debugger attached. Closes #145.
2025-03-12 09:15:24 +01:00
Dennis Reimann
8cc75c2941
Try adding pre-release 2025-03-11 17:43:23 +01:00
Dennis Reimann
305acc2488
Try setting up a debug release 2025-03-11 17:23:11 +01:00
Dennis Reimann
f869b6cb26
Chore: Reformat Android manifest 2025-03-11 16:02:13 +01:00
Dennis Reimann
ebb64e05c6
UI: Fix viewport resizing with keyboard
Context:
- https://learn.microsoft.com/en-us/dotnet/maui/android/platform-specifics/soft-keyboard-input-mode
- https://developer.chrome.com/blog/viewport-resize-behavior/

Fixes #159.
2025-03-11 16:01:21 +01:00
Dennis Reimann
336bafd0f5
Update submodule 2025-03-11 13:05:09 +01:00
Dennis Reimann
5314b03cf6
Add instance debug info 2025-03-10 19:34:42 +01:00
Dennis Reimann
19812fc880
Improve account information updating
Context: #156
2025-03-10 19:34:26 +01:00
Dennis Reimann
13c7a2aed4
Fix build warnings 2025-03-10 16:34:19 +01:00
Dennis Reimann
de70c59a26
Update submodule 2025-03-10 16:00:58 +01:00
Dennis Reimann
f85f361300
Rework build options to optimize release build 2025-03-10 14:54:12 +01:00
Dennis Reimann
8bc4c07edf
Sync: Handle invalid encryption key case 2025-03-07 09:45:50 +01:00
Dennis Reimann
8012a810f1
Connection Manager: Stop syncing on logout 2025-03-06 16:39:43 +01:00
Dennis Reimann
3c4ce6c026
Try cleaning up csproj and build instructions 2025-03-06 12:01:05 +01:00
Dennis Reimann
7ff8b56cee
Update sln and MAUI csproj files
Includes updates by @Ghander from #153.
2025-03-06 11:11:25 +01:00
Dennis Reimann
11981fde2e
Use config provider for device identifier
Reverts the switch to secure config provider made in b096d83a5e.
2025-03-06 11:10:36 +01:00
Dennis Reimann
3f96a1717e
Minor fixups 2025-03-05 16:25:59 +01:00
Dennis Reimann
436d398bd4
UI: Update peers list 2025-03-04 12:24:48 +01:00
Dennis Reimann
cd8c95ba8c
Adapt to checkout iframe changes 2025-03-03 14:30:39 +01:00
Dennis Reimann
f73d964d6f
Update submodule 2025-03-03 14:30:25 +01:00
Dennis Reimann
9bf84303fb
Dashboard: Add seed recovery hint 2025-02-26 13:28:11 +01:00
Dennis Reimann
151f7059d8
Fix QR scanner call 2025-02-26 13:14:11 +01:00
Dennis Reimann
9de6dd42fa
Pairing: Improve UI and UX 2025-02-26 10:42:47 +01:00
Dennis Reimann
0f0fe8d064
Update submodule 2025-02-25 15:37:47 +01:00
Dennis Reimann
c4d2ce57c6
Dashboard: Make withdraw button more prominent 2025-02-25 13:26:28 +01:00
Dennis Reimann
69fedc0767
UI: Integrate passcode page 2025-02-20 15:02:44 +01:00
Dennis Reimann
a9da978061
Clean up project files and setup script 2025-02-20 11:59:08 +01:00
Dennis Reimann
d29bef6ce6
MAUI: Try fixing CSS reference 2025-02-19 16:08:28 +01:00
Dennis Reimann
9bb42417b8
Add keystore for MAUI build 2025-02-19 15:35:37 +01:00
Dennis Reimann
39895f780b
Update submodule 2025-02-19 13:16:17 +01:00
Dennis Reimann
0b7c25aa54
Allow HTTPS connection to server in local network 2025-02-19 11:21:07 +01:00
Dennis Reimann
96dd48f6ba
Ignore files in tmp directories 2025-02-18 17:14:05 +01:00
Dennis Reimann
66befda61f
UI: Improve seed confirmation page 2025-02-18 16:38:46 +01:00
Dennis Reimann
3c413fb2c9
UI: Remove unused top navbar 2025-02-18 12:00:29 +01:00
Dennis Reimann
c54c3e3aea
Refactoring: Switch mode instead of users 2025-02-18 11:25:19 +01:00
Dennis Reimann
82746713d0
Update submodule 2025-02-17 17:10:58 +01:00
Dennis Reimann
f1a149ffa3
Seed confirmation page fix 2025-02-17 17:10:10 +01:00
Dennis Reimann
f9e7e82e50
NavBar: Fix missing Checkout active state 2025-02-17 17:09:18 +01:00
Dennis Reimann
dd1ce2cc94
UI fixes 2025-02-14 16:50:40 +01:00
Dennis Reimann
113eecae61
Wallet: Implement recovery phrase confirmation page 2025-02-13 13:42:52 +01:00
Dennis Reimann
c1dce9f922
Wallet: Implement recovery phrase page 2025-02-12 12:40:14 +01:00
Dennis Reimann
02e5f960a6
Wallet: Add basic recovery phrase page 2025-02-11 19:21:17 +01:00
Dennis Reimann
9e6aa4d7e0
Wallet: Update settings page 2025-02-11 17:51:54 +01:00
Dennis Reimann
00d221d411
Improve URL handling on signed out pages 2025-02-11 11:19:47 +01:00
Dennis Reimann
604455cdd5
Invitation: Unset user invite code after successful invitation
Fixes #147.
2025-02-11 10:42:46 +01:00
Dennis Reimann
647662f12a
Update submodule 2025-02-11 07:46:50 +01:00
Dennis Reimann
2aa4c2473b
Update Android build config 2025-02-10 10:28:14 +01:00
Dennis Reimann
c75eca0c21
MAUI: Set PublishTrimmed to false 2025-02-07 12:46:15 +01:00
Dennis Reimann
8dca3455a8
Minor routes changes 2025-02-06 12:35:26 +01:00
Dennis Reimann
582d14aaca
Encryption key updates 2025-02-06 12:35:16 +01:00
Dennis Reimann
b096d83a5e
Store device id with secure config provider instead of in db 2025-02-05 16:31:36 +01:00
Dennis Reimann
d53616224d
Reset Android TargetFramework 2025-02-04 16:22:30 +01:00
Dennis Reimann
72fcfad8d6
Fix for connecting to local instance in debug mode 2025-02-04 15:41:41 +01:00
Dennis Reimann
19d7bb8736
Updates for Android build
Fixes #143.
2025-02-04 15:40:30 +01:00
Dennis Reimann
2304f76f6c
Test updates 2025-02-03 17:56:58 +01:00
Dennis Reimann
71404327c1
Add fiat currency to display toggle 2025-01-31 18:24:53 +01:00
Dennis Reimann
68137f96f1
MAUI updates 2025-01-28 20:31:58 +01:00
Dennis Reimann
c1400db8a9
Switch user preparations 2025-01-27 17:56:03 +01:00
Dennis Reimann
e75fd6b78f
Update submodule 2025-01-27 09:56:42 +01:00
Dennis Reimann
d30d6bd4f9
Remove sensitive info from logs 2025-01-26 14:30:37 +01:00
Dennis Reimann
29bc2a4924
Passcode preparations 2025-01-26 11:01:22 +01:00
Dennis Reimann
cbceadbe6c
More account refactoring 2025-01-25 17:35:01 +01:00
Dennis Reimann
515a765232
Account refactoring
Closes #111.
2025-01-24 17:22:26 +01:00
Dennis Reimann
b46bbaec50
Minor changes 2025-01-24 14:01:19 +01:00
Dennis Reimann
cceb71956a
Account page uses state instead of user info 2025-01-24 07:42:23 +01:00
Dennis Reimann
a0ddb8d2b2
Fix setup script 2025-01-24 07:05:03 +01:00
Dennis Reimann
5ff026d263
Minor refactorings 2025-01-23 19:02:35 +01:00
Dennis Reimann
697466f317
Fix build warnings 2025-01-22 18:56:08 +01:00
Dennis Reimann
fe084a41fd
Fix build warnings 2025-01-22 18:06:04 +01:00
Dennis Reimann
b8ddba72b1
Use setup script for tests 2025-01-22 12:48:19 +01:00
Dennis Reimann
c38e944bcf
Update submodule to use master
Removes the need for mobile-working-branch. Closes btcpayserver/btcpayserver#5968.
2025-01-22 12:26:35 +01:00
Dennis Reimann
57355d4e6b
Fix build warnings 2025-01-22 05:40:42 +01:00
Dennis Reimann
7554573605
Update submodule 2025-01-21 15:58:44 +01:00
Dennis Reimann
05de7d871b
Fix build warnings 2025-01-20 18:22:46 +01:00
Dennis Reimann
4287d3b9a3
Update submodule 2025-01-20 13:09:51 +01:00
Dennis Reimann
87f9de6d49
Fix build warnings 2025-01-20 12:47:33 +01:00
Dennis Reimann
495fa9175f
Update VSS controller and fix test 2025-01-19 09:46:12 +01:00
Dennis Reimann
3b493ab98e
Tests: Include plugin when starting the server 2025-01-17 15:51:37 +01:00
Dennis Reimann
172b3e72a1
Log connection error 2025-01-16 17:24:44 +01:00
Dennis Reimann
b914cc0d8d
Fix LN node generating 2025-01-16 14:48:35 +01:00
Dennis Reimann
8de8c62027
Move client models to app core 2025-01-16 13:35:11 +01:00
Dennis Reimann
9603cf9d11
Settings: Improve debug section 2025-01-16 11:58:38 +01:00
Dennis Reimann
94af2bf529
Integrate kukks' tracking branch 2025-01-15 19:32:42 +01:00
Dennis Reimann
fdd84c9f17
Updates for plugin 2025-01-15 18:08:12 +01:00
Andrew Camilleri
933e74649b
* Make the Server run config build the app plugin before running
* Make the app plugin publish all dlls (maybe change this onlt for Debug configuarion publish)
* Make migration runner use Startup task and add logging
* regenerate migration
2025-01-15 11:38:29 +01:00
Dennis Reimann
83e4434f7b
Move app files out into plugin structure 2025-01-14 21:25:11 +01:00
Dennis Reimann
a3e49c9c2b
Update submodule 2025-01-13 18:17:55 +01:00
Dennis Reimann
8d0880ac85
Minor updates 2025-01-13 18:17:21 +01:00
Dennis Reimann
cac99f6e87
Fix more build warnings 2025-01-09 15:20:53 +01:00
Dennis Reimann
550db94272
Consolidate migrations 2025-01-09 15:03:03 +01:00
Dennis Reimann
277386d333
MAUI: Fix build warnings 2025-01-09 14:34:17 +01:00
Dennis Reimann
6b03fdb4bc
Update submodule 2025-01-09 14:33:49 +01:00
Dennis Reimann
2e9f628f9c
Try to progress with fixing the build 2025-01-08 22:02:50 +01:00
Dennis Reimann
58334b1b37
Additional cleanups 2025-01-08 21:20:41 +01:00
Dennis Reimann
ef76b76876
Refactoring: Use API key instead of bearer token 2025-01-08 14:53:01 +01:00
Dennis Reimann
f796edf061
Remove leftovers on Dashboard page 2024-12-28 09:56:40 +01:00
Dennis Reimann
5fc526a999
UI: Review Send improvements
Closes #128.
2024-12-28 09:56:21 +01:00
Dennis Reimann
42e6cbebef
Remove Recent Invoices from the Dashboard view
Closes #134.
2024-12-23 11:25:44 +01:00
Dennis Reimann
ce7e7f46b3
Update submodule 2024-12-23 11:17:10 +01:00
Dennis Reimann
3780883fa7
Update submodule 2024-12-23 09:52:52 +01:00
Dennis Reimann
b30f8b1bb8
Rename Send to Withdraw in all instances
Closes #136.
2024-12-23 08:19:11 +01:00
Dennis Reimann
454e556e56
Switch Checkout and Dashboard order on the bottom tab bar
Closes #132.
2024-12-23 08:18:23 +01:00
Dennis Reimann
4d7d14571f
Update submodule 2024-12-21 12:30:25 +01:00
316 changed files with 13990 additions and 8213 deletions

View File

@ -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
View File

@ -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/

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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.

View File

@ -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; }
}

View File

@ -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; }
}

View 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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; }
}

View File

@ -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)
{

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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();
}
}
}

View File

@ -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()))
}
};
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View 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; } = [];
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View 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";
}

View File

@ -3,4 +3,5 @@
public interface IDataDirectoryProvider
{
Task<string> GetAppDataDirectory();
}
Task<string> GetCacheDirectory();
}

View File

@ -0,0 +1,7 @@
namespace BTCPayApp.Core.Contracts
{
public interface IEmailService
{
Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null);
}
}

View 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; }
}

View File

@ -1,8 +1,7 @@
namespace BTCPayApp.Core.Contracts;
public interface ISecureConfigProvider
{
Task<T?> Get<T>(string key);
Task Set<T>(string key, T? value);
}
}

View File

@ -1,5 +1,8 @@
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.Contracts;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SetupState
{
Undetermined,

View File

@ -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,

View File

@ -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 { }
}
}
}

View File

@ -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; }
}

View File

@ -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; }
}
}

View 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; }
}

View 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; }
}

View 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();
}
}

View File

@ -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; }
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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";
}

View File

@ -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();
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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(':', '/');

View File

@ -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
}
});
}
}
}

View File

@ -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))
{

View File

@ -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;

View File

@ -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();
}

View File

@ -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());
}
}
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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}";
}
}
}

View File

@ -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,
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
// // }
// }
// }

View File

@ -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()

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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