Compare commits

...

615 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
Kukks
28fb83c8da
wip 2024-12-17 12:56:12 +01:00
Dennis Reimann
f46e0752d1
Settings: Minor UI update for debug section 2024-12-16 17:25:25 +01:00
Dennis Reimann
e10d8b6157
Update user switch 2024-12-15 10:17:30 +01:00
Dennis Reimann
d5605021d4
Move User Selector from Home tab to Settings tab
Closes #55.
2024-12-14 14:36:02 +01:00
Dennis Reimann
e1b34432a0
Update submodule 2024-12-13 18:39:26 +01:00
Dennis Reimann
44170d0e0f
Send: Lightning updates 2024-12-11 17:08:41 +01:00
Dennis Reimann
8162f0a337
Update submodule and event names 2024-12-11 12:34:27 +01:00
Dennis Reimann
188b995caf
UI: React to onchain snapshot update 2024-12-10 18:46:47 +01:00
Dennis Reimann
5fb4b6efef
Improve event handling 2024-12-10 14:43:20 +01:00
Dennis Reimann
49128f09e1
Update submodule 2024-12-10 14:43:18 +01:00
Dennis Reimann
8d46bde0a7
UI: Unify spacing 2024-12-10 14:43:16 +01:00
Kukks
61254044a3
skip verif for now 2024-12-10 14:21:49 +01:00
Kukks
d73a3e0fd0
dmg 2024-12-10 12:56:24 +01:00
Kukks
a9a8e6d83f
dmg my life more like it 2024-12-10 12:53:26 +01:00
Kukks
53eb9c2e29
try dmg 2024-12-10 12:51:58 +01:00
Kukks
976ad3c8b3
add a p12 pass 2024-12-10 12:26:32 +01:00
Kukks
c635520200
test ios 2024-12-10 11:45:40 +01:00
Kukks
3142f3396b
add log on path for desktop app 2024-12-10 08:05:56 +01:00
Dennis Reimann
639e3ad66c
UI improvements 2024-12-09 18:41:44 +01:00
Dennis Reimann
6316a1e92c
Update submodule 2024-12-09 08:21:01 +01:00
Dennis Reimann
b6e7777c40
Send and Review: Use back button for cancelling transaction 2024-12-08 13:43:58 +01:00
Dennis Reimann
1b3ca22d6e
Send view updates
Closes #127.
2024-12-06 11:52:43 +01:00
Dennis Reimann
c6b0feb9a2
Lightning Node page update 2024-12-06 10:46:30 +01:00
Dennis Reimann
c411e75a3e
Send view updates 2024-12-05 18:02:02 +01:00
Dennis Reimann
ff824a7d3b
Reorganize pages 2024-12-05 14:54:39 +01:00
Dennis Reimann
c942fe35d8
Move wallets and settings pages
Closes #125.
2024-12-05 11:03:15 +01:00
Dennis Reimann
0a8549bc19
Rename keypad to checkout
Closes #124.
2024-12-05 10:56:20 +01:00
Dennis Reimann
42d0b66ce9
Send: Add transactions list and onchain verify dialog 2024-12-04 18:44:30 +01:00
Dennis Reimann
057ef9889d
Update BTCPay Server 2024-12-04 18:43:02 +01:00
Dennis Reimann
07c9da242d
Add Lightning send 2024-12-03 15:36:17 +01:00
Dennis Reimann
9b1e328ca9
Move LN pages 2024-12-03 15:35:46 +01:00
Dennis Reimann
b9be473bd4
Onchain Wallet Manager: Ensure reported network is defined 2024-12-03 11:11:09 +01:00
Dennis Reimann
bff9ae800f
LNPeerHandler: Fix connecting to unknown peers 2024-12-03 11:03:40 +01:00
Dennis Reimann
8fb53df259
Onchain Wallet Manager: Ensure HubProxy exists and is connected
Fixes #114.
2024-12-03 10:51:17 +01:00
Dennis Reimann
d0aac01f74
Basic onchain send from app's wallet 2024-12-02 20:51:49 +01:00
Dennis Reimann
b7d1d61900
Send page improvements 2024-12-02 19:27:08 +01:00
Dennis Reimann
e02f31e5fe
Encryption key page improvements 2024-12-02 17:04:03 +01:00
Dennis Reimann
fa1f5b3997
Minor settings improvements 2024-12-02 17:03:50 +01:00
Dennis Reimann
2d871ce982
Update submodule 2024-12-02 17:03:27 +01:00
Dennis Reimann
4bd66e783a
Use retail mode by default for new stores created via the app
Closes #109.
2024-12-02 12:04:14 +01:00
Dennis Reimann
34f0cb3a6e
Update submodule 2024-12-02 12:03:24 +01:00
Dennis Reimann
74d9554dc1
Add roles and users to store state 2024-12-01 18:24:58 +01:00
Dennis Reimann
6b8547fe04
Remove unused files 2024-12-01 18:06:46 +01:00
Dennis Reimann
2a6126bdef
Update submodule 2024-12-01 18:06:17 +01:00
Kukks
386dd80925
Support lnurl channel requests 2024-11-29 10:39:22 +01:00
Kukks
d1405d6d88
make endpoint the right type 2024-11-29 10:38:57 +01:00
Kukks
6753e5507f
fix utxo fetching 2024-11-29 08:31:02 +01:00
Kukks
a57b451386
update subm 2024-11-28 16:06:08 +01:00
Kukks
3f4f2e0a57
bump app versions 2024-11-28 16:06:08 +01:00
Dennis Reimann
fecd4c59c6
Remove logout from account switch 2024-11-28 15:29:42 +01:00
Dennis Reimann
c7f1c060d6
Logout disclaimer 2024-11-28 15:29:30 +01:00
Dennis Reimann
03b4640c22
Signed out base page improvement 2024-11-28 14:58:15 +01:00
Kukks
6b189bcec3
fix naming of artifact 2024-11-28 14:35:40 +01:00
Kukks
75d7a9bd97
try mac build 2024-11-28 14:32:24 +01:00
Kukks
9dff2cf230
consolidate gh actions 2024-11-28 14:27:41 +01:00
Kukks
15d4d1d228
runtime identifier not fx 2024-11-28 14:24:17 +01:00
Kukks
d835c7cc75
try build again 2024-11-28 13:32:40 +01:00
Kukks
1f23480b60
build 2024-11-28 12:54:15 +01:00
Dennis Reimann
091e383a07
Add success message for notification settings update 2024-11-28 12:28:05 +01:00
Kukks
d9ed601377
build desktop 2024-11-28 11:34:04 +01:00
Dennis Reimann
1182ed7e40
Wallet page updates
Potentially fixes #114.
2024-11-28 11:16:54 +01:00
Kukks
c7b567c9f0
remove ios build until we have an apple dev account 2024-11-27 14:43:31 +01:00
Kukks
ba06a6f5db
potentially fix #115 2024-11-27 14:38:36 +01:00
Kukks
5c6265e012
fix ios 2024-11-27 11:14:47 +01:00
Kukks
4257d85bd9
try fix photino and ios build 2024-11-27 10:39:31 +01:00
Kukks
d2c445a864
maui build 2024-11-26 12:11:50 +01:00
Kukks
f90f64185e
init code docs 2024-11-26 11:47:39 +01:00
Kukks
02fd84efca
better handling of utxo response 2024-11-26 11:47:39 +01:00
Kukks
409eddf608
maui small cleanup
# Conflicts:
#	submodules/btcpayserver
2024-11-26 11:47:38 +01:00
Kukks
92274d1989
update json converters 2024-11-26 11:47:08 +01:00
Dennis Reimann
48108e7a35
Update BTCPay Server 2024-11-26 07:58:36 +01:00
Dennis Reimann
27dc922beb
Refactoring: Remove CommonServer package 2024-11-20 17:25:18 +01:00
Dennis Reimann
b8072c81df
Fix success message display 2024-11-19 17:23:22 +01:00
Dennis Reimann
fd82c59dd7
Remove custom signup models 2024-11-19 17:23:07 +01:00
Dennis Reimann
567f2e967a
Upgrade packages 2024-11-19 17:22:41 +01:00
Dennis Reimann
5a84e7802c
Improve invoice page 2024-11-18 22:10:04 +01:00
Dennis Reimann
0c651cb687
Use Blazor keypad 2024-11-18 22:06:14 +01:00
Dennis Reimann
951636a25e
Chart updates 2024-11-18 21:51:52 +01:00
Kukks
7d96c69d4f
try switch back to ubuntu 2024-11-15 09:24:03 +01:00
Dennis Reimann
9f244cbb2f
Update BTCPay Server 2024-11-14 15:40:28 +01:00
Dennis Reimann
12120f625b
Update build steps and start infrastructure for MAUI 2024-11-13 17:00:20 +01:00
Dennis Reimann
61e21abcec
Fix package downgrade error 2024-11-13 14:27:56 +01:00
Dennis Reimann
357d700d19
Update icons and submodules 2024-11-13 14:04:12 +01:00
Dennis Reimann
2acc63f3f1
Keypad updates 2024-11-12 13:32:05 +01:00
Dennis Reimann
2818207355
App Item updates 2024-11-12 12:32:21 +01:00
Dennis Reimann
64b46e45f0
Update submodule 2024-11-12 12:32:06 +01:00
Dennis Reimann
b05b54d1ee
More warning fixes 2024-11-07 16:04:17 +01:00
Dennis Reimann
f87076eec2
Update submodule 2024-11-07 15:42:13 +01:00
Dennis Reimann
03a60b22f9
More warning fixes 2024-11-07 15:02:18 +01:00
Dennis Reimann
6e0625c9e5
Fix build warnings 2024-11-07 14:08:09 +01:00
Dennis Reimann
0e5a23d160
Update submodule 2024-11-07 13:16:51 +01:00
Dennis Reimann
e862992c09
Fix tests and build warnings 2024-11-07 13:01:19 +01:00
Dennis Reimann
1143f28eba
Reference tests package 2024-11-07 12:40:55 +01:00
d11n
ac56558028
Add time selection to balance graph (#102)
Prerequisite btcpayserver/btcpayserver#6217. Closes #99.
2024-11-07 10:41:21 +01:00
Dennis Reimann
7cf2f85ad5
Fix reference 2024-11-07 10:40:32 +01:00
Dennis Reimann
66ca7ed6d3
Update BTCPay Server 2024-11-07 10:20:38 +01:00
Kukks
4ff0901e7e
fix desktop 2024-11-07 10:03:39 +01:00
Kukks
ad49ba855a
move vss out 2024-11-07 09:41:42 +01:00
Dennis Reimann
bde26040ce
Update mobile-working-branch 2024-11-06 11:14:13 +01:00
Dennis Reimann
3d3933b464
Update BTCPay Server 2024-11-06 10:34:32 +01:00
Kukks
5587440432
update and refactor to remove race conditions. Add API keys to payment method for app ln
# Conflicts:
#	submodules/btcpayserver
2024-11-04 13:05:08 +01:00
Dennis Reimann
3eef655ac5
Remove duplicate Lightning Address nav item 2024-10-29 14:12:09 +01:00
Dennis Reimann
595ca7c646
Update BTCPay Server 2024-10-29 10:57:37 +01:00
Vincenzo Palazzo
62b5b850df docs: add Troubleshooting section
Fixes https://github.com/btcpayserver/app/issues/105
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
2024-10-22 21:13:22 +02:00
d11n
0f848cf6b1
Merge pull request #104 from vincenzopalazzo/macros/docs
docs: fix the sequence inside the readme
2024-10-22 21:12:35 +02:00
Vincenzo Palazzo
c3c91a9c58 docs: fix the sequence inside the readme
Fixing the following error while running the project for the first time

```
[vincent@vincenzopalazzo-arch-dev app]$ dotnet workload restore
Unhandled exception: Microsoft.Build.Exceptions.InvalidProjectFileException: The project file could not be loaded. Could not find a part of the path '/home/vincent/github/work/app/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj'.  /home/vincent/github/work/app/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj
 ---> System.IO.DirectoryNotFoundException: Could not find a part of the path '/home/vincent/github/work/app/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj'.
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, UnixFileMode openPermissions, Int64& fileLength, UnixFileMode& filePermissions, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at Microsoft.Build.Internal.XmlReaderExtension..ctor(String file, Boolean loadAsReadOnly)
   at Microsoft.Build.Construction.ProjectRootElement.LoadDocument(String fullPath, Boolean preserveFormatting, Boolean loadAsReadOnly)
   --- End of inner exception stack trace ---
   at Microsoft.Build.Shared.ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(Boolean condition, String errorSubCategoryResourceName, BuildEventFileInfo projectFile, Exception innerException, String resourceName, Object[] args)
   at Microsoft.Build.Construction.ProjectRootElement.LoadDocument(String fullPath, Boolean preserveFormatting, Boolean loadAsReadOnly)
   at Microsoft.Build.Construction.ProjectRootElement..ctor(String path, ProjectRootElementCacheBase projectRootElementCache, Boolean preserveFormatting)
   at Microsoft.Build.Construction.ProjectRootElement.CreateProjectFromPath(String projectFile, ProjectRootElementCacheBase projectRootElementCache, Boolean preserveFormatting)
   at Microsoft.Build.Evaluation.ProjectRootElementCache.GetOrLoad(String projectFile, OpenProjectRootElement loadProjectRootElement, Boolean isExplicitlyLoaded, Nullable`1 preserveFormatting)
   at Microsoft.Build.Evaluation.ProjectRootElementCache.Get(String projectFile, OpenProjectRootElement loadProjectRootElement, Boolean isExplicitlyLoaded, Nullable`1 preserveFormatting)
   at Microsoft.Build.Construction.ProjectRootElement.OpenProjectOrSolution(String fullPath, IDictionary`2 globalProperties, String toolsVersion, ProjectRootElementCacheBase projectRootElementCache, Boolean isExplicitlyLoaded)
   at Microsoft.Build.Execution.ProjectInstance..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion, String subToolsetVersion, ProjectCollection projectCollection, Nullable`1 projectLoadSettings, EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory, Boolean interactive)
   at Microsoft.Build.Execution.ProjectInstance..ctor(String projectFile, IDictionary`2 globalProperties, String toolsVersion)
   at Microsoft.DotNet.Workloads.Workload.Restore.WorkloadRestoreCommand.RunTargetToGetWorkloadIds(IEnumerable`1 allProjects)
   at Microsoft.DotNet.Workloads.Workload.Restore.WorkloadRestoreCommand.Execute()
   at System.CommandLine.Invocation.InvocationPipeline.Invoke(ParseResult parseResult)
   at Microsoft.DotNet.Cli.Program.ProcessArgs(String[] args, TimeSpan startupTime, ITelemetry telemetryClient)
```

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
2024-10-21 22:13:21 +02:00
Dennis Reimann
5bbbed8963
Fix build and some warnings 2024-10-21 20:36:34 +02:00
Dennis Reimann
5c480024d2
Update BTCPay Server 2024-10-21 13:37:33 +02:00
Dennis Reimann
8269a9d3ea
Remove balance graph from funds page
As discussed in the design meeting.
2024-10-06 10:06:58 +02:00
Kukks
610acc0542
full payment flow test 2024-10-02 14:31:05 +02:00
Dennis Reimann
b6a96dac7b
Upgrade BTCPay Server and packages 2024-10-02 12:00:32 +02:00
Dennis Reimann
ff0708a32c
Bootstrap updates 2024-09-27 12:43:34 +02:00
Dennis Reimann
6f4dbcb397
Update BTCPay Server 2024-09-27 12:43:02 +02:00
Kukks
6b6de10474
add fundamentals.md 2024-09-26 13:13:28 +02:00
Kukks
772471f435
deeper payment tests 2024-09-26 11:28:00 +02:00
Dennis Reimann
31d855465f
Fix freezing UI when generating a wallet manually
Closes #101.
2024-09-25 18:33:28 +02:00
Dennis Reimann
6318b706e6
Fix missing initial wallet generation
Fixes #100.
2024-09-25 18:18:12 +02:00
Dennis Reimann
1c7e57b093
Upgrade MAUI packages to fix restore error 2024-09-25 18:01:57 +02:00
Dennis Reimann
f144c2137b
Updates from debugging 2024-09-25 18:01:26 +02:00
Dennis Reimann
837f4163cd
Fix missing store error after create store 2024-09-25 18:00:13 +02:00
Dennis Reimann
d4b2954d34
Fix truncate center component 2024-09-25 17:04:00 +02:00
Kukks
416ffddebc
stupid gitignore 2024-09-25 16:56:53 +02:00
Kukks
edccc495aa
refactor 2024-09-25 12:46:59 +02:00
Dennis Reimann
b2d5777922
Catch event listener errors 2024-09-25 12:40:51 +02:00
Dennis Reimann
80afbb7a22
Add back initial auto-generate for onchain wallet 2024-09-25 12:39:21 +02:00
Dennis Reimann
96d7f18856
Update BTCPay Server and design system assets 2024-09-25 09:09:29 +02:00
Kukks
2d8a7c5498
self pay test 2024-09-19 13:50:29 +02:00
Kukks
fc8c42e17b
60% coverage of app core 2024-09-19 13:01:08 +02:00
Kukks
1298fb7d68
fix peer inbound 2024-09-19 12:37:52 +02:00
Kukks
c2ca7e2d09
deeper into test 2024-09-19 11:39:18 +02:00
Kukks
06c5989acd
sub update 2024-09-19 10:42:45 +02:00
Kukks
70e4eb29ed
fix race condition 2024-09-18 16:05:04 +02:00
Kukks
cf5ab8f5a3
fix test Logging 2024-09-18 15:30:08 +02:00
Kukks
b7368b4cfa
merge fix 2024-09-18 13:54:14 +02:00
Kukks
1c4e14e265
tests & fixes 2024-09-18 12:25:08 +02:00
Dennis Reimann
715037affa
App item editor file upload 2024-09-17 11:47:33 +02:00
Dennis Reimann
d717333bac
AppItemEditor updates 2024-09-16 19:24:53 +02:00
Dennis Reimann
a413bf7b62
Update BTCPay Server 2024-09-13 16:59:19 +02:00
Dennis Reimann
ae8a871826
Update BTCPay Server 2024-09-12 17:34:34 +02:00
Kukks
f4e4c317d4
fixes from tests 2024-09-12 15:08:23 +02:00
Dennis Reimann
f177dfd037
Add checkout page 2024-09-11 10:10:27 +02:00
Dennis Reimann
15429277a7
Add server app events and ensure an existing POS app
Fixes #95.
2024-09-10 18:40:20 +02:00
Kukks
1a9a8c8126
life by a thousand tiny fixes 2024-09-09 12:36:11 +02:00
Dennis Reimann
6dab1df016
Update and add profiles 2024-09-09 11:15:07 +02:00
Dennis Reimann
6f65c41079
Clean up Store Selector in Settings
Closes #52.
2024-09-07 07:38:39 +02:00
Dennis Reimann
8d08227b9b
Improve invoices list 2024-09-06 15:12:22 +02:00
Dennis Reimann
a85373ee35
Encryption key page improvements
Closes #94.
2024-09-06 13:54:56 +02:00
Dennis Reimann
49ef23f0e1
Update BTCPay Server 2024-09-06 13:24:39 +02:00
Dennis Reimann
95dc670214
Fix invoice data and loading
Fixes #96
2024-09-06 12:46:09 +02:00
Dennis Reimann
69560adb49
Fix connection string setting 2024-09-06 12:06:05 +02:00
Dennis Reimann
710a5a7d77
Update BTCPay Server 2024-09-06 11:26:05 +02:00
Kukks
c9d06fa099
mutiny 2024-08-29 13:40:23 +02:00
Dennis Reimann
3b10624617
Remove Newtonsoft.Json from UI 2024-08-28 18:32:15 +02:00
Dennis Reimann
4f00e50c94
Item editor basics 2024-08-28 09:18:44 +02:00
Dennis Reimann
12b467e02f
Various minor fixes and improvements 2024-08-28 09:18:42 +02:00
Dennis Reimann
0821c79b62
Add POS sales statistics 2024-08-21 12:39:14 +02:00
Dennis Reimann
21eea05f4d
Minor POS updates 2024-08-20 23:03:07 +02:00
Dennis Reimann
133125b6da
Unify scan qr besides input fields 2024-08-20 23:02:32 +02:00
Dennis Reimann
6e3ff406b5
Add scan QR to destination fields 2024-08-20 23:02:12 +02:00
Dennis Reimann
66218bbcd7
Fix unset store after login 2024-08-20 23:01:47 +02:00
Dennis Reimann
6812c535de
Update BTCPay Server 2024-08-20 18:17:53 +02:00
Dennis Reimann
00f88d4471
Cleanup histogram 2024-08-20 17:57:51 +02:00
Kukks
998eccd038
one more reorg fix 2024-08-20 16:02:38 +02:00
Kukks
5a450d18a4
fix reorg situation 2024-08-20 15:52:10 +02:00
Kukks
b71773f6ac
better state text for channel 2024-08-20 15:51:56 +02:00
Kukks
9d0ded4399
fix peer styling 2024-08-20 14:49:58 +02:00
Kukks
c851b33984
fixes part 2 2024-08-20 13:14:56 +02:00
Dennis Reimann
1e8deb38d0
Implement send 2024-08-19 17:56:27 +02:00
Kukks
a072511751
fix lsp 2024-08-19 17:01:55 +02:00
Dennis Reimann
5b7b0e5e94
Send: Add Send maximum button 2024-08-19 15:32:26 +02:00
Dennis Reimann
3b9ab06704
Improve InputAmount with rate and conversion 2024-08-19 15:28:36 +02:00
Dennis Reimann
da8d3bb513
Fix wallet overview loading state 2024-08-19 15:28:14 +02:00
Kukks
074876e544
fix long sync time issue 2024-08-19 13:09:19 +02:00
Dennis Reimann
b649596dc2
Catch file upload exceptions 2024-08-17 15:57:53 +02:00
Dennis Reimann
ed124079a6
Extract AmountDisplay component 2024-08-16 18:00:17 +02:00
Dennis Reimann
b0b70a3d95
Extract InputAmount component 2024-08-16 18:00:15 +02:00
Kukks
c117e27872
fix onew state code 2024-08-16 16:04:22 +02:00
Kukks
dbfbed666d
more code 2024-08-16 16:02:04 +02:00
Dennis Reimann
20fd84542d
Update funds pages 2024-08-16 15:40:37 +02:00
Kukks
f7bef6f582
run other thread 2024-08-16 14:54:56 +02:00
Dennis Reimann
3260ad02ef
Fix units on wallet funds page 2024-08-16 14:42:49 +02:00
Dennis Reimann
61d4cb9a76
Invoice page access fixes 2024-08-16 14:42:48 +02:00
Dennis Reimann
67e90e6b98
LN channels page fix 2024-08-16 14:42:46 +02:00
Kukks
fcec8cb549
connection stuff 2024-08-16 14:37:57 +02:00
Kukks
76d5dd225b
fix testnet pproofile 2024-08-16 14:37:57 +02:00
Dennis Reimann
77766a7923
Update signed out pages 2024-08-16 13:58:25 +02:00
Dennis Reimann
e620ab832a
Update funds pages 2024-08-16 13:58:11 +02:00
Dennis Reimann
330c467adf
Update LightningPaymentList 2024-08-16 13:57:49 +02:00
Kukks
65c1f52e25
try fix 2024-08-16 11:14:56 +02:00
Kukks
716531984e
dont wait to connect 2024-08-16 10:21:22 +02:00
Dennis Reimann
ff3db4d7f7
Move and update wallet pages 2024-08-16 09:57:03 +02:00
Dennis Reimann
1461108a2e
Simplify markup 2024-08-15 17:11:05 +02:00
Kukks
efca7ca412
fixing stuff when we use a real network 2024-08-15 16:21:04 +02:00
Dennis Reimann
6a9269d908
Peers and Channels empty states 2024-08-15 15:01:55 +02:00
Dennis Reimann
dde1ae2c59
Clean up event handlers on dispose 2024-08-15 14:55:15 +02:00
Dennis Reimann
156c49aa75
Lightning settings page updates 2024-08-15 14:54:49 +02:00
Dennis Reimann
22766308e9
Show receipt link on invoice details page
Submodule update fixes #89.
2024-08-15 10:25:41 +02:00
Dennis Reimann
ac421fc050
Unify titlebars 2024-08-15 09:33:28 +02:00
Dennis Reimann
ae7bd120d4
Encryption View should only have Back or Close
Fixes #87.
2024-08-15 09:33:18 +02:00
Dennis Reimann
1c737363e1
Tab Bar shouldn't be visible on first Store Selection view
Closes #88.
2024-08-15 09:20:07 +02:00
Dennis Reimann
1432e8a526
Fix going back to keypad
Fixes #85.
2024-08-14 16:00:14 +02:00
Dennis Reimann
095e618239
Remove balances from debug section 2024-08-14 13:15:00 +02:00
Dennis Reimann
00d9855883
Close button fixes 2024-08-14 13:10:35 +02:00
Dennis Reimann
34002b4d84
Display encryption key as QR code 2024-08-14 13:10:22 +02:00
Kukks
cd5d78b27c
dont wait on jit too long 2024-08-14 12:36:51 +02:00
Kukks
acecbfb4ab
dont switch to sync on shutdown 2024-08-14 10:36:05 +02:00
Kukks
35e4f91420
testnet sucks so treat feerate compute as shitty 2024-08-14 10:21:58 +02:00
Kukks
be7c8047a9
do not trigger accepted channel if not accepted 2024-08-14 10:02:01 +02:00
Kukks
51d2f71f9d
JIT fixes 2024-08-14 09:39:33 +02:00
Kukks
b6c668bcd8
use rgs timestamp from networkgraph directly 2024-08-14 08:17:49 +02:00
Dennis Reimann
28fd2a44b6
Send preparations 2024-08-13 18:55:07 +02:00
Dennis Reimann
65ca9237f6
Add LightningPaymentsList 2024-08-13 16:32:33 +02:00
Kukks
45cb172521
oops 2024-08-13 15:43:50 +02:00
Kukks
e4c9a6f3c9
additional fixes around connection 2024-08-13 15:38:52 +02:00
Dennis Reimann
28d05bd4f1
Move receive functionality to Receive page 2024-08-13 14:55:47 +02:00
Dennis Reimann
d07f01d116
Improve InvoiceItem 2024-08-13 14:55:45 +02:00
Kukks
0c2694548c
lsp refactor and fixes part 1 2024-08-13 14:52:26 +02:00
Dennis Reimann
aca73d1724
Move Withdraw button from Settings to Dashboard 2024-08-13 14:08:30 +02:00
Dennis Reimann
df468b0476
Extract WalletOverview component 2024-08-13 13:41:43 +02:00
Dennis Reimann
1aa98fc243
Component improvements 2024-08-13 13:39:16 +02:00
Kukks
364e26112e
handle logged out on connection 2024-08-13 11:29:03 +02:00
dstrukt
63bf13ae8f #82 tweaks 2024-08-12 13:38:31 -06:00
dstrukt
d1f2b7a7dd lightning channel page formatting 2024-08-12 13:22:43 -06:00
dstrukt
dbb105491a more formatting 2024-08-12 13:08:08 -06:00
dstrukt
b6b4dad7a7 encryption key formatting 2024-08-12 12:52:19 -06:00
dstrukt
747ef70f87 wallet + lighting page format consistency 2024-08-12 12:33:48 -06:00
Dennis Reimann
bf98bb8f13
Improve channel opening UI 2024-08-12 19:16:11 +02:00
Dennis Reimann
5c10767939
Toggle bitcoin unit 2024-08-12 17:50:33 +02:00
Kukks
f917f42bdd
add testnet run profile 2024-08-12 13:37:28 +02:00
Kukks
707e5c398f
Rapid gossip sync support 2024-08-12 13:15:19 +02:00
Dennis Reimann
1cb3e82d51
Notifications: Mark all as read 2024-08-12 11:40:57 +02:00
Dennis Reimann
d8a2144bbf
Refactoring: Views use user info from state
Fixes #81.
2024-08-12 11:21:13 +02:00
Kukks
80e4c2b805
fix channel creation 2024-08-12 09:20:08 +02:00
Kukks
6921484099
accept ther mnemonic seed when neding the encryption key 2024-08-12 08:50:58 +02:00
Dennis Reimann
9c721eb273
Fixup settings index 2024-08-09 20:50:00 +02:00
Dennis Reimann
e22a25bc27
Fixes #78 2024-08-09 20:49:50 +02:00
Dennis Reimann
e5cea23d0b
Quick encryption key page improvements 2024-08-09 20:36:05 +02:00
Dennis Reimann
09dfb8a28f
Select/Create store fix
Fixes #79.
2024-08-09 20:31:26 +02:00
Dennis Reimann
9d6d975c8e
Histogram fixes 2024-08-09 19:45:34 +02:00
Dennis Reimann
84d913a180
Authorize access to invoice pages 2024-08-09 19:45:12 +02:00
Kukks
750e1676a3
fix effect uri 2024-08-09 12:56:33 +02:00
Kukks
35576385e9
switch too slave 2024-08-09 12:49:36 +02:00
Kukks
1f9d599229
fix sync bug when there are multiple _ in name 2024-08-09 12:14:16 +02:00
Dennis Reimann
4aeb4136ab
Update BTCPay Server
Closes #61 and fixes #77.
2024-08-09 11:54:47 +02:00
Kukks
61f5383fc2
fix encryption key load 2024-08-09 11:09:57 +02:00
Kukks
1c6875d8c6
fix start/stop ln 2024-08-09 09:37:36 +02:00
Dennis Reimann
d51a28c635
Update StoreInfo in settings 2024-08-08 18:32:35 +02:00
Dennis Reimann
9d6f3cc8e6
Sign in user after accepting an invitation and setting the password 2024-08-08 18:32:12 +02:00
Kukks
1efd8a5a21
do not kill needlessly 2024-08-08 16:13:09 +02:00
Kukks
e9d912cbe7
better error for vss 2024-08-08 15:47:33 +02:00
Kukks
4f47d3ee55
more logs 2024-08-08 14:56:51 +02:00
Kukks
0394d84eef
more work around sync 2024-08-08 14:32:14 +02:00
Kukks
772015d56f
fix vss errror handling 2024-08-08 12:51:42 +02:00
Kukks
eb4bea4381
fix states machiens 2024-08-08 12:32:44 +02:00
Kukks
f204dc76cd
fix https and camera on android 2024-08-08 11:23:01 +02:00
Dennis Reimann
2f86950d6d
Invoices: check for store 2024-08-08 11:17:44 +02:00
Dennis Reimann
982e625ca3
Graph and balance updates 2024-08-08 11:14:19 +02:00
Dennis Reimann
34a7777297
Order connection statuses 2024-08-08 11:03:57 +02:00
Dennis Reimann
d33b3734ab
Chart basics 2024-08-07 19:53:11 +02:00
Kukks
199f480936
tryt fix 2024-08-07 13:20:17 +02:00
Kukks
56a75a07b8
fix camera on maui, fix local https on android 2024-08-07 13:03:06 +02:00
Kukks
8b63001c61
clean up attempts for beter bg service 2024-08-07 13:03:06 +02:00
Kukks
bf687d516f
upport hosted services in maui (rudiemntary, background may kill it) 2024-08-07 13:03:05 +02:00
Dennis Reimann
c7deb7e381
Update BTCPay Server 2024-08-06 19:52:08 +02:00
Dennis Reimann
de5c4d1a45
Update BTCPay Server 2024-08-06 17:04:14 +02:00
Dennis Reimann
686796f347
Balance check improvement 2024-08-05 16:20:10 +02:00
Kukks
a192ca2b49
Fix self payment 2024-08-05 16:06:03 +02:00
Kukks
6d7eed333d
fix withdraw send option 2024-08-05 16:05:53 +02:00
Kukks
4af0a90e07
Make jits disabled if not available on network 2024-08-05 16:05:42 +02:00
Kukks
f13649238d
add zeus olympus JIT lsp 2024-08-05 15:05:33 +02:00
Kukks
28b5272ce5
fix serializer 2024-08-05 14:51:27 +02:00
Kukks
77e9a81b3d
android works 2024-08-05 13:45:06 +02:00
Kukks
ac2f058bfe
attempt android 2024-08-05 13:28:20 +02:00
Kukks
e6e6ccb31c
add sqlite to maui 2024-08-05 13:28:20 +02:00
Kukks
c0b215efcd
fix fluxor on maui 2024-08-05 13:28:20 +02:00
Kukks
3f1c1968b9
impl ln methods, qait on remote sync fro ldk update, fix nres, attempt android foreground 2024-08-05 13:28:20 +02:00
Kukks
4fa6a708cb
add subm 2024-08-05 13:28:20 +02:00
Kukks
5178b0f190
compile maui 2024-08-05 13:28:20 +02:00
Kukks
b8834f8411
wip 2024-08-05 13:28:19 +02:00
Dennis Reimann
630679028e
Try to fix MAUI build 2024-08-05 13:28:19 +02:00
Dennis Reimann
ec04028b8e
Lightning node page: Fix state for destroy
Should fix #71.
2024-08-02 18:35:52 +02:00
Dennis Reimann
628c16015e
Lightning Channels page improvements 2024-08-02 18:26:19 +02:00
Dennis Reimann
988d00e29a
Lightning node page improvements
Fixes #70.
2024-08-02 18:16:07 +02:00
Dennis Reimann
315e517987
Onchain wallet page improvements 2024-08-02 18:16:05 +02:00
Dennis Reimann
ed6e51a744
Minor page fixes 2024-08-02 17:25:20 +02:00
Dennis Reimann
12518ee193
Refresh rates every five minutes 2024-08-02 15:29:40 +02:00
Dennis Reimann
e043d57e1c
Improve store change handling 2024-08-02 14:56:44 +02:00
Dennis Reimann
24a160a1c0
Navbar: Handling for no store selected case 2024-08-02 14:48:14 +02:00
Dennis Reimann
21a1d8ad7b
Minor connection improvements 2024-08-02 14:47:51 +02:00
Kukks
e8c816c04a
try to become master when master drops out 2024-08-01 10:07:49 +02:00
Kukks
a6acf2d8d0
Make sure wallet config state is reloaded after a sync 2024-08-01 09:40:50 +02:00
Dennis Reimann
94e8bc624b
Permission updates 2024-07-31 17:50:27 +02:00
Dennis Reimann
72b1c02780
Add debug items for balances 2024-07-31 17:38:10 +02:00
Dennis Reimann
586beb5ec8
Error handling improvements 2024-07-31 17:19:14 +02:00
Dennis Reimann
a8d29eb123
Better store switch handling 2024-07-31 17:18:51 +02:00
Dennis Reimann
9ddf88db27
Add connect page link to login page
See https://github.com/btcpayserver/app/issues/74#issuecomment-2260245116
2024-07-31 15:26:58 +02:00
Kukks
6dd14b482a
add state for key input 2024-07-31 15:04:38 +02:00
Dennis Reimann
ed553ca574
Mark clicked notifications as read 2024-07-31 14:30:57 +02:00
Kukks
564649f4af
fix restore 2024-07-31 13:28:41 +02:00
Kukks
313e3cf666
handle vss better + derive encryption key from wallet itself 2024-07-31 13:10:57 +02:00
Kukks
935ec8c1c0
start descriptor logic later 2024-07-31 11:22:59 +02:00
Kukks
8d4f6547b2
fix encryptor and adds logs to sync 2024-07-31 11:14:33 +02:00
Kukks
3ced11cc0c
Try fix mac build 2024-07-31 09:47:02 +02:00
Dennis Reimann
a239f92aed
Handle user data changes
Closes #21.
2024-07-30 23:58:54 +02:00
Dennis Reimann
f0f4feef41
Dashboard: UI unifications 2024-07-30 16:57:32 +02:00
Dennis Reimann
933280396c
Update BTCPay for state refresh and remove reload buttons 2024-07-30 16:57:18 +02:00
Kukks
864013da9b
Refactor the connection manager + sync
# Conflicts:
#	BTCPayApp.Core/Attempt2/LightningNodeService.cs
#	submodules/btcpayserver
2024-07-30 16:11:12 +02:00
Kukks
d4954659b7
Revert "Squashed commit of the following:"
This reverts commit d85ffe0704.
2024-07-30 16:10:37 +02:00
Kukks
d85ffe0704
Squashed commit of the following:
commit b4dea11bc6
Author: Kukks <evilkukka@gmail.com>
Date:   Tue Jul 30 15:57:17 2024 +0200

    Refactor the connection manager + sync

commit f06cf66e81
Merge: e6ee0d8 209b799
Author: Kukks <evilkukka@gmail.com>
Date:   Mon Jul 29 13:38:12 2024 +0200

    Merge remote-tracking branch 'github.com/master' into jit-backups

    # Conflicts:
    #	submodules/btcpayserver

commit e6ee0d8a2f
Author: Kukks <evilkukka@gmail.com>
Date:   Mon Jul 29 13:37:49 2024 +0200

    do not use syncer for now

commit 53fe9def9d
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jul 26 16:03:27 2024 +0200

    wip

commit 12ef00719e
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jul 26 13:21:41 2024 +0200

    restorer

commit 9b7a2ff7a2
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jul 26 11:28:12 2024 +0200

    moroe backup related code

commit b4896685cb
Merge: 9c09a44 882c22b
Author: Kukks <evilkukka@gmail.com>
Date:   Thu Jul 25 14:54:40 2024 +0200

    Merge remote-tracking branch 'github.com/master' into jit-backups

    # Conflicts:
    #	BTCPayApp.UI/Pages/Settings/LightningPage.razor

commit 9c09a440e2
Author: Kukks <evilkukka@gmail.com>
Date:   Thu Jul 25 14:50:25 2024 +0200

    WIP

commit ec215938df
Author: Kukks <evilkukka@gmail.com>
Date:   Tue Jul 23 16:28:35 2024 +0200

    wip

commit a05d039a55
Merge: f7fe33a 85643b8
Author: Kukks <evilkukka@gmail.com>
Date:   Mon Jul 22 12:03:48 2024 +0200

    Merge remote-tracking branch 'github.com/master' into jit-backups

    # Conflicts:
    #	BTCPayApp.Core/Attempt2/BTCPayAppServerClient.cs
    #	BTCPayApp.Core/Attempt2/BTCPayConnectionManager.cs
    #	BTCPayApp.UI/StateMiddleware.cs
    #	submodules/btcpayserver

commit f7fe33a730
Author: Kukks <evilkukka@gmail.com>
Date:   Mon Jul 22 11:54:06 2024 +0200

    separate file

commit e02ca2ba78
Author: Kukks <evilkukka@gmail.com>
Date:   Wed Jul 10 22:33:59 2024 +0200

    WIP (submodule not updated)

commit ee3a83c235
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jun 28 15:15:10 2024 +0200

    wip triggers

commit 4526824fb4
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jun 28 08:17:26 2024 +0200

    wip

commit 9953264836
Author: Kukks <evilkukka@gmail.com>
Date:   Thu Jun 27 13:21:56 2024 +0200

    Refactor connection to isolate json frameworks and start backup

commit c1788faefe
Author: Kukks <evilkukka@gmail.com>
Date:   Wed Jun 26 14:20:20 2024 +0200

    theoretically functional

commit 86c699d823
Author: Kukks <evilkukka@gmail.com>
Date:   Wed Jun 26 13:29:52 2024 +0200

    crying my way to success

commit a75e6b9929
Author: Kukks <evilkukka@gmail.com>
Date:   Fri Jun 21 08:24:18 2024 +0200

    jit wip

    subm
2024-07-30 16:02:09 +02:00
Dennis Reimann
d55a87f167
Remove create store success message
Closes #75.
2024-07-30 10:54:54 +02:00
Dennis Reimann
e3438ff78c
Introduce LN inactive state 2024-07-29 20:00:52 +02:00
Dennis Reimann
8af8cf5757
Refresh balance on wallet/node change 2024-07-29 20:00:18 +02:00
Dennis Reimann
c731a9574d
Refresh user info after creating a store
Fixes #42.
2024-07-29 19:59:47 +02:00
Dennis Reimann
8e3fb455a6
Remove POS created message 2024-07-29 19:23:34 +02:00
Dennis Reimann
35165b7fc8
Fix passed InvoiceList error parameter 2024-07-29 19:21:57 +02:00
Dennis Reimann
be6559fc08
Ensure base initializers are called 2024-07-29 19:21:31 +02:00
Dennis Reimann
1ae53b491e
Upgrade MAUI packages 2024-07-29 17:46:45 +02:00
Dennis Reimann
79489ae26c
Refactor store state 2024-07-29 17:45:19 +02:00
Dennis Reimann
eba2684b6a
Update BTCPay Server 2024-07-29 16:52:08 +02:00
Andrew Camilleri
05164ec471
JIT + Server Backup + JSON refactor (#69) 2024-07-29 13:39:12 +02:00
443 changed files with 28187 additions and 9223 deletions

310
.github/workflows/build-test.yml vendored Normal file
View File

@ -0,0 +1,310 @@
name: 'Test & Build the app'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
- '**/*.gitignore'
- '**/*.gitattributes'
pull_request_target:
branches:
- master
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
jobs:
test-desktop:
runs-on: ubuntu-latest
env:
CI: true
BTCPAY_SERVER_URL: http://localhost:14142
steps:
# Setup code, .NET and Android
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build --configuration Release BTCPayApp.Server
# Setup infrastructure
- name: Start containers
run: |
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" build
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
- name: Start BTCPay
run: |
./setup.sh
cd submodules/btcpayserver
# Start non-HTTPS to avoid certificate errors
nohup dotnet run -c Debug --project BTCPayServer --launch-profile Bitcoin &
while ! curl -s -k http://localhost:14142/api/v1/health > /dev/null; do
echo "Waiting for BTCPay Server to start..."
sleep 10
done
# Run tests
- name: Run tests
run: |
dotnet test -c Release -v n --logger "console;verbosity=normal" BTCPayApp.Tests
build-android:
runs-on: windows-latest
steps:
# Setup code, .NET and Android
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Install workloads
run: dotnet workload install maui
- name: Clean before build
run: |
dotnet clean BTCPayApp.Maui/BTCPayApp.Maui.csproj
- name: Build
# TODO: Use proper keystore once we switch to real releases
# https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-8.0#code-try-4
run: |
dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-android -c Release -o publish/android
env:
ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: org.btcpayserver.BTCPayApp-Signed.apk
path: publish/android/org.btcpayserver.BTCPayApp-Signed.apk
- name: Create pre-release
if: success() && github.ref == 'refs/heads/master'
uses: marvinpinto/action-automatic-releases@latest
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: |
publish/android/org.btcpayserver.BTCPayApp-Signed.apk
# build-desktop-win:
# runs-on: windows-latest
# steps:
# # Setup code, .NET and Android
# - uses: actions/checkout@v4
# with:
# submodules: recursive
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: 8.0.x
# - name: Build win x64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x64 -c Debug -o publish/win-x64
# - name: Build win x86
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x86 -c Debug -o publish/win-x86
# - name: Build win-arm64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-arm64 -c Debug -o publish/win-arm64
# - name: Upload artifact win
# uses: actions/upload-artifact@v4
# with:
# name: windows build
# path: |
# publish/win-x64
# publish/win-x86
# publish/win-arm64
# build-desktop-linux:
# runs-on: ubuntu-latest
# steps:
# # Setup code, .NET and Android
# - uses: actions/checkout@v4
# with:
# submodules: recursive
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: 8.0.x
# - name: Build linux x64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-x64 -c Debug -o publish/linux-x64
# - name: Build linux arm64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-arm64 -c Debug -o publish/linux-arm64
# - name: Upload artifact linux
# uses: actions/upload-artifact@v4
# with:
# name: linux build
# path: |
# publish/linux-x64
# publish/linux-arm64
# build-desktop-mac:
# runs-on: macos-latest
# steps:
# # Checkout the code
# - uses: actions/checkout@v4
# with:
# submodules: recursive
# # Import code-signing certificates
# - name: Import Code-Signing Certificates
# uses: apple-actions/import-codesign-certs@v3
# with:
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
# # Verify imported certificates and extract Developer ID
# - name: Find Code Signing Certificate
# id: find-cert
# run: |
# CERT_NAME=$(security find-identity -v -p codesigning | grep "Apple Distribution" | awk -F '"Apple Distribution: ' '{print $2}' | awk -F '"' '{print $1}')
# if [ -z "$CERT_NAME" ]; then
# echo "No valid Apple Distribution certificate found!"
# exit 1
# fi
# echo "Certificate Name: Apple Distribution: $CERT_NAME"
# echo "CERT_NAME=Apple Distribution: $CERT_NAME" >> $GITHUB_ENV
# # Setup .NET
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: 8.0.x
# # Build the app for macOS architectures
# - name: Build mac x64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-x64 -o publish/osx-x64
# - name: Build mac arm64
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-arm64 -o publish/osx-arm64
# # Create the .app bundle
# - name: Create .app bundle for x64
# run: |
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/MacOS
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/Resources
# ls -lA publish/osx-x64/
# cp -R publish/osx-x64/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
# #cp -R publish/osx-x64/* dist/osx-x64/BTCPayApp.app/Contents/MacOS/
# #mv dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
# <plist version=\"1.0\">
# <dict>
# <key>CFBundleExecutable</key>
# <string>BTCPayApp</string>
# <key>CFBundleIdentifier</key>
# <string>org.btcpayserver.app</string>
# <key>CFBundleName</key>
# <string>BTCPay App</string>
# <key>CFBundleVersion</key>
# <string>1.0</string>
# <key>CFBundlePackageType</key>
# <string>APPL</string>
# </dict>
# </plist>" > dist/osx-x64/BTCPayApp.app/Contents/Info.plist
# cat dist/osx-x64/BTCPayApp.app/Contents/Info.plist
# ls -lA dist/osx-x64/BTCPayApp.app/Contents/**
# - name: Create .app bundle for arm64
# run: |
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/MacOS
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/Resources
# ls -lA publish/osx-arm64/
# cp -R publish/osx-arm64/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
# #cp -R publish/osx-arm64/* dist/osx-arm64/BTCPayApp.app/Contents/MacOS/
# #mv dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
# <plist version=\"1.0\">
# <dict>
# <key>CFBundleExecutable</key>
# <string>BTCPayApp</string>
# <key>CFBundleIdentifier</key>
# <string>org.btcpayserver.app</string>
# <key>CFBundleName</key>
# <string>BTCPay App</string>
# <key>CFBundleVersion</key>
# <string>1.0</string>
# <key>CFBundlePackageType</key>
# <string>APPL</string>
# </dict>
# </plist>" > dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
# cat dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
# ls -lA dist/osx-arm64/BTCPayApp.app/Contents/**
# # Sign the .app bundles using the dynamic certificate name
# - name: Sign x64 app bundle
# run: |
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-x64/BTCPayApp.app
# codesign --verify --deep --strict dist/osx-x64/BTCPayApp.app
# - name: Sign arm64 app bundle
# run: |
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-arm64/BTCPayApp.app
# codesign --verify --deep --strict dist/osx-arm64/BTCPayApp.app
# # Verify app bundle signing
# - name: Verify x64 app bundle signing
# run: spctl --assess --type execute dist/osx-x64/BTCPayApp.app
# continue-on-error: true
# - name: Verify arm64 app bundle signing
# run: spctl --assess --type execute dist/osx-arm64/BTCPayApp.app
# continue-on-error: true
# # Create DMG files
# - name: Create DMG for x64
# run: |
# mkdir -p dmg
# hdiutil create -size 1gb -volname "BTCPayApp-osx-x64" -srcfolder "dist/osx-x64" -ov -format UDZO dmg/BTCPayApp-x64.dmg
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-x64.dmg
# codesign --verify --deep --strict dmg/BTCPayApp-x64.dmg
# - name: Create DMG for arm64
# run: |
# mkdir -p dmg
# hdiutil create -size 1gb -volname "BTCPayApp-osx-arm64" -srcfolder "dist/osx-arm64" -ov -format UDZO dmg/BTCPayApp-arm64.dmg
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-arm64.dmg
# codesign --verify --deep --strict dmg/BTCPayApp-arm64.dmg
# # Verify DMG signing
# - name: Verify x64 DMG signing
# run: spctl --assess --type execute dmg/BTCPayApp-x64.dmg
# continue-on-error: true
# - name: Verify arm64 DMG signing
# run: spctl --assess --type execute dmg/BTCPayApp-arm64.dmg
# continue-on-error: true
# # Upload artifacts
# - name: Upload DMG artifacts
# uses: actions/upload-artifact@v4
# with:
# name: mac-dmg
# path: dmg
#
# build-ios:
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@v4
# with:
# submodules: recursive
# - name: Setup Xcode version
# uses: maxim-lobanov/setup-xcode@v1.6.0
# with:
# xcode-version: 16.0
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: 8.0.x
# - name: Install workloads
# run: dotnet workload install maui
# - name: Import Code-Signing Certificates
# uses: Apple-Actions/import-codesign-certs@v1
# with:
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
# - name: Download Apple Provisioning Profiles
# uses: Apple-Actions/download-provisioning-profiles@v1
# with:
# bundle-id: ${{ secrets.APPLE_BUNDLE_ID }}
# issuer-id: ${{ secrets.APPLE_ISSUER_ID }}
# api-key-id: ${{ secrets.APPLE_KEY_ID }}
# api-private-key: ${{ secrets.APPLE_KEY }}
# - name: Build
# run: dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-ios -c Debug -o publish/ios
# - name: Upload artifact
# uses: actions/upload-artifact@v4
# with:
# name: ios build
# path: |
# publish/ios

View File

@ -1,51 +0,0 @@
name: 'Test the desktop app'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
- '**/*.gitignore'
- '**/*.gitattributes'
pull_request:
branches:
- master
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
jobs:
build:
runs-on: ubuntu-latest
steps:
# Setup code, .NET and Android
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build --configuration Release BTCPayApp.Server
# E2E tests
- name: Start containers
run: docker-compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
- name: Start BTCPay
run: |
cd submodules/btcpayserver
nohup dotnet run -c Release --project BTCPayServer &
while ! curl -s http://localhost:14142/api/v1/health > /dev/null; do
echo "Waiting for BTCPay Server to start..."
sleep 10
done
# Unit and integration tests
- name: Run fast tests
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Fast=Fast" BTCPayApp.Tests
- name: Run integration tests
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Integration=Integration" BTCPayApp.Tests
- name: Run Selenium tests
run: dotnet test --filter "Selenium=Selenium" -v n --logger "console;verbosity=normal" BTCPayApp.Tests

View File

@ -1,51 +0,0 @@
name: 'Test the mobile app'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
- '**/*.gitignore'
- '**/*.gitattributes'
pull_request:
branches:
- master
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
jobs:
build:
runs-on: windows-latest
steps:
# Setup code, .NET and Android
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '22'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# Restore and build
- name: Install workloads
run: dotnet workload install maui --ignore-failed-sources
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
# Unit and integration tests
- name: Run fast tests
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Fast=Fast"
- name: Run integration tests
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Integration=Integration"

3
.gitignore vendored
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

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="App-Server 2nd Instance" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
<option name="LAUNCH_PROFILE_NAME" value="https-second-instance" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="App-Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
<option name="LAUNCH_PROFILE_NAME" value="mutinynet" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,18 @@
·<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BTCPayApp.Maui" type="XamarinAndroidProject" factoryName="Xamarin.Android">
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEPLOY_BEHAVIOUR_NAME" value="Default" />
<method v="2">
<option name="Build" />
</method>
</configuration>
<configuration default="false" name="BTCPayApp.Maui" type="XamarinIOSProject" factoryName="Xamarin.iOS">
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="IS_PASS_PARENT_ENVS" value="false" />
<option name="EXTRA_MLAUNCH_PARAMETERS" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DEV ALL WITH SECOND APP" type="CompoundRunConfigurationType">
<toRun name="App-Server" type="LaunchSettings" />
<toRun name="App-Server 2nd Instance" type="LaunchSettings" />
<toRun name="Server Regtest" type="LaunchSettings" />
<toRun name="Docker compose" type="docker-deploy" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DEV ALL testnet" type="CompoundRunConfigurationType">
<toRun name="App-Server" type="LaunchSettings" />
<toRun name="Server Testnet" type="LaunchSettings" />
<toRun name="Docker compose testnet" type="docker-deploy" />
<method v="2" />
</configuration>
</component>

View File

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DEV ALL" type="CompoundRunConfigurationType">
<toRun name="App-Server" type="LaunchSettings" />
<toRun name="Server" type="LaunchSettings" />
<toRun name="Server Regtest" type="LaunchSettings" />
<toRun name="Docker compose" type="docker-deploy" />
<method v="2" />
</configuration>

7
.run/DEV ANDROID.run.xml Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DEV ANDROID" type="CompoundRunConfigurationType">
<toRun name="Server Regtest" type="LaunchSettings" />
<toRun name="Docker compose" type="docker-deploy" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker compose mutiny" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envFilePath" value="" />
<option name="services">
<list>
<option value="dev" />
</list>
</option>
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.mutinynet.yml" />
</settings>
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker compose testnet" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envFilePath" value="" />
<option name="services">
<list>
<option value="dev" />
</list>
</option>
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.testnet.yml" />
</settings>
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-MUTINYNET" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server Regtest" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" default="false" projectName="BTCPayApp.Core" projectPath="$PROJECT_DIR$/BTCPayApp.Core/BTCPayApp.Core.csproj" />
<option name="Build" default="false" projectName="BTCPayServer.Plugins.App" projectPath="$PROJECT_DIR$/BTCPayServer.Plugins.App/BTCPayServer.Plugins.App.csproj" />
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server Testnet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-TESTNET" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,26 @@
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core;
public class AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
if (pendingMigrationsAsync.Length != 0)
{
logger.LogInformation("Applying {Length} migrations", pendingMigrationsAsync.Length);
await dbContext.Database.MigrateAsync(cancellationToken);
logger.LogInformation("Migrations applied: {Migrations}", string.Join(", ", pendingMigrationsAsync));
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

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

@ -1,119 +0,0 @@
using System.Text;
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Crypto;
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
namespace BTCPayApp.Core.Attempt2;
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> logger, IServiceProvider serviceProvider) : IBTCPayAppHubClient
{
public event AsyncEventHandler<string>? OnNewBlock;
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
public event AsyncEventHandler<string>? OnNotifyNetwork;
public event AsyncEventHandler<string>? OnServerNodeInfo;
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
public async Task NotifyServerEvent(ServerEvent serverEvent)
{
logger.LogInformation("NotifyServerEvent: {Type} - {Details}", serverEvent.Type, serverEvent.ToString());
await OnNotifyServerEvent?.Invoke(this, serverEvent)!;
}
public async Task NotifyNetwork(string network)
{
logger.LogInformation("NotifyNetwork: {network}", network);
await OnNotifyNetwork?.Invoke(this, network);
}
public async Task NotifyServerNode(string nodeInfo)
{
logger.LogInformation("NotifyServerNode: {nodeInfo}", nodeInfo);
await OnServerNodeInfo?.Invoke(this, nodeInfo);
}
public async Task TransactionDetected(TransactionDetectedRequest request)
{
logger.LogInformation($"OnTransactionDetected: {request.TxId}");
await OnTransactionDetected?.Invoke(this, request);
}
public async Task NewBlock(string block)
{
logger.LogInformation("NewBlock: {block}", block);
await OnNewBlock?.Invoke(this, block);
}
private PaymentsManager PaymentsManager =>
serviceProvider.GetRequiredService<LightningNodeManager>().Node.PaymentsManager;
public async Task<LightningPayment> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest)
{
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
false);
return await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
createLightningInvoiceRequest.Expiry, descHash);
}
public async Task<LightningPayment?> GetLightningInvoice(string paymentHash)
{
var invs = await PaymentsManager.List(payments =>
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
return invs.FirstOrDefault();
}
public async Task<LightningPayment?> GetLightningPayment(string paymentHash)
{
var invs = await PaymentsManager.List(payments =>
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
return invs.FirstOrDefault();
}
public async Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request)
{
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default);
}
public async Task<List<LightningPayment>> GetLightningInvoices(ListInvoicesParams request)
{
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default);
}
public async Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi)
{
var network = serviceProvider.GetRequiredService<OnChainWalletManager>().Network;
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
try
{
var result = await PaymentsManager.PayInvoice(bolt,
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
return new PayResponse()
{
Result = result.Status switch
{
LightningPaymentStatus.Unknown => PayResult.Unknown,
LightningPaymentStatus.Pending => PayResult.Unknown,
LightningPaymentStatus.Complete => PayResult.Ok,
LightningPaymentStatus.Failed => PayResult.Error,
_ => throw new ArgumentOutOfRangeException()
},
Details = new PayDetails()
{
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
Status = result.Status
}
};
}
catch (Exception e)
{
logger.LogError(e, "Error paying invoice");
return new PayResponse(PayResult.Error, e.Message);
}
}
}

View File

@ -1,191 +0,0 @@
using System.Net;
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
using TypedSignalR.Client;
namespace BTCPayApp.Core.Attempt2;
public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
{
private readonly IAccountManager _accountManager;
private readonly AuthenticationStateProvider _authStateProvider;
private readonly ILogger<BTCPayConnectionManager> _logger;
private readonly BTCPayAppServerClient _btcPayAppServerClient;
private readonly IBTCPayAppHubClient _btcPayAppServerClientInterface;
private IDisposable? _subscription;
public IBTCPayAppHubServer? HubProxy { get; private set; }
public HubConnection? Connection { get; private set; }
public Network? ReportedNetwork { get; private set; }
public string ReportedNodeInfo { get; set; }
public event AsyncEventHandler<(HubConnectionState Old, HubConnectionState New)>? ConnectionChanged;
private HubConnectionState _connectionState = HubConnectionState.Disconnected;
public HubConnectionState ConnectionState
{
get => Connection?.State ?? HubConnectionState.Disconnected;
private set
{
if (_connectionState == value)
return;
var old = _connectionState;
_connectionState = value;
_logger.LogInformation("Connection state changed: {State}", _connectionState);
ConnectionChanged?.Invoke(this, (old, _connectionState));
}
}
public BTCPayConnectionManager(
IAccountManager accountManager,
AuthenticationStateProvider authStateProvider,
ILogger<BTCPayConnectionManager> logger,
BTCPayAppServerClient btcPayAppServerClient,
IBTCPayAppHubClient btcPayAppServerClientInterface)
{
_accountManager = accountManager;
_authStateProvider = authStateProvider;
_logger = logger;
_btcPayAppServerClient = btcPayAppServerClient;
_btcPayAppServerClientInterface = btcPayAppServerClientInterface;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
_btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
_btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
await StartOrReplace();
_ = TryStayConnected();
}
private async Task OnServerNodeInfo(object? sender, string e)
{
ReportedNodeInfo = e;
}
private async Task OnNotifyServerEvent(object? sender, ServerEvent e)
{
_logger.LogInformation("OnNotifyServerEvent: {Type} - {Details}", e.Type, e.ToString());
}
private async Task OnNotifyNetwork(object? sender, string e)
{
ReportedNetwork = Network.GetNetwork(e);
}
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
{
try
{
await task;
var authenticated = await _accountManager.CheckAuthenticated();
if (!authenticated)
await Kill();
else
await StartOrReplace();
}
catch (Exception e)
{
_logger.LogError(e, "Error while handling authentication state change");
}
}
private async Task TryStayConnected()
{
while (true)
{
try
{
if (Connection is not null && ConnectionState == HubConnectionState.Disconnected)
{
await Connection.StartAsync();
ConnectionState = HubConnectionState.Connected;
}
else
{
await Task.Delay(5000);
}
}
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
{
var result = await _accountManager.RefreshAccess();
if (result.Succeeded)
await StartOrReplace();
else
await Kill();
await Task.Delay(1000);
}
catch (Exception e)
{
await Task.Delay(1000);
}
}
}
private async Task Kill()
{
if (Connection is not null)
await Connection.StopAsync();
Connection = null;
ConnectionState = HubConnectionState.Disconnected;
_subscription?.Dispose();
HubProxy = null;
}
private async Task StartOrReplace()
{
await Kill();
var account = _accountManager.GetAccount();
if (account is null)
return;
Connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol()
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(), options =>
{
options.AccessTokenProvider = () => Task.FromResult(_accountManager.GetAccount()?.AccessToken);
})
.WithAutomaticReconnect()
.Build();
_subscription = Connection.Register(_btcPayAppServerClientInterface);
HubProxy = Connection.CreateHubProxy<IBTCPayAppHubServer>();
}
public Task StopAsync(CancellationToken cancellationToken)
{
_authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
return Task.CompletedTask;
}
public Task OnClosed(Exception? exception)
{
_logger.LogError(exception, "Hub connection closed");
ConnectionState = HubConnectionState.Disconnected;
return Task.CompletedTask;
}
public Task OnReconnected(string? connectionId)
{
_logger.LogInformation("Hub reconnected: {ConnectionId}", connectionId);
ConnectionState = HubConnectionState.Connected;
return Task.CompletedTask;
}
public Task OnReconnecting(Exception? exception)
{
_logger.LogWarning(exception, "Hub reconnecting");
ConnectionState = HubConnectionState.Connecting;
return Task.CompletedTask;
}
}

View File

@ -1,41 +0,0 @@
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core.Attempt2;
public class BTCPayPaymentsNotifier : IScopedHostedService
{
private readonly PaymentsManager _paymentsManager;
private readonly BTCPayConnectionManager _connectionManager;
private readonly OnChainWalletManager _onChainWalletManager;
public BTCPayPaymentsNotifier(
PaymentsManager paymentsManager, BTCPayConnectionManager connectionManager,
OnChainWalletManager onChainWalletManager)
{
_paymentsManager = paymentsManager;
_connectionManager = connectionManager;
_onChainWalletManager = onChainWalletManager;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
}
private async Task OnPaymentUpdate(object? sender, LightningPayment e)
{
await _connectionManager.HubProxy
.SendPaymentUpdate(
_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier, e)
.RunSync();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
}
}

View File

@ -1,18 +0,0 @@
using org.ldk.structs;
namespace BTCPayApp.Core.Attempt2;
public class LDKChangeDestinationSource:ChangeDestinationSourceInterface
{
private readonly LightningNodeManager _lightningNodeManager;
public LDKChangeDestinationSource( LightningNodeManager lightningNodeManager)
{
_lightningNodeManager = lightningNodeManager;
}
public Result_CVec_u8ZNoneZ get_change_destination_script()
{
var s = _lightningNodeManager.Node.DeriveScript().ConfigureAwait(false).GetAwaiter().GetResult();
return Result_CVec_u8ZNoneZ.ok(s.ToBytes());
}
}

View File

@ -1,43 +0,0 @@
using BTCPayApp.Core.Contracts;
using org.ldk.enums;
using org.ldk.structs;
namespace BTCPayApp.Core.Attempt2;
public class LDKKVStore:KVStoreInterface
{
private readonly IConfigProvider _configProvider;
public LDKKVStore(IConfigProvider configProvider)
{
_configProvider = configProvider;
}
public Result_CVec_u8ZIOErrorZ read(string primary_namespace, string secondary_namespace, string key)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
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 = $"{primary_namespace}:{secondary_namespace}:{key}";
_configProvider.Set(key1, buf).ConfigureAwait(false).GetAwaiter().GetResult();
return Result_NoneIOErrorZ.ok();
}
public Result_NoneIOErrorZ remove(string primary_namespace, string secondary_namespace, string key, bool lazy)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
_configProvider.Set<byte[]>(key1, null).ConfigureAwait(false).GetAwaiter().GetResult();
return Result_NoneIOErrorZ.ok();
}
public Result_CVec_StrZIOErrorZ list(string primary_namespace, string secondary_namespace)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:";
var result = _configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
return Result_CVec_StrZIOErrorZ.ok(result.ToArray());
}
}

View File

@ -1,384 +0,0 @@
using System.Collections.Specialized;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
using org.ldk.structs;
using OutPoint = NBitcoin.OutPoint;
using UInt128 = org.ldk.util.UInt128;
namespace BTCPayApp.Core.Attempt2;
public partial class LDKNode:
ILDKEventHandler<Event.Event_ChannelClosed>,
ILDKEventHandler<Event.Event_ChannelPending>,
ILDKEventHandler<Event.Event_ChannelReady>
{
public async Task<ChannelDetails[]> GetChannels(CancellationToken cancellationToken = default)
{
return await _memoryCache.GetOrCreateAsync(nameof(GetChannels), async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return ServiceProvider.GetRequiredService<ChannelManager>().list_channels();
}).WithCancellation(cancellationToken);
}
public async Task Handle(Event.Event_ChannelClosed evt)
{
_memoryCache.Remove(nameof(GetChannels));
}
public async Task Handle(Event.Event_ChannelPending @event)
{
_memoryCache.Remove(nameof(GetChannels));
}
public async Task Handle(Event.Event_ChannelReady @event)
{
_memoryCache.Remove(nameof(GetChannels));
}
public async Task<PeerDetails[]> GetPeers(CancellationToken cancellationToken = default)
{
return await _memoryCache.GetOrCreateAsync(nameof(GetPeers), async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return ServiceProvider.GetRequiredService<PeerManager>().list_peers();
}).WithCancellation(cancellationToken);
}
public void PeersChanged()
{
_memoryCache.Remove(nameof(GetPeers));
}
private void InvalidateCache()
{
_memoryCache.Remove(nameof(GetPeers));
_memoryCache.Remove(nameof(GetChannels));
}
public async Task<Result_ChannelIdAPIErrorZ> OpenChannel(Money amount, PubKey nodeId, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Opening channel with {nodeId} for {amount}", nodeId, amount);
var channelManager = ServiceProvider.GetRequiredService<ChannelManager>();
var entropySource = ServiceProvider.GetRequiredService<EntropySource>();
var userConfig = ServiceProvider.GetRequiredService<UserConfig>();
var temporaryChannelId = ChannelId.temporary_from_entropy_source(entropySource);
var userChannelId = new UInt128(temporaryChannelId.get_a().Take(16).ToArray());
try
{
return await Task.Run(() => channelManager.create_channel(nodeId.ToBytes(), amount.Satoshi, 0, userChannelId,
temporaryChannelId, userConfig), cancellationToken);
}
finally
{
_logger.LogInformation("finished (trying to) opening channel with {nodeId} for {amount}", nodeId, amount);
}
}
}
public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
{
private readonly IMemoryCache _memoryCache;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly BTCPayConnectionManager _connectionManager;
private readonly ILogger _logger;
private readonly IConfigProvider _configProvider;
private readonly OnChainWalletManager _onChainWalletManager;
public LDKNode(
IMemoryCache cache,
IDbContextFactory<AppDbContext> dbContextFactory,
BTCPayConnectionManager connectionManager,
IServiceProvider serviceProvider,
LDKWalletLogger logger,
IConfigProvider configProvider,
OnChainWalletManager onChainWalletManager)
{
_memoryCache = cache;
_dbContextFactory = dbContextFactory;
_connectionManager = connectionManager;
_logger = logger;
_configProvider = configProvider;
_onChainWalletManager = onChainWalletManager;
ServiceProvider = serviceProvider;
}
private IServiceProvider ServiceProvider { get; }
private TaskCompletionSource? _started;
private readonly SemaphoreSlim _semaphore = new(1);
public Network Network => ServiceProvider.GetRequiredService<Network>();
public async Task StartAsync(CancellationToken cancellationToken)
{
bool exists;
try
{
await _semaphore.WaitAsync(cancellationToken);
exists = _started is not null;
_started ??= new TaskCompletionSource();
}
finally
{
_semaphore.Release();
}
if (exists)
{
await _started.Task;
return;
}
InvalidateCache();
_config = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key)?? new LightningConfig();
_configLoaded.SetResult();
var keyPath = KeyPath.Parse(_config.LightningDerivationPath);
Seed = new Mnemonic( _onChainWalletManager.WalletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
var services = ServiceProvider.GetServices<IScopedHostedService>();
_logger.LogInformation("Starting LDKNode services");
var bb = await _onChainWalletManager.GetBestBlock();
if (bb is null)
{
throw new InvalidOperationException("Best block could not be retrieved. Killing the startup");
}
foreach (var service in services)
{
_logger.LogInformation($"Starting {service.GetType().Name}");
await service.StartAsync(cancellationToken);
}
_started.SetResult();
_logger.LogInformation("LDKNode started");
}
private readonly TaskCompletionSource _configLoaded = new();
public async Task<LightningConfig> GetConfig()
{
await _configLoaded.Task;
return _config!;
}
private async Task UpdateConfig(LightningConfig config)
{
await _started.Task;
await _configProvider.Set(LightningConfig.Key, config);
_config = config;
ConfigUpdated?.Invoke(this, config);
}
public AsyncEventHandler<LightningConfig>? ConfigUpdated;
public byte[] Seed { get; private set; }
public PaymentsManager PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
public LDKPeerHandler PeerHandler => ServiceProvider.GetRequiredService<LDKPeerHandler>();
public PubKey NodeId => new(ServiceProvider.GetRequiredService<ChannelManager>().get_our_node_id());
public async Task StopAsync(CancellationToken cancellationToken)
{
bool exists;
try
{
await _semaphore.WaitAsync(cancellationToken);
exists = _started is not null;
}
finally
{
_semaphore.Release();
}
if (!exists)
return;
var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
_logger.LogInformation("Stopping LDKNode services");
var services = ServiceProvider.GetServices<IScopedHostedService>();
var tasks = services.Select(async service =>
{
_logger.LogInformation($"Stopping {service.GetType().Name}");
await service.StopAsync(cancellationToken);
_logger.LogInformation($"Stopped {service.GetType().Name}");
}).ToArray();
await Task.WhenAll(tasks);
_ = _connectionManager.HubProxy.IdentifierActive(identifier, false).RunSync();
}
public void Dispose() => DisposeAsync().GetAwaiter().GetResult();
public async ValueTask DisposeAsync()
{
// await StopAsync(CancellationToken.None);
}
private readonly TaskCompletionSource<ChannelMonitor[]?> icm = new();
private LightningConfig? _config;
public async Task<ChannelMonitor[]> GetInitialChannelMonitors()
{
return await icm.Task;
}
private async Task<ChannelMonitor[]> GetInitialChannelMonitors(EntropySource entropySource,
SignerProvider signerProvider)
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var data = await db.LightningChannels.Select(channel => channel.Data)
.ToArrayAsync();
var channels = ChannelManagerHelper.GetInitialMonitors(data, entropySource, signerProvider);
icm.SetResult(channels);
return channels;
}
public async Task<byte[]?> GetRawChannelManager()
{
return await _configProvider.Get<byte[]>("ChannelManager") ?? null;
}
public async Task UpdateChannelManager(ChannelManager serializedChannelManager)
{
await _configProvider.Set("ChannelManager", serializedChannelManager.write());
}
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
{
await _configProvider.Set("NetworkGraph", networkGraph.write());
}
public async Task UpdateScore(WriteableScore score)
{
await _configProvider.Set("Score", score.write());
}
public async Task<(byte[] serializedChannelManager, ChannelMonitor[] channelMonitors)?> GetSerializedChannelManager(
EntropySource entropySource, SignerProvider signerProvider)
{
var data = await GetRawChannelManager();
if (data is null)
{
icm.SetResult(Array.Empty<ChannelMonitor>());
return null;
}
var channels = await GetInitialChannelMonitors(entropySource, signerProvider);
return (data, channels);
}
public async Task<Script> DeriveScript()
{
var derivationKey = (await GetConfig()).ScriptDerivationKey;
return await _onChainWalletManager.DeriveScript(derivationKey);
}
public async Task TrackScripts(Script[] scripts, string derivation = WalletDerivation.LightningScripts)
{
try
{
_logger.LogDebug("Tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
var identifier = _onChainWalletManager.WalletConfig.Derivations[derivation].Identifier;
await _connectionManager.HubProxy.TrackScripts(identifier,
scripts.Select(script => script.ToHex()).ToArray()).RunSync();
_logger.LogDebug("Tracked scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
}
catch (Exception e)
{
_logger.LogError(e, "Error tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
}
}
public async Task UpdateChannel(string id, byte[] write)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
var channel = await context.LightningChannels.SingleOrDefaultAsync(lightningChannel => lightningChannel.Id == id || lightningChannel.Aliases.Contains(id));
if (channel is not null)
{
if (!channel.Aliases.Contains(channel.Id))
{
channel.Aliases.Add(channel.Id);
}
if (!channel.Aliases.Contains(id))
{
channel.Aliases.Add(id);
}
channel.Id = id;
channel.Data = write;
}
else
{
await context.LightningChannels.AddAsync(new Channel()
{
Id = id,
Data = write,
Aliases = [id]
});
}
await context.SaveChangesAsync();
}
public async Task Peer(string toString, PeerInfo? value)
{
toString = toString.ToLowerInvariant();
var config = await GetConfig();
if (value is null)
{
if (config.Peers.Remove(toString))
{
await UpdateConfig(config);
return;
}
}
config.Peers.AddOrReplace(toString, value);
await UpdateConfig(config);
}
}

View File

@ -1,266 +0,0 @@
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core.Attempt2;
public class LightningNodeManager : BaseHostedService
{
public const string PaymentMethodId = "BTC-LN";
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly ILogger<LightningNodeManager> _logger;
private readonly OnChainWalletManager _onChainWalletManager;
private readonly BTCPayConnectionManager _btcPayConnectionManager;
private readonly IServiceScopeFactory _serviceScopeFactory;
private IServiceScope? _nodeScope;
public LDKNode? Node => _nodeScope?.ServiceProvider.GetService<LDKNode>();
private LightningNodeState _state = LightningNodeState.Init;
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
private bool IsOnchainConfigured => _onChainWalletManager.WalletConfig is not null;
private bool IsOnchainLightningDerivationConfigured => _onChainWalletManager.WalletConfig?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
public bool CanConfigureLightningNode => IsHubConnected && IsOnchainConfigured && !IsOnchainLightningDerivationConfigured && State == LightningNodeState.NotConfigured;
public string? ConnectionString => IsOnchainLightningDerivationConfigured
? $"type=app;group={_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower()
: null;
public LightningNodeState State
{
get => _state;
private set
{
if (_state == value)
return;
var old = _state;
_state = value;
_logger.LogInformation("Lightning node state changed: {State}", _state);
StateChanged?.Invoke(this, (old, value));
}
}
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
public LightningNodeManager(
IDbContextFactory<AppDbContext> dbContextFactory,
ILogger<LightningNodeManager> logger,
OnChainWalletManager onChainWalletManager,
BTCPayConnectionManager btcPayConnectionManager,
IServiceScopeFactory serviceScopeFactory)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
_onChainWalletManager = onChainWalletManager;
_btcPayConnectionManager = btcPayConnectionManager;
_serviceScopeFactory = serviceScopeFactory;
}
public async Task StartNode()
{
if (_nodeScope is not null || State is LightningNodeState.Loaded)
return;
await _controlSemaphore.WaitAsync();
try
{
if (_nodeScope is null)
{
_nodeScope = _serviceScopeFactory.CreateScope();
_cancellationTokenSource = new CancellationTokenSource();
}
await Node.StartAsync(_cancellationTokenSource.Token);
State = LightningNodeState.Loaded;
}
catch (Exception e)
{
_nodeScope.Dispose();
_logger.LogError(e, "Error while starting lightning node");
_nodeScope = null;
State = LightningNodeState.Error;
}
finally
{
_controlSemaphore.Release();
}
}
public async Task StopNode()
{
if (_nodeScope is null || State is not LightningNodeState.Loaded)
return;
await _controlSemaphore.WaitAsync();
try
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token);
cts.CancelAfter(5000);
await Node.StopAsync(cts.Token);
}
catch (Exception e)
{
_logger.LogError(e, "Error while stopping lightning node");
}
finally
{
_nodeScope?.Dispose();
_nodeScope = null;
_controlSemaphore.Release();
State = LightningNodeState.Stopped;
}
}
public async Task CleanseTask()
{
await StopNode();
if (_nodeScope is not null || State == LightningNodeState.NotConfigured) return;
await _controlSemaphore.WaitAsync();
try
{
await _onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
await using var context = await _dbContextFactory.CreateDbContextAsync();
context.LightningPayments.RemoveRange(context.LightningPayments);
context.LightningChannels.RemoveRange(context.LightningChannels);
context.Settings.RemoveRange(context.Settings.Where(s => new string[]{"ChannelManager","NetworkGraph","Score","lightningconfig"}.Contains(s.Key)));
await context.SaveChangesAsync();
}
finally
{
_controlSemaphore.Release();
State = LightningNodeState.NotConfigured;
}
}
public async Task Generate()
{
await _controlSemaphore.WaitAsync();
try
{
if (State != LightningNodeState.NotConfigured) return;
if (!IsHubConnected)
throw new InvalidOperationException("Cannot configure lightning node without BTCPay connection");
if (!IsOnchainConfigured)
throw new InvalidOperationException("Cannot configure lightning node without on-chain wallet configuration");
if (IsOnchainLightningDerivationConfigured)
throw new InvalidOperationException("On-chain wallet is already configured with a lightning derivation");
await _onChainWalletManager.AddDerivation(WalletDerivation.LightningScripts, "Lightning", null);
// await _onChainWalletManager.AddDerivation(WalletDerivation.SpendableOutputs, "Lightning Spendables", null);
State = LightningNodeState.WaitingForConnection;
}
finally
{
_controlSemaphore.Release();
}
}
private async Task OnConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) state)
{
if (IsHubConnected && State == LightningNodeState.WaitingForConnection)
{
State = LightningNodeState.Loading;
}
else if (_btcPayConnectionManager.ConnectionState == HubConnectionState.Disconnected && State is LightningNodeState.Loading or LightningNodeState.Loaded)
{
_ = StopNode();
}
}
private async Task OnChainWalletManagerOnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
{
if (e.New == OnChainWalletState.Loaded)
{
State = LightningNodeState.Loading;
}
}
private async Task OnStateChanged(object? sender, (LightningNodeState Old, LightningNodeState New) state)
{
LightningNodeState? newState = null;
try
{
switch (state.New)
{
case LightningNodeState.WaitingForConnection:
{
if (IsHubConnected)
newState = LightningNodeState.Loading;
break;
}
case LightningNodeState.Loading:
if (!IsHubConnected)
{
newState = LightningNodeState.WaitingForConnection;
break;
}
if (!IsOnchainConfigured || !IsOnchainLightningDerivationConfigured)
{
newState = LightningNodeState.NotConfigured;
break;
}
var result = await _btcPayConnectionManager.HubProxy!
.IdentifierActive(_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier, true)
.RunSync();
if (result)
{
await StartNode();
}
else
{
//TODO: Introduce a new state so that this node knows that another instance is active
newState = LightningNodeState.Error;
}
break;
case LightningNodeState.NotConfigured:
if (CanConfigureLightningNode)
{
await Generate();
}
break;
case LightningNodeState.Loaded:
await _controlSemaphore.WaitAsync();
_controlSemaphore.Release();
break;
// case LightningNodeState.Unloading:
// _nodeScope?.Dispose();
// State = _walletConfig is null
// ? LightningNodeState.NotConfigured
// : LightningNodeState.WaitingForConnection;
// break;
}
}
finally
{
if (newState is not null)
State = newState.Value;
}
}
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
{
State = LightningNodeState.Init;
StateChanged += OnStateChanged;
_btcPayConnectionManager.ConnectionChanged += OnConnectionChanged;
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
}
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
{
_btcPayConnectionManager.ConnectionChanged -= OnConnectionChanged;
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
StateChanged -= OnStateChanged;
_nodeScope?.Dispose();
}
}

View File

@ -1,515 +0,0 @@
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Scripting;
using OutPoint = NBitcoin.OutPoint;
using TxOut = NBitcoin.TxOut;
namespace BTCPayApp.Core.Attempt2;
public class OnChainWalletManager : BaseHostedService
{
public const string PaymentMethodId = "BTC-CHAIN";
private readonly IConfigProvider _configProvider;
private readonly BTCPayAppServerClient _btcPayAppServerClient;
private readonly BTCPayConnectionManager _btcPayConnectionManager;
private readonly ILogger<OnChainWalletManager> _logger;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly IMemoryCache _memoryCache;
private OnChainWalletState _state = OnChainWalletState.Init;
public WalletConfig? WalletConfig { get; private set; }
public Network? Network => WalletConfig is null ? null : Network.GetNetwork(WalletConfig.Network);
public OnChainWalletState State
{
get => _state;
private set
{
if (_state == value)
return;
var old = _state;
_state = value;
_logger.LogInformation("Wallet state changed: {State}", _state);
StateChanged?.Invoke(this, (old, value));
}
}
public event AsyncEventHandler<(OnChainWalletState Old, OnChainWalletState New)>? StateChanged;
public OnChainWalletManager(
IConfigProvider configProvider,
BTCPayAppServerClient btcPayAppServerClient,
BTCPayConnectionManager btcPayConnectionManager,
ILogger<OnChainWalletManager> logger,
IDbContextFactory<AppDbContext> dbContextFactory,
IMemoryCache memoryCache)
{
_configProvider = configProvider;
_btcPayAppServerClient = btcPayAppServerClient;
_btcPayConnectionManager = btcPayConnectionManager;
_logger = logger;
_dbContextFactory = dbContextFactory;
_memoryCache = memoryCache;
}
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
{
StateChanged += OnStateChanged;
_btcPayAppServerClient.OnNewBlock += OnNewBlock;
_btcPayAppServerClient.OnTransactionDetected += OnTransactionDetected;
_btcPayConnectionManager.ConnectionChanged += ConnectionChanged;
WalletConfig = await _configProvider.Get<WalletConfig>(WalletConfig.Key);
DetermineState();
if (IsHubConnected)
{
await Track();
_ = GetBestBlock();
State = OnChainWalletState.Loaded;
}
}
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
public bool IsConfigured => WalletConfig is not null;
private async Task OnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
{
if (e is { Old: OnChainWalletState.NotConfigured or OnChainWalletState.WaitingForConnection } && IsHubConnected && !IsConfigured)
{
await Generate();
}
if (e is {New: OnChainWalletState.Loaded} && IsConfigured)
{
await Track();
}
if (e.New is OnChainWalletState.Loading)
{
DetermineState();
}
}
public async Task Generate()
{
await _controlSemaphore.WaitAsync();
try
{
if (State != OnChainWalletState.NotConfigured || IsConfigured || !IsHubConnected)
{
throw new InvalidOperationException("Cannot generate wallet in current state");
}
var mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
var mainnet = _btcPayConnectionManager.ReportedNetwork == Network.Main;
var path = new KeyPath($"m/84'/{(mainnet ? "0" : "1")}'/0'");
var fingerprint = mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint();
var xpub = mnemonic.DeriveExtKey().Derive(path).Neuter().ToString(_btcPayConnectionManager.ReportedNetwork);
var walletConfig = new WalletConfig
{
Mnemonic = mnemonic.ToString(),
Network = _btcPayConnectionManager.ReportedNetwork.ToString(),
Derivations = new Dictionary<string, WalletDerivation>()
{
[WalletDerivation.NativeSegwit] = new WalletDerivation()
{
Name = "Native Segwit",
Descriptor = OutputDescriptor.AddChecksum(
$"wpkh([{fingerprint.ToString()}/{path}]{xpub}/0/*)")
}
}
};
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest()
{
Derivations = walletConfig.Derivations.ToDictionary(pair => pair.Key, pair => pair.Value.Descriptor)
}).RunSync();
foreach (var keyValuePair in result)
{
walletConfig.Derivations[keyValuePair.Key].Identifier = keyValuePair.Value;
}
await _configProvider.Set(WalletConfig.Key, walletConfig);
WalletConfig = walletConfig;
State = OnChainWalletState.Loaded;
}
finally
{
_controlSemaphore.Release();
}
}
public async Task AddDerivation(string key, string name, string? descriptor)
{
await _controlSemaphore.WaitAsync();
try
{
if (State != OnChainWalletState.Loaded || !IsConfigured || !IsHubConnected)
{
throw new InvalidOperationException("Cannot add deriv in current state");
}
if (WalletConfig.Derivations.ContainsKey(key))
throw new InvalidOperationException("Derivation already exists");
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest
{
Derivations = new Dictionary<string, string?>()
{
[key] = descriptor
}
}).RunSync();
WalletConfig.Derivations[key] = new WalletDerivation()
{
Name = name,
Descriptor = descriptor,
Identifier = result[key]
};
await _configProvider.Set(WalletConfig.Key, WalletConfig);
}
finally
{
_controlSemaphore.Release();
}
}
private async Task ConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) _)
{
DetermineState();
}
private void DetermineState()
{
if (IsHubConnected && IsConfigured)
State = OnChainWalletState.Loaded;
else if (!IsHubConnected)
State = OnChainWalletState.WaitingForConnection;
else if (!IsConfigured)
State = OnChainWalletState.NotConfigured;
}
private async Task Track()
{
if (!IsConfigured || !IsHubConnected)
return;
var identifiers = WalletConfig.Derivations.Select(pair => pair.Value.Identifier).ToArray();
var response = await _btcPayConnectionManager.HubProxy.Handshake(new AppHandshake
{
Identifiers = identifiers
}).RunSync();
var missing =
WalletConfig.Derivations.Where(pair => !response.IdentifiersAcknowledged.Contains(pair.Value.Identifier));
if (missing.Any())
{
_logger.LogWarning("Some identifiers that we had asked for BtcPayServer to track were not confirmed as being listened to. Tracking will be incomplete and functionality will critically fail.");
}
}
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
{
_btcPayAppServerClient.OnNewBlock -= OnNewBlock;
_btcPayAppServerClient.OnTransactionDetected -= OnTransactionDetected;
_btcPayConnectionManager.ConnectionChanged -= ConnectionChanged;
WalletConfig = null;
State = OnChainWalletState.Init;
}
private async Task OnTransactionDetected(object? sender, TransactionDetectedRequest transactionDetectedRequest)
{
}
private async Task OnNewBlock(object? sender, string e)
{
_memoryCache.Remove("bestblock");
_ = GetBestBlock();
}
public async Task<Script> DeriveScript(string derivation)
{
var identifier = WalletConfig?.Derivations[derivation].Identifier;
var addr = await _btcPayConnectionManager.HubProxy.DeriveScript(identifier).RunSync();
return Script.FromHex(addr);
}
public async Task<byte[]?> SignTransaction(byte[] psbtBytes)
{
var psbt = PSBT.Load(psbtBytes, Network);
psbt = await SignTransaction(psbt);
return psbt?.ToBytes();
}
public async Task<PSBT?> SignTransaction(PSBT psbt)
{
var identifiers = WalletConfig.Derivations.Select(derivation => derivation.Value.Identifier).ToArray();
var updated = await _btcPayConnectionManager.HubProxy.UpdatePsbt(identifiers, psbt.ToHex()).RunSync();
psbt = PSBT.Parse(updated, Network);
var rootKey =new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey();
foreach (var deriv in WalletConfig.Derivations.Values.Where(derivation => derivation.Descriptor is not null))
{
var data = deriv.Descriptor.ExtractFromDescriptor(Network);
if(data is null)
continue;
var accKey = rootKey.Derive(data.Value.Item2);
psbt = psbt.SignAll(data.Value.Item1.AsHDScriptPubKey(data.Value.Item3), accKey);
if(psbt.TryFinalize(out _))
break;
}
return psbt;
}
private static ICoin ToCoin(CoinResponse response)
{
var outpoint = OutPoint.Parse(response.Outpoint);
var scriptPubKey = Script.FromHex(response.Script);
var amount = Money.Coins(response.Value);
return new Coin(outpoint, new TxOut(amount, scriptPubKey));
}
// // public class SpendableOutputDescriptorCoin : Coin,ISignableCoin
// {
// public SpendableOutputDescriptorCoin(OutPoint fromOutpoint, TxOut fromTxOut, SpendableOutputDescriptor descriptor) : base(fromOutpoint, fromTxOut)
// {
// Descriptor = descriptor;
// }
//
// public SpendableOutputDescriptor Descriptor { get;}
// public async Task<PSBT> Sign(PSBT psbt)
// {
//
// UtilMethods.
// UtilMethods.SpendableOutputDescriptor_create_spendable_outputs_psbt(new SpendableOutputDescriptor[]{Descriptor}, )
// Descriptor.create_spendable_outpcreate_spendable_outputs_psbtuts_psbt
// switch (Descriptor)
// {
// case SpendableOutputDescriptor.SpendableOutputDescriptor_DelayedPaymentOutput spendableOutputDescriptorDelayedPaymentOutput:
// spendableOutputDescriptorDelayedPaymentOutput.delayed_payment_output.
// break;
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticOutput spendableOutputDescriptorStaticOutput:
// //ignore
// break;
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticPaymentOutput spendableOutputDescriptorStaticPaymentOutput:
// spendableOutputDescriptorStaticPaymentOutput.static_payment_output.psb
// break;
// default:
// throw new ArgumentOutOfRangeException(nameof(Descriptor));
// }
// }
// }
public class CoinWithKey : Coin,ISignableCoin
{
public Key Key { get; }
public CoinWithKey(OutPoint fromOutpoint, TxOut fromTxOut, Key key) : base(fromOutpoint, fromTxOut)
{
Key = key;
}
public async Task<PSBT> Sign(PSBT psbt)
{
return psbt.SignWithKeys(Key);
}
}
public interface ISignableCoin : ICoin
{
Task<PSBT> Sign(PSBT psbt);
}
public async Task<TxResp[]> GetTransactions()
{
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
var res= await _btcPayConnectionManager.HubProxy.GetTransactions(identifiersWhichWeCanDeriveKeysFor).RunSync();
return res.SelectMany(pair => pair.Value).OrderByDescending(resp => resp.Timestamp).ToArray();
}
public async Task<IEnumerable<ICoin>> GetUTXOS()
{
var identifiers = WalletConfig.Derivations.Values.Select(derivation => derivation.Identifier).ToArray();
var utxos = await _btcPayConnectionManager.HubProxy.GetUTXOs(identifiers).RunSync();
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
var result = new List<ICoin>();
var utxosThatWeCanDeriveKeysFor = utxos.Where(utxo => identifiersWhichWeCanDeriveKeysFor.Contains(utxo.Identifier)).ToArray();
foreach (var coin in utxosThatWeCanDeriveKeysFor)
{
var derivation =
WalletConfig.Derivations.Values.First(derivation => derivation.Identifier == coin.Identifier);
var data = derivation.Descriptor.ExtractFromDescriptor(Network);
if (data is null)
continue;
var coinKeyPath = KeyPath.Parse(coin.Path);
var key = new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey().Derive(data.Value.Item2.KeyPath)
.Derive(coinKeyPath).PrivateKey;
var c = ToCoin(coin);
result.Add(new CoinWithKey(c.Outpoint, c.TxOut, key));
}
// if (WalletConfig.Derivations.TryGetValue(WalletDerivation.SpendableOutputs, out var spendableOutputDerivation))
// {
//
// var spendableOutputUtxos = utxos.Where(response => response.Identifier == spendableOutputDerivation.Identifier).ToArray();
// await using var context = await _dbContextFactory.CreateDbContextAsync();
// var scipts = spendableOutputUtxos.Select(response => response.Script).Distinct();
// var spendableCoins = await context.SpendableCoins.Where(coin => scipts.Contains(coin.Script)).ToListAsync();
//
// foreach (var spendableOutputUtxo in spendableOutputUtxos)
// {
// var spendableCoin = spendableCoins.FirstOrDefault(coin => coin.Script == spendableOutputUtxo.Script);
// if (spendableCoin is null)
// continue;
// var coin = ToCoin(spendableOutputUtxo);
// var data = SpendableOutputDescriptor.read(spendableCoin.Data);
// if(data is Result_SpendableOutputDescriptorDecodeErrorZ.Result_SpendableOutputDescriptorDecodeErrorZ_OK ok)
// result.Add(new SpendableOutputDescriptorCoin(coin.Outpoint, coin.TxOut, ok.res));
// }
// }
return result;
}
public async Task<(NBitcoin.Transaction Tx, ICoin[] SpentCoins, NBitcoin.Script Change)> CreateTransaction(
List<TxOut> txOuts, FeeRate? feeRate, List<Coin> explicitIns = null)
{
var availableCoins = (await GetUTXOS()).ToList();
feeRate ??= await GetFeeRate(1);
//TODO: do not hardcode this constant
var changeScript = await DeriveScript(WalletDerivation.NativeSegwit);
var txBuilder = Network
.CreateTransactionBuilder()
.SetChange(changeScript)
.SendEstimatedFees(feeRate);
txBuilder = txOuts.Aggregate(txBuilder, (current, c) => current.Send(c.ScriptPubKey, c.Value));
txBuilder.SendAllRemainingToChange();
NBitcoin.Transaction? tx;
if (explicitIns?.Any() is true)
{
txBuilder.AddCoins(explicitIns.ToArray());
}
while (true)
{
try
{
tx = txBuilder.BuildTransaction(true);
return (tx, txBuilder.FindSpentCoins(tx), changeScript);
}
catch (NotEnoughFundsException e)
{
if (!availableCoins.Any())
throw;
var newCoin = availableCoins.First();
//TODO: switch to nuilding a psbt and signing with the ISignableCoin interface
if(newCoin is CoinWithKey newCoinWithKey)
{
txBuilder.AddCoins(newCoin);
txBuilder.AddKeys(newCoinWithKey.Key);
}
availableCoins.Remove(newCoin);
}
}
}
public async Task RemoveDerivation(params string[] key)
{
await _controlSemaphore.WaitAsync();
try
{
if (State != OnChainWalletState.Loaded || WalletConfig is null)
{
throw new InvalidOperationException("Cannot remove deriv in current state");
}
var updated = key.Aggregate(false, (current, k) => current || WalletConfig.Derivations.Remove(k));
if (updated)
await _configProvider.Set(WalletConfig.Key, WalletConfig);
}
finally
{
_controlSemaphore.Release();
}
}
public async Task<BestBlockResponse?> GetBestBlock()
{
var res = await _memoryCache.GetOrCreateAsync("bestblock", async entry =>
{
_logger.LogInformation("Getting best block");
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
try
{
return await _btcPayConnectionManager.HubProxy.GetBestBlock().RunSync();
}
catch(Exception e)
{
_logger.LogError(e, "Error getting best block");
return null;
}
finally
{
_logger.LogInformation("Got best block");
}
});
if (res is null)
{
_memoryCache.Remove("bestblock");
}
return res;
}
public async Task BroadcastTransaction(Transaction valueTx, CancellationToken cancellationToken = default)
{
await _btcPayConnectionManager.HubProxy.BroadcastTransaction(valueTx.ToHex()).RunSync();
}
public async Task<FeeRate> GetFeeRate(int blockTarget)
{
try
{
return await _memoryCache.GetOrCreateAsync($"feerate_{blockTarget}", async entry =>
{
_logger.LogInformation("Getting fee rate for block target {BlockTarget}", blockTarget);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
try
{
return new FeeRate(await _btcPayConnectionManager.HubProxy.GetFeeRate(blockTarget).RunSync());
}
finally
{
_logger.LogInformation("Got fee rate for block target {BlockTarget}", blockTarget);
}
});
}
catch (Exception e)
{
_logger.LogError(e, "Error getting fee rate, using hardcoded 100");
return new FeeRate(100m);
}
}
}
public enum OnChainWalletState
{
Init,
NotConfigured,
WaitingForConnection,
Loading,
Loaded
}

View File

@ -1,9 +0,0 @@
namespace BTCPayApp.Core.Attempt2;
public enum SetupState
{
Undetermined,
Pending,
Completed,
Failed
}

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,9 +1,7 @@
using System.Security.Claims;
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.AspNetRip;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Helpers;
using BTCPayServer.Abstractions.Constants;
using BTCPayApp.Core.Models;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
@ -13,67 +11,67 @@ using Microsoft.Extensions.Options;
namespace BTCPayApp.Core.Auth;
public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, IHostedService
public class AuthStateProvider(
IHttpClientFactory clientFactory,
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 BTCPayAccount? _account;
// TODO: Move _userInfo to state
private AppUserInfo? _userInfo;
private bool _refreshUserInfo;
private string? _currentStoreId;
private CancellationTokenSource? _pingCts;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly ClaimsPrincipal _unauthenticated = new(new ClaimsIdentity());
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
private readonly IAuthorizationService _authService;
private readonly IConfigProvider _config;
public BTCPayAccount? GetAccount() => _account;
public AppUserInfo? GetUserInfo() => _userInfo;
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 AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
public AuthStateProvider(
IConfigProvider config,
IAuthorizationService authService,
IOptionsMonitor<IdentityOptions> identityOptions)
public Task StartAsync(CancellationToken cancellationToken)
{
_config = config;
_authService = authService;
_identityOptions = identityOptions;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = PingOccasionally();
}
private async Task PingOccasionally()
{
while (_userInfo != null)
{
await GetAuthenticationStateAsync();
await Task.Delay(TimeSpan.FromSeconds(5));
}
_pingCts = new CancellationTokenSource();
_ = PingOccasionally(_pingCts.Token);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_pingCts?.Cancel();
return Task.CompletedTask;
}
public BTCPayAppClient GetClient(string? baseUri = null)
private async Task PingOccasionally(CancellationToken pingCtsToken)
{
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(_account?.BaseUri))
while (pingCtsToken.IsCancellationRequested is false)
{
await GetAuthenticationStateAsync();
await Task.Delay(TimeSpan.FromSeconds(5), pingCtsToken);
}
}
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
{
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(Account?.BaseUri))
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
var client = new BTCPayAppClient(baseUri ?? _account!.BaseUri);
if (string.IsNullOrEmpty(baseUri) && !string.IsNullOrEmpty(_account?.AccessToken) && !string.IsNullOrEmpty(_account.RefreshToken))
client.SetAccess(_account.AccessToken, _account.RefreshToken, _account.AccessExpiry.GetValueOrDefault());
client.AccessRefreshed += OnAccessRefresh;
return client;
token ??= Account?.ModeToken ?? Account?.OwnerToken;
return new BTCPayAppClient(baseUri ?? Account!.BaseUri, token, clientFactory.CreateClient());
}
public async Task<string?> GetEncryptionKey()
{
return await secureProvider.Get<string>("encryptionKey");
}
public async Task SetEncryptionKey(string value)
{
await secureProvider.Set("encryptionKey", value);
OnEncryptionKeyChanged?.Invoke(this, value);
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
@ -85,49 +83,56 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
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;
if (_userInfo == null && _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);
await FetchUserInfo(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 =>
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
if (_userInfo.Stores?.Any() is true)
claims.AddRange(_userInfo.Stores.Select(store =>
new Claim(store.Id, string.Join(',', store.Permissions))));
user = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationSchemes.GreenfieldBearer));
// update account user info
_account!.SetInfo(_userInfo.Email!, _userInfo.Name, _userInfo.ImageUrl);
await UpdateAccount(_account);
if (UserInfo.Roles?.Any() is true)
claims.AddRange(UserInfo.Roles.Select(role =>
new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
if (UserInfo.Stores?.Any() is true)
claims.AddRange(UserInfo.Stores.Select(store =>
new Claim(store.Id, string.Join(',', store.Permissions ?? []))));
if (hasOwnerToken && !hasModeToken)
claims.Add(new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, "DeviceOwner"));
user = new ClaimsPrincipal(new ClaimsIdentity(claims, "Greenfield"));
}
var res = new AuthenticationState(user);
if (AppUserInfo.Equals(oldUserInfo, _userInfo))
return res;
if (AppUserInfo.Equals(oldUserInfo, UserInfo)) return res;
OnUserInfoChanged?.Invoke(this, UserInfo);
if (Account != null && UserInfo != null)
await UpdateAccount(Account);
NotifyAuthenticationStateChanged(Task.FromResult(res));
return res;
}
catch
{
UserInfo = null;
return new AuthenticationState(user);
}
finally
@ -138,78 +143,78 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
public async Task<bool> CheckAuthenticated(bool refreshUser = false)
{
if (refreshUser) await FetchUserInfo();
if (refreshUser) _refreshUserInfo = true;
await GetAuthenticationStateAsync();
return _userInfo != null;
return UserInfo != null;
}
public async Task<bool> IsAuthorized(string policy, object? resource = null)
{
var authState = await GetAuthenticationStateAsync();
var result = await _authService.AuthorizeAsync(authState.User, resource, policy);
var result = await authService.AuthorizeAsync(authState.User, resource, policy);
return result.Succeeded;
}
public async Task Logout()
public async Task<FormResult> SetCurrentStoreId(string? storeId)
{
_userInfo = null;
_account!.ClearAccess();
await UpdateAccount(_account);
await SetCurrentAccount(null);
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);
}
public async Task<FormResult> SetCurrentStoreId(string storeId)
private async Task SetCurrentStore(AppUserStoreInfo? store)
{
var store = GetUserStore(storeId);
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
if (_currentStoreId == store?.Id) return;
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
string? message = null;
if (store != null)
store = await EnsureStorePos(store);
// create associated POS app if there is none
if (string.IsNullOrEmpty(store.PosAppId))
_currentStoreId = store?.Id;
var appConfig = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig();
appConfig.CurrentStoreId = _currentStoreId;
await configProvider.Set(BTCPayAppConfig.Key, appConfig, true);
OnStoreChanged?.Invoke(this, store);
}
public async Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false)
{
if (string.IsNullOrEmpty(store.PosAppId) || forceCreate is true)
{
try
{
var posConfig = new PointOfSaleAppRequest { AppName = store.Name, DefaultView = PosViewType.Light };
var app = await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
message = $"The Point of Sale called \"{app.AppName}\" has been created for use with the app.";
await FetchUserInfo();
await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
await CheckAuthenticated(true);
store = GetUserStore(store.Id)!;
}
catch (Exception e)
catch (Exception ex)
{
return new FormResult(false, e.Message);
// ignored
}
}
_account!.CurrentStoreId = storeId;
await UpdateAccount(_account);
OnAfterStoreChange?.Invoke(this, store);
return new FormResult(true, string.IsNullOrEmpty(message) ? null : [message]);
return store;
}
public async Task UnsetCurrentStore()
private AppUserStoreInfo? GetUserStore(string storeId)
{
_account!.CurrentStoreId = null;
await UpdateAccount(_account);
}
public AppUserStoreInfo? GetUserStore(string storeId)
{
return _userInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
}
public AppUserStoreInfo? GetCurrentStore()
{
var storeId = _account?.CurrentStoreId;
return string.IsNullOrEmpty(storeId) ? null : GetUserStore(storeId);
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
@ -220,8 +225,8 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
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.";
@ -232,9 +237,22 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
: " 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);
}
}
@ -248,11 +266,10 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
};
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)
@ -261,16 +278,36 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
}
}
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)
@ -281,31 +318,31 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
public async Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default)
{
var payload = new SignupRequest
var payload = new CreateApplicationUserRequest
{
Email = email,
Password = password
};
try
{
var expiryOffset = DateTimeOffset.Now;
var response = await GetClient(serverUrl).RegisterUser(payload, cancellation.GetValueOrDefault());
var account = new BTCPayAccount(serverUrl, email);
var message = "Account created.";
if (response.ContainsKey("accessToken"))
{
var access = response.ToObject<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<SignupResult>();
if (signup.RequiresConfirmedEmail)
var signup = response.ToObject<ApplicationUserData>();
if (signup?.RequiresEmailConfirmation is true)
message += " Please confirm your email.";
if (signup.RequiresUserApproval)
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)
@ -325,7 +362,14 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
try
{
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
var response = await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
if (response?.ContainsKey("accessToken") is true)
{
var access = response.ToObject<AuthenticationResponse>();
var account = new BTCPayAccount(serverUrl, email, access!.AccessToken);
await SetAccount(account);
}
return new FormResult(true, isForgotStep
? "You should have received an email with a password reset code."
: "Your password has been reset.");
@ -354,32 +398,23 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
}
}
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);
_userInfo!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
return new FormResult<ApplicationUserData>(true, "Your account info has been changed.", userData);
}
catch (Exception e)
{
return new FormResult<ApplicationUserData>(false, e.Message, null);
}
}
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)
@ -388,66 +423,53 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
}
}
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 _config.List(prefix)).ToArray();
var accounts = new List<BTCPayAccount>();
foreach (var key in keys)
var payload = new LoginRequest
{
var account = await _config.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 _config.Set(GetKey(account.Id), account);
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 _config.Set<BTCPayAccount>(GetKey(account.Id), null);
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 _config.Get<BTCPayAccount>(GetKey(accountId));
return account ?? new BTCPayAccount(serverUrl, email);
}
var storeId = CurrentStore?.Id;
private async Task<BTCPayAccount?> GetCurrentAccount()
{
var accountId = await _config.Get<string>(CurrentAccountKey);
if (string.IsNullOrEmpty(accountId)) return null;
return await _config.Get<BTCPayAccount>(GetKey(accountId));
}
private async Task SetCurrentAccount(BTCPayAccount? account)
{
OnBeforeAccountChange?.Invoke(this, _account);
if (account != null) await UpdateAccount(account);
await _config.Set(CurrentAccountKey, account?.Id);
_account = account;
_userInfo = null;
await UpdateAccount(account);
Account = account;
UserInfo = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
OnAfterAccountChange?.Invoke(this, _account);
}
private async Task FetchUserInfo(CancellationToken cancellationToken = default)
{
_userInfo = await GetClient().GetUserInfo(cancellationToken);
if (!string.IsNullOrEmpty(storeId)) await SetCurrentStoreId(storeId);
}
}

View File

@ -0,0 +1,76 @@
using BTCPayApp.Core.Helpers;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BTCPayApp.Core.Auth;
public class AuthorizationHandler(IOptionsMonitor<IdentityOptions> identityOptions) : AuthorizationHandler<PolicyRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
{
if (context.User.Identity?.AuthenticationType != "Greenfield")
return Task.CompletedTask;
var userId = context.User.Claims.FirstOrDefault(c => c.Type == identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType)?.Value;
if (string.IsNullOrEmpty(userId))
return Task.CompletedTask;
var permissionSet = new PermissionSet();
var success = false;
var isAdmin = context.User.IsInRole("ServerAdmin");
var isOwner = context.User.IsInRole("DeviceOwner");
var storeId = context.Resource as string;
var policy = requirement.Policy;
var requiredUnscoped = false;
if (policy.EndsWith(':'))
{
policy = policy[..^1];
requiredUnscoped = true;
storeId = null;
}
if (!string.IsNullOrEmpty(storeId))
{
var permissions = context.User.Claims.FirstOrDefault(c => c.Type == storeId)?.Value;
if (!string.IsNullOrEmpty(permissions))
{
permissionSet = new PermissionSet(permissions.Split(',')
.Select(s => Permission.TryParse(s, out var permission) ? permission : null)
.Where(s => s != null).ToArray());
}
}
if (Policies.IsServerPolicy(policy) && isAdmin)
{
success = true;
}
else if (Policies.IsUserPolicy(policy) && !string.IsNullOrEmpty(userId))
{
success = true;
}
else if (Policies.IsStorePolicy(policy) && !string.IsNullOrEmpty(storeId))
{
if (!success && permissionSet.Contains(policy, storeId))
{
success = true;
}
if (!success && requiredUnscoped && string.IsNullOrEmpty(storeId))
{
success = true;
}
}
else if (Policies.IsPluginPolicy(policy) && policy.StartsWith("btcpay.plugin.app"))
{
success = isOwner;
}
if (success)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}

View File

@ -1,34 +1,33 @@
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.Models;
using BTCPayServer.Client.Models;
namespace BTCPayApp.Core.Auth;
public interface IAccountManager
{
public BTCPayAccount? 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<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,11 +0,0 @@
namespace BTCPayApp.Core.Auth;
public class Invoice
{
public string? Id { get; set; }
public string? OrderId { get; set; }
public string? Status { get; set; }
public DateTimeOffset Date { get; set; }
public string? Currency { get; set; }
public decimal Amount { get; set; }
}

View File

@ -1,10 +0,0 @@
namespace BTCPayApp.Core.Auth;
public class Notification
{
public string? Id { get; set; }
public string? Type { get; set; }
public DateTimeOffset Created { get; set; }
public string? Body { get; set; }
public bool Seen { get; set; }
}

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,51 +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>
<ItemGroup>
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.6" />
<PackageReference Include="org.ldk" Version="0.0.123" />
<PackageReference Include="TypedSignalR.Client" Version="3.5.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\submodules\btcpayserver\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<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,129 +1,22 @@
using System.Net;
using System.Globalization;
using System.Net.Http.Headers;
using System.Web;
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.AspNetRip;
using BTCPayApp.Core.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using AccessTokenResponse = BTCPayApp.Core.AspNetRip.AccessTokenResponse;
using ProblemDetails = BTCPayApp.Core.AspNetRip.ProblemDetails;
using RefreshRequest = BTCPayApp.Core.AspNetRip.RefreshRequest;
namespace BTCPayApp.Core;
public class BTCPayAppClient(string baseUri) : BTCPayServerClient(new Uri(baseUri))
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");
if (!string.IsNullOrEmpty(AccessToken))
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
req.Headers.Add("Accept", "application/json");
return req;
}
protected override async Task<T> HandleResponse<T>(HttpResponseMessage res)
{
if (res is { IsSuccessStatusCode: false })
{
var req = res.RequestMessage;
if (res.StatusCode == HttpStatusCode.Unauthorized && !string.IsNullOrEmpty(RefreshToken))
{
// try refresh and recurse if the token could be renewed
var uri = req!.RequestUri;
var path = uri!.AbsolutePath;
if (!path.EndsWith(RefreshPath))
{
var (refresh, _) = await RefreshAccess(RefreshToken);
if (refresh != null)
{
if (req.Content is not null)
{
var content = await req.Content.ReadAsStringAsync();
var payload = JsonConvert.DeserializeObject<T>(content);
return await SendHttpRequest<T>(path, bodyPayload: payload, method: req.Method);
}
var query = HttpUtility.ParseQueryString(uri.Query);
var queryPayload = query.HasKeys() ? query.AllKeys.ToDictionary(k => k, k => query[k]) : null;
return await SendHttpRequest<T>(path, queryPayload, method: req.Method);
}
}
ClearAccess();
}
else
{
// try parsing as ProblemDetails
try
{
var content = await res.Content.ReadAsStringAsync();
var err = JsonConvert.DeserializeObject<ProblemDetails>(content);
if (err?.Status != null && !string.IsNullOrEmpty(err.Detail))
{
var error = new GreenfieldAPIError("unauthorized", err.Detail);
throw new GreenfieldAPIException(err.Status.Value, error);
}
}
catch (JsonSerializationException e)
{
// ignored
}
}
}
return await base.HandleResponse<T>(res);
}
private AccessTokenResult HandleAccessTokenResponse(AccessTokenResponse response, DateTimeOffset expiryOffset)
{
var expiry = expiryOffset + TimeSpan.FromSeconds(response.ExpiresIn);
SetAccess(response.AccessToken, response.RefreshToken, expiry);
return new AccessTokenResult(response.AccessToken, response.RefreshToken, expiry);
}
public async Task<(AccessTokenResult? success, string? errorCode)> RefreshAccess(string? refreshToken = null, CancellationToken? cancellation = default)
{
var token = refreshToken ?? RefreshToken;
if (string.IsNullOrEmpty(token))
throw new ArgumentException("No refresh token present or provided.", nameof(refreshToken));
var payload = new RefreshRequest { RefreshToken = token };
var now = DateTimeOffset.Now;
try
{
var tokenResponse = await SendHttpRequest<AccessTokenResponse>(RefreshPath, bodyPayload: payload, method: HttpMethod.Post);
var res = HandleAccessTokenResponse(tokenResponse, now);
AccessRefreshed?.Invoke(this, res);
return (res, null);
}
catch (Exception e)
{
return (null, e.Message);
}
}
public async Task<AppInstanceInfo?> GetInstanceInfo(CancellationToken cancellation = default)
{
return await SendHttpRequest<AppInstanceInfo>("btcpayapp/instance", null, HttpMethod.Get, cancellation);
@ -139,30 +32,66 @@ public class BTCPayAppClient(string baseUri) : BTCPayServerClient(new Uri(baseUr
return await SendHttpRequest<CreateStoreData>("btcpayapp/create-store", null, HttpMethod.Get, cancellation);
}
public async Task<JObject> RegisterUser(SignupRequest payload, CancellationToken cancellation)
public async Task<JObject> RegisterUser(CreateApplicationUserRequest payload, CancellationToken cancellation = default)
{
return await SendHttpRequest<JObject>("btcpayapp/register", payload, HttpMethod.Post, cancellation);
}
public async Task<AccessTokenResponse> Login(LoginRequest payload, CancellationToken cancellation)
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)
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<AcceptInviteResult> AcceptInvite(AcceptInviteRequest payload, CancellationToken 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)
{
return await SendHttpRequest<AcceptInviteResult>("btcpayapp/accept-invite", payload, HttpMethod.Post, cancellation);
}
public async Task ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation)
public async Task<JObject?> ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation = default)
{
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
var path = isForgotStep ? "btcpayapp/forgot-password" : "btcpayapp/reset-password";
await SendHttpRequest<EmptyResult>(path, payload, HttpMethod.Post, cancellation);
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
}
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
{
var query = new Dictionary<string, object>();
if (req.DiscountPercent != null) query.Add("discount", req.DiscountPercent.Value.ToString(CultureInfo.InvariantCulture));
if (req.Tip != null) query.Add("tip", req.Tip.Value.ToString(CultureInfo.InvariantCulture));
if (req.PosData != null) query.Add("posData", req.PosData);
return await SendHttpRequest<JObject?>($"apps/{req.AppId}/pos/light", query, HttpMethod.Post, cancellation);
}
public async Task<string> SubmitLNURLWithdrawForInvoice(SubmitLnUrlRequest req, CancellationToken cancellation = default)
{
return await SendHttpRequest<string>($"plugins/NFC", req, HttpMethod.Post, cancellation);
}
public virtual async Task<T> UploadFileRequest<T>(string apiPath, StreamContent fileContent, string fileName, string mimeType, CancellationToken token = default)
{
using MultipartFormDataContent multipartContent = new();
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
multipartContent.Add(fileContent, "file", fileName);
var req = CreateHttpRequest(apiPath, null, HttpMethod.Post);
req.Content = multipartContent;
using var resp = await _httpClient.SendAsync(req, token);
return await HandleResponse<T>(resp);
}
}

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

@ -0,0 +1,59 @@
using BTCPayApp.Core.Data;
using BTCPayApp.Core.LDK;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.BTCPayServer;
public static class AppToServerHelper
{
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
{
return new LightningInvoice
{
Id = lightningPayment.PaymentHash?.ToString(),
Amount = lightningPayment.Value,
PaymentHash = lightningPayment.PaymentHash?.ToString(),
Preimage = lightningPayment.Preimage,
ExpiresAt = lightningPayment.AdditionalData[PaymentsManager.LightningPaymentExpiryKey].GetDateTimeOffset(),
PaidAt = lightningPayment.Status == LightningPaymentStatus.Complete
? DateTimeOffset.UtcNow
: null, //TODO: store these in ln payment
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
Status = lightningPayment.Status == LightningPaymentStatus.Complete
? LightningInvoiceStatus.Paid
: lightningPayment.PaymentRequest?.ExpiryDate < DateTimeOffset.UtcNow
? LightningInvoiceStatus.Expired
: LightningInvoiceStatus.Unpaid,
AmountReceived = lightningPayment.Status == LightningPaymentStatus.Complete? lightningPayment.Value: null
};
}
public static LightningPayment ToPayment(this AppLightningPayment lightningPayment)
{
return new LightningPayment
{
Id = lightningPayment.PaymentHash?.ToString(),
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
PaymentHash = lightningPayment.PaymentHash?.ToString(),
Preimage = lightningPayment.Preimage,
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
Status = lightningPayment.Status,
Fee = lightningPayment.AdditionalData.TryGetValue("feePaid", out var feePaid) ? LightMoney.MilliSatoshis((long)feePaid.GetInt64()) : null,
CreatedAt = lightningPayment.Timestamp
};
}
public static async Task<List<LightningPayment>> ToPayments(this Task<List<AppLightningPayment>> appLightningPayments)
{
var result = await appLightningPayments;
return result.Select(ToPayment).ToList();
}
public static async Task<List<LightningInvoice>> ToInvoices(this Task<List<AppLightningPayment>> appLightningPayments)
{
var result = await appLightningPayments;
return result.Select(ToInvoice).ToList();
}
}

View File

@ -0,0 +1,237 @@
using System.Text;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayApp.Core.Wallet;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Crypto;
using org.ldk.structs;
namespace BTCPayApp.Core.BTCPayServer;
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServiceProvider _serviceProvider)
: IBTCPayAppHubClient
{
public event AsyncEventHandler<string>? OnNewBlock;
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
public event AsyncEventHandler<string>? OnNotifyNetwork;
public event AsyncEventHandler<string>? OnServerNodeInfo;
public event AsyncEventHandler<long?>? OnMasterUpdated;
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
private LDKNode? Node => _serviceProvider.GetRequiredService<LightningNodeManager>().Node;
private PaymentsManager? PaymentsManager => Node?.PaymentsManager;
private LightningAPIKeyManager? ApiKeyManager => Node?.ApiKeyManager;
public async Task NotifyServerEvent(ServerEvent ev)
{
_logger.LogInformation("NotifyServerEvent: {Event}", ev.ToString());
if (OnNotifyServerEvent is null) return;
await OnNotifyServerEvent.Invoke(this, ev);
}
public async Task NotifyNetwork(string network)
{
_logger.LogInformation("NotifyNetwork: {Network}", network);
if (OnNotifyNetwork is null) return;
await OnNotifyNetwork.Invoke(this, network);
}
public async Task NotifyServerNode(string nodeInfo)
{
_logger.LogInformation("NotifyServerNode: {NodeInfo}", nodeInfo);
if (OnServerNodeInfo is null) return;
await OnServerNodeInfo.Invoke(this, nodeInfo);
}
public async Task TransactionDetected(TransactionDetectedRequest request)
{
_logger.LogInformation("OnTransactionDetected: {TxId}", request.TxId);
if (OnTransactionDetected is null) return;
await OnTransactionDetected.Invoke(this, request);
}
public async Task NewBlock(string block)
{
_logger.LogInformation("NewBlock: {Block}", block);
if (OnNewBlock is null) return;
await OnNewBlock.Invoke(this, block);
}
public async Task StartListen(string key)
{
await AssertPermission(key, APIKeyPermission.Read);
_serviceProvider
.GetRequiredService<LightningNodeManager>().Node?
.GetServiceProvider()
.GetRequiredService<BTCPayPaymentsNotifier>()
.StartListen();
}
private async Task AssertPermission(string key, APIKeyPermission permission)
{
if (ApiKeyManager is null)
throw new HubException("Api Key Manager not available");
if (!await ApiKeyManager.CheckPermission(key, permission))
throw new HubException("Permission denied");
}
public async Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest)
{
await AssertPermission(key, APIKeyPermission.Read);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
false);
return (await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
createLightningInvoiceRequest.Expiry, descHash)).ToInvoice();
}
public async Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash)
{
await AssertPermission(key, APIKeyPermission.Read);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
var invoices = await PaymentsManager.List(payments =>
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
return invoices.FirstOrDefault()?.ToInvoice();
}
public async Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash)
{
await AssertPermission(key, APIKeyPermission.Read);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
var invoices = await PaymentsManager.List(payments =>
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
return invoices.FirstOrDefault()?.ToPayment();
}
public async Task CancelInvoice(string key, uint256 paymentHash)
{
await AssertPermission(key, APIKeyPermission.Write);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
await PaymentsManager.CancelInbound(paymentHash);
}
public async Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request)
{
await AssertPermission(key, APIKeyPermission.Read);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound))
.ToPayments();
}
public async Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request)
{
await AssertPermission(key, APIKeyPermission.Read);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound)).ToInvoices();
}
public async Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi)
{
await AssertPermission(key, APIKeyPermission.Write);
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
var config = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetConfig();
var network = config?.NBitcoinNetwork;
if (network is null) throw new HubException("Network info not available");
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
try
{
var result = await PaymentsManager.PayInvoice(bolt,
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
return new PayResponse
{
Result = result.Status switch
{
LightningPaymentStatus.Unknown => PayResult.Unknown,
LightningPaymentStatus.Pending => PayResult.Unknown,
LightningPaymentStatus.Complete => PayResult.Ok,
LightningPaymentStatus.Failed => PayResult.Error,
_ => throw new ArgumentOutOfRangeException()
},
Details = new PayDetails
{
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
Status = result.Status
}
};
}
catch (Exception e)
{
_logger.LogError(e, "Error paying invoice");
return new PayResponse(PayResult.Error, e.Message);
}
}
public Task MasterUpdated(long? deviceIdentifier)
{
_logger.LogInformation("MasterUpdated: {DeviceIdentifier}", deviceIdentifier);
OnMasterUpdated?.Invoke(this, deviceIdentifier);
return Task.CompletedTask;
}
public async Task<LightningNodeInformation> GetLightningNodeInfo(string key)
{
await AssertPermission(key, APIKeyPermission.Read);
if (Node is null) throw new HubException("Lightning Node not available");
var config = await Node.GetConfig();
var peers = await Node.GetPeers();
var chans = await Node.GetChannels() ?? [];
var channels = chans
.Where(channel => channel.channelDetails is not null)
.Select(channel => channel.channelDetails)
.OfType<ChannelDetails>()
.ToArray();
var bb = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetBestBlock();
return new LightningNodeInformation
{
Alias = config.Alias,
Color = config.Color,
Version = "preprepreprealpha",
BlockHeight = bb?.BlockHeight ?? 0,
PeersCount = peers.Length,
ActiveChannelsCount = channels.Count(channel => channel.get_is_usable()),
PendingChannelsCount = channels.Count(channel => !channel.get_is_usable() && !channel.get_is_channel_ready()),
InactiveChannelsCount = channels.Count(channel => !channel.get_is_usable() && channel.get_is_channel_ready())
};
}
public async Task<LightningNodeBalance> GetLightningBalance(string key)
{
await AssertPermission(key, APIKeyPermission.Read);
if (Node is null) throw new HubException("Lightning Node not available");
var chans = await Node.GetChannels() ?? [];
var channels = chans
.Where(channel => channel.channelDetails is not null)
.Select(channel => channel.channelDetails)
.OfType<ChannelDetails>()
.ToArray();
var balances = Node.ClaimableBalances;
var closing = balances
.Where(b => b is Balance.Balance_ClaimableAwaitingConfirmations)
.ToArray();
return new LightningNodeBalance
{
OffchainBalance = new OffchainBalance
{
Local = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_outbound_capacity_msat())),
Remote = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_inbound_capacity_msat())),
Closing = LightMoney.Satoshis(closing.Sum(balance => balance.claimable_amount_satoshis()))
}
};
}
}

View File

@ -0,0 +1,389 @@
using System.Net;
using System.Net.WebSockets;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Backup;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using TypedSignalR.Client;
namespace BTCPayApp.Core.BTCPayServer;
public class BTCPayConnectionManager(
IServiceProvider serviceProvider,
IAccountManager accountManager,
AuthenticationStateProvider authStateProvider,
ILogger<BTCPayConnectionManager> logger,
BTCPayAppServerClient btcPayAppServerClient,
IBTCPayAppHubClient btcPayAppServerClientInterface,
ConfigProvider configProvider,
SyncService syncService)
: BaseHostedService(logger), IHubConnectionObserver
{
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
private CancellationTokenSource _cts = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private IDisposable? _subscription;
private IBTCPayAppHubServer? _hubProxy;
public IBTCPayAppHubServer? HubProxy
{
get => Connection?.State == HubConnectionState.Connected ? _hubProxy : null;
private set => _hubProxy = value;
}
private HubConnection? Connection { get; set; }
public Network? ReportedNetwork { get; private set; }
public string? ReportedNodeInfo { get; set; }
private bool ForceSlaveMode { get; set; }
public bool RunningInBackground { get; set; }
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
public BTCPayConnectionState ConnectionState
{
get => _connectionState;
private set
{
_lock.Wait();
try
{
if (_connectionState == value) return;
var old = _connectionState;
_connectionState = value;
logger.LogInformation("Connection state changed{BgInfo}: {Old} -> {ConnectionState}", BgInfo, old, _connectionState);
ConnectionChanged?.Invoke(this, (old, _connectionState));
}
finally
{
_lock.Release();
}
}
}
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
ConnectionChanged += OnConnectionChanged;
authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
btcPayAppServerClient.OnMasterUpdated += OnMasterUpdated;
accountManager.OnEncryptionKeyChanged += OnEncryptionKeyChanged;
await OnConnectionChanged(this, (BTCPayConnectionState.Init, BTCPayConnectionState.Init));
}
private async Task OnMasterUpdated(object? sender, long? masterId)
{
await WrapInLock(async () =>
{
if (_cts.IsCancellationRequested)
return;
var deviceId = await configProvider.GetDeviceIdentifier();
if (masterId is null && ConnectionState == BTCPayConnectionState.ConnectedAsSecondary && !ForceSlaveMode)
{
logger.LogInformation("OnMasterUpdated{BgInfo}: Syncing slave {DeviceId}", BgInfo, deviceId);
ConnectionState = BTCPayConnectionState.Syncing;
}
else if (deviceId == masterId)
{
logger.LogInformation("OnMasterUpdated{BgInfo}: Setting master to {DeviceId}", BgInfo, deviceId);
ConnectionState = BTCPayConnectionState.ConnectedAsPrimary;
}
else if (ConnectionState == BTCPayConnectionState.ConnectedAsPrimary && masterId != deviceId)
{
logger.LogInformation("OnMasterUpdated{BgInfo}: New master {MasterId} - Device: {DeviceId}", BgInfo, masterId, deviceId);
ConnectionState = BTCPayConnectionState.Syncing;
}
}, _cts.Token);
}
private async Task OnEncryptionKeyChanged(object? sender, string encryptionKey)
{
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
await WrapInLock(async () =>
{
if (_connectionState == BTCPayConnectionState.WaitingForEncryptionKey)
{
ConnectionState = BTCPayConnectionState.Syncing;
}
}, _cts.Token);
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
{
var deviceIdentifier = await configProvider.GetDeviceIdentifier();
var newState = e.New;
try
{
var account = accountManager.Account;
switch (e.New)
{
case BTCPayConnectionState.Init:
newState = BTCPayConnectionState.WaitingForAuth;
break;
case BTCPayConnectionState.WaitingForAuth:
await syncService.StopSync();
if (account is not null && await accountManager.CheckAuthenticated())
{
newState = BTCPayConnectionState.Connecting;
}
break;
case BTCPayConnectionState.Connecting:
if (account is null)
{
newState = BTCPayConnectionState.WaitingForAuth;
break;
}
await Kill();
var url = new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString();
var connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol(options =>
{
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(options.PayloadSerializerSettings);
options.PayloadSerializerSettings.Converters.Add(new global::BTCPayServer.Lightning.JsonConverters.LightMoneyJsonConverter());
})
.WithUrl(url, options =>
{
options.AccessTokenProvider = () =>
Task.FromResult(accountManager.Account?.OwnerToken);
options.HttpMessageHandlerFactory = serviceProvider
.GetService<Func<HttpMessageHandler, HttpMessageHandler>>();
options.WebSocketConfiguration =
serviceProvider.GetService<Action<ClientWebSocketOptions>>();
})
.Build();
_subscription = connection.Register(btcPayAppServerClientInterface);
HubProxy = new ExceptionWrappedHubProxy(connection, logger);
if (connection.State == HubConnectionState.Disconnected)
{
try
{
connection.Closed += OnClosed;
connection.Reconnected += OnReconnected;
connection.Reconnecting += OnReconnecting;
await connection.StartAsync();
}
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
{
await accountManager.Logout();
logger.LogInformation("Signed out user because of unauthorized response{BgInfo}", BgInfo);
}
catch (Exception ex)
{
await Task.Delay(500);
if (ex is not TaskCanceledException)
logger.LogError("Error while connecting to hub{BgInfo}: {Message}", BgInfo, ex.Message);
}
}
Connection = connection;
newState = Connection.State switch
{
HubConnectionState.Connected => BTCPayConnectionState.Syncing,
HubConnectionState.Connecting => BTCPayConnectionState.Connecting,
_ => BTCPayConnectionState.WaitingForAuth
};
break;
case BTCPayConnectionState.Syncing:
await syncService.StopSync();
if (await syncService.EncryptionKeyRequiresImport())
{
newState = BTCPayConnectionState.WaitingForEncryptionKey;
logger.LogWarning(
"Existing state found but encryption key is missing, waiting until key is provided");
}
else
{
//check if we are the master previously to process outbox items
var masterDevice = await HubProxy!.GetCurrentMaster();
if (deviceIdentifier == masterDevice)
{
logger.LogInformation("Syncing master to remote{BgInfo}: {DeviceId}", BgInfo, deviceIdentifier);
await syncService.SyncToRemote(CancellationToken.None);
}
else
{
logger.LogInformation("Syncing to local{BgInfo}. Master: {MasterId} - Device: {DeviceId}", BgInfo, masterDevice, deviceIdentifier);
await syncService.SyncToLocal();
}
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;
var config = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key);
if (!string.IsNullOrEmpty(config?.CurrentStoreId))
{
await accountManager.SetCurrentStoreId(config.CurrentStoreId);
}
}
break;
case BTCPayConnectionState.ConnectedFinishedInitialSync:
if (ForceSlaveMode)
{
await HubProxy!.DeviceMasterSignal(deviceIdentifier, false);
ForceSlaveMode = false;
newState = BTCPayConnectionState.ConnectedAsSecondary;
}
else if (!await HubProxy!.DeviceMasterSignal(deviceIdentifier, true))
{
newState = BTCPayConnectionState.ConnectedAsSecondary;
}
break;
case BTCPayConnectionState.ConnectedAsPrimary:
await syncService.StartSync(false);
break;
case BTCPayConnectionState.ConnectedAsSecondary:
await syncService.StartSync(true);
break;
case BTCPayConnectionState.Disconnected:
newState = BTCPayConnectionState.WaitingForAuth;
break;
}
}
catch (System.Security.Cryptography.CryptographicException ex) when (newState is BTCPayConnectionState.Syncing or BTCPayConnectionState.Connecting)
{
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
newState = BTCPayConnectionState.WaitingForEncryptionKey;
}
catch (Exception ex)
{
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
throw;
}
finally
{
_ = Task.Run(() => ConnectionState = newState);
}
}
private Task OnServerNodeInfo(object? sender, string? e)
{
ReportedNodeInfo = e;
return Task.CompletedTask;
}
private Task OnNotifyServerEvent(object? sender, ServerEvent e)
{
logger.LogInformation("OnNotifyServerEvent{BgInfo}: {Type} - {Details}", BgInfo, e.Type, e.ToString());
return Task.CompletedTask;
}
private Task OnNotifyNetwork(object? sender, string e)
{
ReportedNetwork = Network.GetNetwork(e);
return Task.CompletedTask;
}
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
{
await WrapInLock(async () =>
{
try
{
await task;
var authState = await accountManager.CheckAuthenticated();
if (ConnectionState == BTCPayConnectionState.WaitingForAuth && authState)
{
ConnectionState = BTCPayConnectionState.Connecting;
}
else if (ConnectionState > BTCPayConnectionState.WaitingForAuth && !authState)
{
ConnectionState = BTCPayConnectionState.WaitingForAuth;
}
}
catch (Exception e)
{
logger.LogError(e, "Error while handling authentication state change{BgInfo}", BgInfo);
}
}, _cts.Token);
}
private async Task Kill()
{
if (Connection is not null)
{
logger.LogWarning("Killing connection{BgInfo}", BgInfo);
}
var conn = Connection;
Connection = null;
if (conn is not null)
{
conn.Closed -= OnClosed;
conn.Reconnected -= OnReconnected;
conn.Reconnecting -= OnReconnecting;
await conn.StopAsync();
}
_subscription?.Dispose();
_subscription = null;
HubProxy = null;
await syncService.StopSync();
}
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
{
await _cts.CancelAsync();
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
{
var deviceId = await configProvider.GetDeviceIdentifier();
logger.LogInformation("Sending device master signal to turn off {DeviceId}{BgInfo}", deviceId, BgInfo);
await syncService.StopSync();
await syncService.SyncToRemote(CancellationToken.None);
if (HubProxy is not null)
{
await HubProxy.DeviceMasterSignal(deviceId, false);
}
}
await Kill();
authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
btcPayAppServerClient.OnNotifyNetwork -= OnNotifyNetwork;
accountManager.OnEncryptionKeyChanged -= OnEncryptionKeyChanged;
ConnectionChanged -= OnConnectionChanged;
}
public Task OnClosed(Exception? ex)
{
logger.LogError("Hub connection closed{BgInfo}: {Message}", BgInfo, ex?.Message);
if (Connection?.State == HubConnectionState.Disconnected && ConnectionState != BTCPayConnectionState.Connecting)
{
ConnectionState = BTCPayConnectionState.Disconnected;
}
return Task.CompletedTask;
}
public Task OnReconnected(string? connectionId)
{
logger.LogInformation("Hub connection reconnected{BgInfo}", BgInfo);
ConnectionState = BTCPayConnectionState.Syncing;
return Task.CompletedTask;
}
public Task OnReconnecting(Exception? ex)
{
logger.LogWarning("Hub connection reconnecting{BgInfo}: {Message}", BgInfo, ex?.Message);
ConnectionState = BTCPayConnectionState.Connecting;
return Task.CompletedTask;
}
public async Task SwitchToSecondary()
{
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
{
ForceSlaveMode = true;
var deviceId = await configProvider.GetDeviceIdentifier();
logger.LogInformation("Sending device master signal to turn off {DeviceId}", deviceId);
await syncService.StopSync();
await syncService.SyncToRemote(CancellationToken.None);
await HubProxy!.DeviceMasterSignal(deviceId, false);
}
}
private string BgInfo => RunningInBackground ? " (in background mode)" : string.Empty;
}

View File

@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.BTCPayServer;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum BTCPayConnectionState
{
Init,
Disconnected,
WaitingForAuth,
Connecting,
Syncing,
WaitingForEncryptionKey,
ConnectedAsPrimary,
ConnectedAsSecondary,
ConnectedFinishedInitialSync
}

View File

@ -0,0 +1,43 @@
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
namespace BTCPayApp.Core.BTCPayServer;
public class BTCPayPaymentsNotifier(
PaymentsManager paymentsManager,
BTCPayConnectionManager connectionManager)
: IScopedHostedService
{
private bool _listening;
public Task StartAsync(CancellationToken cancellationToken)
{
paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
connectionManager.ConnectionChanged += ConnectionManagerOnConnectionChanged;
return Task.CompletedTask;
}
private Task ConnectionManagerOnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
{
_listening = false;
return Task.CompletedTask;
}
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
{
if (!_listening || connectionManager.HubProxy is null) return;
await connectionManager.HubProxy.SendInvoiceUpdate(e.ToInvoice());
}
public Task StopAsync(CancellationToken cancellationToken)
{
paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
return Task.CompletedTask;
}
public void StartListen()
{
_listening = true;
}
}

View File

@ -0,0 +1,112 @@
using BTCPayApp.Core.Helpers;
using BTCPayServer.Lightning;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using TypedSignalR.Client;
namespace BTCPayApp.Core.BTCPayServer;
public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
{
private readonly IBTCPayAppHubServer _hubProxy;
private readonly ILogger _logger;
public ExceptionWrappedHubProxy(HubConnection connection, ILogger logger)
{
_hubProxy = connection.CreateHubProxy<IBTCPayAppHubServer>();
_logger = logger;
}
private async Task<T> Wrap<T>(Func<Task<T>> func)
{
return await AsyncExtensions.RunInOtherThread(async () =>
{
// executes in thread pool
try
{
return await func();
}
catch (InvalidOperationException e)
{
_logger.LogError(e, $"Error while calling hub method");
return default!;
}
catch (Exception e)
{
_logger.LogError(e, "Error while calling hub method");
return default!;
}
}).Unwrap();
}
public async Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active)
{
return await Wrap(async () => await _hubProxy.DeviceMasterSignal(deviceIdentifier, active));
}
public async Task<Dictionary<string, string>> Pair(PairRequest request)
{
return await Wrap(async () => await _hubProxy.Pair(request));
}
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
{
return await Wrap(async () => await _hubProxy.Handshake(request));
}
public async Task<bool> BroadcastTransaction(string tx)
{
return await Wrap(async () => await _hubProxy.BroadcastTransaction(tx));
}
public async Task<decimal> GetFeeRate(int blockTarget)
{
return await Wrap(async () => await _hubProxy.GetFeeRate(blockTarget));
}
public async Task<BestBlockResponse?> GetBestBlock()
{
return await Wrap(async () => await _hubProxy.GetBestBlock());
}
public async Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints)
{
return await Wrap(async () => await _hubProxy.FetchTxsAndTheirBlockHeads(identifier, txIds, outpoints));
}
public async Task<ScriptResponse> DeriveScript(string identifier)
{
return await Wrap(async () => await _hubProxy.DeriveScript(identifier));
}
public async Task TrackScripts(string identifier, string[] scripts)
{
await Wrap(() => Task.FromResult(_hubProxy.TrackScripts(identifier, scripts)));
}
public async Task<string> UpdatePsbt(string[] identifiers, string psbt)
{
return await Wrap(async () => await _hubProxy.UpdatePsbt(identifiers, psbt));
}
public async Task<Dictionary<string, CoinResponse[]>> GetUTXOs(string[] identifiers)
{
return await Wrap(async () => await _hubProxy.GetUTXOs(identifiers));
}
public async Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers)
{
return await Wrap(async () => await _hubProxy.GetTransactions(identifiers));
}
public async Task SendInvoiceUpdate(LightningInvoice lightningInvoice)
{
await Wrap(() => Task.FromResult(_hubProxy.SendInvoiceUpdate(lightningInvoice)));
}
public async Task<long?> GetCurrentMaster()
{
return await Wrap(async () => await _hubProxy.GetCurrentMaster());
}
}

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

@ -0,0 +1,52 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace BTCPayApp.Core.Backup;
public class SingleKeyDataProtector : IDataProtector
{
private readonly byte[] _key;
public SingleKeyDataProtector(byte[] key)
{
if (key.Length != 32) // AES-256 key size
throw new ArgumentException("Key length must be 32 bytes.");
_key = key;
}
public IDataProtector CreateProtector(string purpose)
{
using var hmac = new HMACSHA256(_key);
var purposeBytes = Encoding.UTF8.GetBytes(purpose);
var key = hmac.ComputeHash(purposeBytes).Take(32).ToArray();
return new SingleKeyDataProtector(key);
}
public byte[] Protect(byte[] plaintext)
{
using var aes = Aes.Create();
aes.Key = _key;
aes.GenerateIV();
var iv = aes.IV;
var encrypted = aes.EncryptCbc(plaintext, iv);
return iv.Concat(encrypted).ToArray();
}
public byte[] Unprotect(byte[] protectedData)
{
using var aes = Aes.Create();
aes.Key = _key;
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

@ -0,0 +1,423 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using Google.Protobuf;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using VSS;
using VSSProto;
namespace BTCPayApp.Core.Backup;
public class SyncService(
ConfigProvider configProvider,
ILogger<SyncService> logger,
IAccountManager accountManager,
IHttpClientFactory httpClientFactory,
IDbContextFactory<AppDbContext> dbContextFactory)
: IDisposable
{
public AsyncEventHandler<(List<Outbox> OutboxItemsProcesed, PutObjectRequest RemoteRequest)>? RemoteObjectUpdated;
public AsyncEventHandler<string[]>? LocalUpdated;
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
private readonly SemaphoreSlim _syncLock = new(1, 1);
private async Task<IDataProtector?> GetDataProtector()
{
var key = await accountManager.GetEncryptionKey();
return string.IsNullOrEmpty(key) ? null : new SingleKeyDataProtector(Convert.FromHexString(key));
}
public async Task<bool> EncryptionKeyRequiresImport()
{
var dataProtector = await GetDataProtector();
if (dataProtector is not null)
return false;
var api = await GetUnencryptedVSSAPI();
try
{
var res = await api.GetObjectAsync(new GetObjectRequest
{
Key = "encryptionKeyTest"
});
if (res.Value is null or {Value.Length: 0})
return false;
if (dataProtector is null)
return true;
var decrypted = dataProtector.Unprotect(res.Value.ToByteArray());
return "kukks" == Encoding.UTF8.GetString(decrypted);
}
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
{
return false;
}
catch (Exception e)
{
logger.LogError(e, "Error while checking if encryption key requires import");
throw;
}
}
public async Task<bool> SetEncryptionKey(Mnemonic mnemonic)
{
var key = mnemonic.DeriveExtKey().Derive(1337).PrivateKey.ToBytes();
return await SetEncryptionKey(Convert.ToHexString(key));
}
public async Task<bool> SetEncryptionKey(string key)
{
if (key.Contains(' ')) return await SetEncryptionKey(new Mnemonic(key));
var dataProtector = new SingleKeyDataProtector(Convert.FromHexString(key));
var encrypted = dataProtector.Protect("kukks"u8.ToArray());
var api = await GetUnencryptedVSSAPI();
try
{
var res = await api.GetObjectAsync(new GetObjectRequest
{
Key = "encryptionKeyTest"
});
if (res.Value is {Value.Length: > 0})
{
var decrypted = dataProtector.Unprotect(res.Value.Value.ToByteArray());
if ("kukks" == Encoding.UTF8.GetString(decrypted))
{
await accountManager.SetEncryptionKey(key);
return true;
}
return false;
}
}
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
{
}
catch (Exception e)
{
logger.LogError("Error while setting encryption key: {Message}", e.Message);
return false;
}
await api.PutObjectAsync(new PutObjectRequest
{
GlobalVersion = await configProvider.GetDeviceIdentifier(),
TransactionItems =
{
new KeyValue
{
Key = "encryptionKeyTest",
Value = ByteString.CopyFrom(encrypted)
}
},
});
await accountManager.SetEncryptionKey(key);
return true;
}
private Task<IVSSAPI> GetUnencryptedVSSAPI()
{
var account = accountManager.Account;
if (account is null)
throw new InvalidOperationException("Account not found");
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
var httpClient = httpClientFactory.CreateClient("vss");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", account.OwnerToken);
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
return Task.FromResult<IVSSAPI>(vssClient);
}
private async Task<IVSSAPI?> GetVSSAPI()
{
var dataProtector = await GetDataProtector();
return dataProtector is null ? null : new VSSApiEncryptorClient(await GetUnencryptedVSSAPI(), dataProtector);
}
private static async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
{
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue
{
Key = setting.EntityKey,
Version = setting.Version
});
var channels = dbContext.LightningChannels.Select(channel => new KeyValue
{
Key = channel.EntityKey,
Version = channel.Version
});
var payments = dbContext.LightningPayments.Select(payment => new KeyValue
{
Key = payment.EntityKey,
Version = payment.Version
});
return await settings.Concat(channels).Concat(payments).ToArrayAsync();
}
public async Task SyncToLocal(CancellationToken cancellationToken = default)
{
var backupApi = await GetVSSAPI();
if (backupApi is null)
return;
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var localVersions = await CreateLocalVersions(db);
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
await db.Database.BeginTransactionAsync(cancellationToken);
try
{
var triggers = await db.Database
.SqlQuery<TriggerRecord>($"SELECT name, sql FROM sqlite_master WHERE type = 'trigger'")
.ToListAsync(cancellationToken: cancellationToken);
await db.Database.ExecuteSqlRawAsync(
string.Join("; ", triggers.Select(trigger => $"DROP TRIGGER IF EXISTS {trigger.name}")),
cancellationToken: cancellationToken);
// delete local versions that are not in remote
// delete local versions which are lower than remote
var toDelete = localVersions.Where(localVersion =>
remoteVersions.KeyVersions.All(remoteVersion => remoteVersion.Key != localVersion.Key)
|| remoteVersions.KeyVersions.All(remoteVersion =>
remoteVersion.Key == localVersion.Key && remoteVersion.Version > localVersion.Version)).ToArray();
var toUpsert = remoteVersions.KeyVersions.Where(remoteVersion => localVersions.All(localVersion =>
localVersion.Key != remoteVersion.Key || localVersion.Version < remoteVersion.Version)).Where(value => value.Key != "encryptionKeyTest").ToArray();
if (toDelete.Length == 0 && toUpsert.Length == 0)
return;
logger.LogInformation("Syncing to local: {ToDelete} to delete, {ToUpsert} to upsert", toDelete.Length,
toUpsert.Length);
foreach (var upsertItem in toUpsert)
{
if (upsertItem.Value is not (null or { Length: 0 })) continue;
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
{
Key = upsertItem.Key,
}, cancellationToken);
upsertItem.MergeFrom(item.Value);
}
var settingsToDelete = toDelete.Where(key => key.Key.StartsWith("Setting_")).Select(key => key.Key);
var channelsToDelete = toDelete.Where(key => key.Key.StartsWith("Channel_")).Select(key => key.Key);
var paymentsToDelete = toDelete.Where(key => key.Key.StartsWith("Payment_")).Select(key => key.Key);
var deleteCount = 0;
deleteCount += await db.Settings.Where(setting => settingsToDelete.Contains(setting.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
deleteCount += await db.LightningChannels.Where(channel => channelsToDelete.Contains(channel.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
deleteCount += await db.LightningPayments.Where(payment => paymentsToDelete.Contains(payment.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
// upsert the rest when needed
var settingsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Setting_")).Select(setting => new Setting()
{
Key = setting.Key.Replace("Setting_", ""),
Value = setting.Value.ToByteArray(),
Version = setting.Version,
Backup = true
}).ToArray();
var channelsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Channel_"))
.Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!);
var paymentsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Payment_")).Select(value =>
JsonSerializer.Deserialize<AppLightningPayment>(value.Value.ToStringUtf8())!);
var upsertCount = 0;
upsertCount += await db.Settings.UpsertRange(settingsToUpsert).On(setting => setting.EntityKey)
.RunAsync(cancellationToken);
upsertCount += await db.LightningChannels.UpsertRange(channelsToUpsert).On(channel => channel.EntityKey)
.RunAsync(cancellationToken);
upsertCount += await db.LightningPayments.UpsertRange(paymentsToUpsert).On(payment => payment.EntityKey)
.RunAsync(cancellationToken);
await db.Database.ExecuteSqlRawAsync(string.Join("; ", triggers.Select(record => record.sql)),
cancellationToken: cancellationToken);
await db.SaveChangesAsync(cancellationToken);
await db.Database.CommitTransactionAsync(cancellationToken);
logger.LogInformation("Synced to local: {DeleteCount} deleted, {UpsertCount} upserted", deleteCount,
upsertCount);
LocalUpdated?.Invoke(this, toDelete.Concat(toUpsert).Select(key => key.Key).ToArray());
settingsToUpsert.Select(setting => setting.Key).Concat(settingsToDelete).Distinct().ToList()
.ForEach(key => configProvider.Updated?.Invoke(this, key));
}
catch (Exception e)
{
await db.Database.RollbackTransactionAsync(cancellationToken);
logger.LogError(e, "Error while syncing to local");
throw;
}
}
private static async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
{
switch (outbox.Entity)
{
case "Setting":
var setting = await dbContext.Settings.SingleOrDefaultAsync(setting1 =>
setting1.EntityKey == outbox.Key && setting1.Backup);
if (setting == null)
return null;
return new KeyValue
{
Key = outbox.Key,
Value = ByteString.CopyFrom(setting.Value),
Version = setting.Version
};
case "Channel":
var channel = await dbContext.LightningChannels.Include(channel1 => channel1.Aliases)
.SingleOrDefaultAsync(channel1 => channel1.EntityKey == outbox.Key);
if (channel == null)
return null;
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
return new KeyValue
{
Key = outbox.Key,
Value = ByteString.CopyFrom(val),
Version = channel.Version
};
case "Payment":
var payment = await dbContext.LightningPayments.SingleOrDefaultAsync(lightningPayment =>
lightningPayment.EntityKey == outbox.Key);
if (payment == null)
return null;
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
return new KeyValue
{
Key = outbox.Key,
Value = ByteString.CopyFrom(paymentBytes),
Version = payment.Version
};
default:
throw new ArgumentOutOfRangeException();
}
}
public async Task SyncToRemote(CancellationToken cancellationToken = default)
{
try
{
await _syncLock.WaitAsync(cancellationToken);
var backupAPi = await GetVSSAPI();
if (backupAPi is null)
return;
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var putObjectRequest = new PutObjectRequest
{
GlobalVersion = await configProvider.GetDeviceIdentifier()
};
var outbox = await db.OutboxItems.GroupBy(outbox1 => outbox1.Key)
.ToListAsync(cancellationToken: cancellationToken);
if (outbox.Count != 0)
{
logger.LogInformation("Syncing to remote {Count} outbox items", outbox.Count);
}
var removedOutboxItems = new List<Outbox>();
foreach (var outboxItemSet in outbox)
{
var orderedEnumerable = outboxItemSet.OrderByDescending(outbox1 => outbox1.Version)
.ThenByDescending(outbox1 => outbox1.ActionType).ToArray();
foreach (var item in orderedEnumerable)
{
if (item.ActionType == OutboxAction.Delete)
{
putObjectRequest.DeleteItems.Add(new KeyValue()
{
Key = item.Key, Version = item.Version
});
}
else
{
var kv = await GetValue(db, item);
if (kv != null)
{
putObjectRequest.TransactionItems.Add(kv);
break;
}
}
}
db.OutboxItems.RemoveRange(orderedEnumerable);
removedOutboxItems.AddRange(orderedEnumerable);
// Process outbox item
}
if (putObjectRequest.TransactionItems.Count == 0 && putObjectRequest.DeleteItems.Count == 0 && _syncTask is not null) return;
await backupAPi.PutObjectAsync(putObjectRequest, cancellationToken);
await db.SaveChangesAsync(cancellationToken);
logger.LogInformation("Synced to remote {TransactionItemsCount} items and deleted {DeleteItemsCount} items {Join}",
putObjectRequest.TransactionItems.Count,
putObjectRequest.DeleteItems.Count,
string.Join(", ", putObjectRequest.TransactionItems.Select(kv => kv.Key + " " + kv.Version)));
RemoteObjectUpdated?.Invoke(this, (removedOutboxItems, putObjectRequest.Clone()));
}
finally
{
_syncLock.Release();
}
}
public async Task StartSync(bool local,CancellationToken cancellationToken = default)
{
if (_syncTask.HasValue && _syncTask.Value.local == local && !_syncTask.Value.cts.IsCancellationRequested)
return;
if (_syncTask.HasValue && _syncTask.Value.local != local)
await _syncTask.Value.cts.CancelAsync();
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_syncTask = (ContinuouslySync(local, cts.Token), cts, local);
}
public async Task StopSync()
{
if (_syncTask.HasValue)
{
await _syncTask.Value.cts.CancelAsync();
_syncTask = null;
}
}
private async Task ContinuouslySync(bool local, CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (local)
await SyncToLocal(cancellationToken);
else
await SyncToRemote(cancellationToken);
}
catch (OperationCanceledException)
{
}
catch (Exception e)
{
logger.LogError(e, "Error while syncing to {Target}", local ? "local" : "remote");
}
finally
{
if (!cancellationToken.IsCancellationRequested)
await Task.Delay(2000, cancellationToken);
}
}
}
public void Dispose()
{
RemoteObjectUpdated = null;
LocalUpdated = null;
}
}

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

@ -1,8 +1,16 @@
namespace BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Helpers;
public interface IConfigProvider
namespace BTCPayApp.Core.Contracts;
public abstract class ConfigProvider : IDisposable
{
Task<T?> Get<T>(string key);
Task Set<T>(string key, T? value);
Task<IEnumerable<string>> List(string prefix);
public abstract Task<T?> Get<T>(string key);
public abstract Task Set<T>(string key, T? value, bool backup);
public abstract Task<IEnumerable<string>> List(string prefix);
public AsyncEventHandler<string>? Updated;
public virtual void Dispose()
{
Updated = null;
}
}

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,3 +1,7 @@
namespace BTCPayApp.Core.Contracts;
public interface ISecureConfigProvider : IConfigProvider;
public interface ISecureConfigProvider
{
Task<T?> Get<T>(string key);
Task Set<T>(string key, T? value);
}

View File

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.Contracts;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SetupState
{
Undetermined,
Pending,
Completed,
Failed
}

View File

@ -1,40 +1,196 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.JsonConverters;
using BTCPayServer.Lightning;
using Laraue.EfCoreTriggers.Common.Extensions;
using Microsoft.EntityFrameworkCore;
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<LightningPayment> LightningPayments { get; set; }
// public DbSet<SpendableCoin> SpendableCoins { 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)
{
//TODO: add paymentId to the primary key and generate a random one if not provided
modelBuilder.Entity<LightningPayment>()
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
//we use system.text.json because it is natively supported in efcore for querying
modelBuilder.Entity<LightningPayment>().Property(p => p.AdditionalData)
modelBuilder.Entity<Outbox>()
.HasKey(w => new {w.Entity, w.Key, w.ActionType, w.Version});
modelBuilder.Entity<Outbox>().Property(payment => payment.Timestamp).HasDefaultValueSql("datetime('now')");
modelBuilder.Entity<AppLightningPayment>().HasIndex(payment => payment.EntityKey).IsUnique();
modelBuilder.Entity<Setting>().HasIndex(payment => payment.EntityKey).IsUnique();
modelBuilder.Entity<Channel>().HasIndex(payment => payment.EntityKey).IsUnique();
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentRequest)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(v, JsonSerializerOptions.Default)!);
request => request!.ToString(),
str => NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network)));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Secret)
.HasConversion(
request => request!.ToString(),
str => uint256.Parse(str));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentHash)
.HasConversion(
request => request!.ToString(),
str => uint256.Parse(str));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Value)
.HasConversion(
request => request!.MilliSatoshi,
str => new LightMoney(str));
modelBuilder.Entity<Channel>().Property(channel => channel.AdditionalData).HasJsonConversion();
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.AdditionalData).HasJsonConversion();
modelBuilder.Entity<AppLightningPayment>()
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
//handling versioned data
//settings, channels, payments
//when creating, set the version to 0
//when updating, increment the version
// outbox creation
// when creating, insert an outbox item
// when updating, insert an outbox item
// when deleting, insert an outbox item
modelBuilder.Entity<Setting>()
.AfterInsert(trigger => trigger
.Action(group =>
{
group
.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
{
Entity = "Setting",
Version = @ref.New.Version,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Insert
});
}))
.AfterDelete(trigger => trigger
.Action(group => group
.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
{
Entity = "Setting",
Version = @ref.Old.Version,
Key = @ref.Old.EntityKey,
ActionType = OutboxAction.Delete
})))
.AfterUpdate(trigger => trigger
.Action(group => group
.Condition(@ref => @ref.Old.Backup)
// .Condition(@ref => @ref.Old.Value != @ref.New.Value)
.Update<Setting>(
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
(tableRefs, setting) => new Setting { Key = tableRefs.Old.Key, Version = tableRefs.Old.Version + 1 })
.Insert(
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
@ref => new Outbox
{
Entity = "Setting",
Version = @ref.Old.Version + 1,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Update
})));
// .Action(group => group
// .Condition(@ref => @ref.Old.Backup && !@ref.New.Backup)
// .Insert(
// // .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
// @ref => new Outbox()
// {
// Entity = "Setting",
// Version = @ref.Old.Version +1,
// Key = @ref.New.Key,
// ActionType = OutboxAction.Delete
// })));
modelBuilder.Entity<Channel>()
.AfterInsert(trigger => trigger
.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
{
Entity = "Channel",
Version = @ref.New.Version,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Insert
})))
.AfterDelete(trigger => trigger
.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
{
Entity = "Channel",
Version = @ref.Old.Version,
Key = @ref.Old.EntityKey,
ActionType = OutboxAction.Delete
})))
.AfterUpdate(trigger => trigger
.Action(group => group.Update<Channel>(
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
(tableRefs, setting) => new Channel { Id = tableRefs.Old.Id, Version = tableRefs.Old.Version + 1 }).Insert(
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
@ref => new Outbox
{
Entity = "Channel",
Version = @ref.Old.Version +1,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Update
})));
modelBuilder.Entity<AppLightningPayment>()
.AfterInsert(trigger => trigger
.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
{
Entity = "Payment",
Version = @ref.New.Version,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Insert
})))
.AfterDelete(trigger => trigger
.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
{
Entity = "Payment",
Version = @ref.Old.Version,
Key = @ref.Old.EntityKey,
ActionType = OutboxAction.Delete
})))
.AfterUpdate(trigger => trigger
.Action(group =>
group.Update<AppLightningPayment>(
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
(tableRefs, setting) => new AppLightningPayment {Version = tableRefs.Old.Version + 1}).Insert(
// .InsertIfNotExists( (@ref, outbox) =>
// 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
{
Entity = "Payment",
Version = @ref.Old.Version +1,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Update
})));
base.OnModelCreating(modelBuilder);
}
}
public class SpendableCoin
{
public string Script { get; set; }
[Key] public string Outpoint { get; set; }
public byte[] Data { get; set; }
}

View File

@ -0,0 +1,41 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using BTCPayApp.Core.JsonConverters;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayApp.Core.Data;
public class AppLightningPayment : VersionedData
{
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256? PaymentHash { get; set; }
public string? PaymentId { get; set; }
public string? Preimage { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256? Secret { get; set; }
public bool Inbound { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney? Value { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public LightningPaymentStatus Status { get; set; }
[JsonConverter(typeof(BOLT11PaymentRequestJsonConverter))]
public BOLT11PaymentRequest? PaymentRequest { get; set; }
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
public override string EntityKey
{
get => $"Payment_{PaymentHash}_{PaymentId}_{Inbound}";
init { }
}
}

View File

@ -1,10 +1,32 @@
namespace BTCPayApp.Core.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
public class Channel
namespace BTCPayApp.Core.Data;
public class Channel:VersionedData
{
public string Id { get; set; }
public List<string> Aliases { get; set; }
public byte[] Data { 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();
public override string EntityKey
{
get => $"Channel_{Id}";
init { }
}
}
public class ChannelAlias
{
public required string Id { get; init; }
public required string Type { get; init; }
public string? ChannelId { get; set; }
[JsonIgnore]
public Channel? Channel { get; set; }
}

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Laraue.EfCoreTriggers.SqlLite.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace BTCPayApp.Core.Data;
@ -9,6 +10,7 @@ public class DesignTimeAppContextFactory : IDesignTimeDbContextFactory<AppDbCont
{
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlite("Data Source=fake.db");
optionsBuilder.UseSqlLiteTriggers();
return new AppDbContext(optionsBuilder.Options);
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
namespace BTCPayApp.Core.Data;
public static class EFExtensions
{
public static async Task<int> Upsert<T>(this DbContext ctx, T item, CancellationToken cancellationToken) where T : class
{
return await ctx.Upsert(item).RunAsync(cancellationToken);
// ctx.Attach(item);
// ctx.Entry(item).State = EntityState.Modified;
// try
// {
// return await ctx.SaveChangesAsync(cancellationToken);
// }
// catch (DbUpdateException)
// {
// ctx.Entry(item).State = EntityState.Added;
// return await ctx.SaveChangesAsync(cancellationToken);
// }
}
}

View File

@ -3,12 +3,13 @@
namespace BTCPayApp.Core.Data;
public class LightningConfig
{
public const string Key = "lightningconfig";
public const string Key = "ln:lightningconfig";
public string Alias { get; set; } = "BTCPay Server";
public string ScriptDerivationKey { get; set; } = WalletDerivation.NativeSegwit; //when ldk asks for an address, where do we get it from?
public string LightningDerivationPath { get; set; } = "m/666'";// your lightning node derivation path
public string Color { get; set; } = "#51B13E";
public Uri? RapidGossipSyncUrl { get; set; }
public string? JITLSP { get; set; } // Just In Time Lightning Service Provider
@ -17,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("#"))
{
@ -38,15 +39,8 @@ public class LightningConfig
}
}
}
public Dictionary<string, PeerInfo> Peers { get; set; } = new();
public bool AcceptInboundConnection{ get; set; }
}
public record PeerInfo
{
public string Endpoint { get; set; }
public bool Persistent { get; set; }
public bool Trusted { 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

@ -0,0 +1,10 @@
namespace BTCPayApp.Core.Data;
public class Outbox
{
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
public OutboxAction ActionType { get; set; }
public required string Key { get; set; }
public required string Entity { get; set; }
public required long Version { get; set; }
}

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.Data;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OutboxAction
{
Insert,
Update,
Delete
}

View File

@ -0,0 +1,14 @@
using System.Net;
using System.Text.Json.Serialization;
using BTCPayApp.Core.JsonConverters;
namespace BTCPayApp.Core.Data;
public record PeerInfo
{
[JsonConverter(typeof(EndPointJsonConverter))]
public EndPoint? Endpoint { get; set; }
public bool Persistent { get; set; }
public bool Trusted { get; set; }
public string? Label { get; set; }
}

View File

@ -2,9 +2,16 @@
namespace BTCPayApp.Core.Data;
public class Setting
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
{
get => $"Setting_{Key}";
init { }
}
}

View File

@ -0,0 +1,43 @@
namespace BTCPayApp.Core.Data;
public static class TLVHelper
{
public record TLV(byte Tag, byte[] Value);
public static byte[] Write(List<TLV> tlvList)
{
List<byte> byteArray = new List<byte>();
foreach (var tlv in tlvList)
{
byteArray.Add(tlv.Tag);
byteArray.AddRange(BitConverter.GetBytes(tlv.Value.Length));
byteArray.AddRange(tlv.Value);
}
return byteArray.ToArray();
}
public static List<TLV> Read(byte[] byteArray)
{
var tlvList = new List<TLV>();
var index = 0;
while (index < byteArray.Length)
{
var tag = byteArray[index];
index += 1;
var length = BitConverter.ToInt32(byteArray, index);
index += 4;
var value = new byte[length];
Array.Copy(byteArray, index, value, 0, length);
index += length;
tlvList.Add(new TLV(tag, value));
}
return tlvList;
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayApp.Core.Data;
class TriggerRecord
{
public string name { get; set; }
public string sql { get; set; }
}

View File

@ -0,0 +1,36 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayApp.Core.Data;
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder)
where T : class, new()
{
var converter = new ValueConverter<T, string>
(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<T>(v, JsonSerializerOptions.Default) ?? new T()
);
var comparer = new ValueComparer<T>
(
(l, r) => JsonSerializer.Serialize(l, JsonSerializerOptions.Default) ==
JsonSerializer.Serialize(r, JsonSerializerOptions.Default),
v => v == null ? 0 : JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(),
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
JsonSerializerOptions.Default)!
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace BTCPayApp.Core.Data;
public abstract class VersionedData
{
public long Version { get; set; } = 0;
public abstract string EntityKey { get; init; }
}

View File

@ -1,4 +1,6 @@
using NBitcoin;
using System.Text.Json.Serialization;
using BTCPayApp.Core.JsonConverters;
using NBitcoin;
namespace BTCPayApp.Core.Data;
@ -8,14 +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 required BlockSnapshot Birthday { get; set; }
public required CoinSnapshot CoinSnapshot { get; set; }
}
public class CoinSnapshot
{
public required BlockSnapshot BlockSnapshot { get; set; }
public required Dictionary<string, SavedCoin[]> Coins { get; set; }
}
public class SavedCoin
{
[JsonConverter(typeof(BitcoinSerializableJsonConverterFactory))]
public required OutPoint Outpoint { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath? Path { get; set; }
}
public class BlockSnapshot
{
public required uint BlockHeight { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public required uint256 BlockHash { get; set; }
}

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

@ -1,31 +1,116 @@
using System.Text.Json;
// using System.Collections.Concurrent;
// using System.Text.Json;
// using BTCPayApp.Core.Attempt2;
// using BTCPayApp.Core.Contracts;
// using BTCPayApp.Core.Data;
// using BTCPayApp.VSS;
// using BTCPayServer.Lightning;
// using Microsoft.EntityFrameworkCore;
// using Microsoft.EntityFrameworkCore.Diagnostics;
// using Microsoft.Extensions.Logging;
// using VSSProto;
//
// namespace BTCPayApp.Core;
//
//
//
// public class VSSMapperInterceptor : SaveChangesInterceptor
// {
//
// 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())
// {
// return base.SavedChangesAsync(eventData, result, cancellationToken);
// }
//
// private IVSSAPI api;
// public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
// CancellationToken cancellationToken = new CancellationToken())
// {
// foreach (var entry in eventData.Context.ChangeTracker.Entries())
// {
//
// if (entry.Entity is LightningPayment lightningPayment)
// {
// if (entry.State == EntityState.Deleted)
// {
//
// api.DeleteObjectAsync(new DeleteObjectRequest
// {
// KeyValue = new KeyValue()
// {
//
// }
// Key = $"LightningPayment/{lightningPayment.Id}"
// });
// }
// }
// if (entry.Entity is Channel channel)
// {
//
// }
// if (entry.Entity is Setting setting)
// {
//
// }
// }
//
// return base.SavingChangesAsync(eventData, result, cancellationToken);
// }
//
// public override Task SaveChangesCanceledAsync(DbContextEventData eventData,
// CancellationToken cancellationToken = new CancellationToken())
// {
// PendingEvents.Remove(eventData.EventId, out _);
// return base.SaveChangesCanceledAsync(eventData, cancellationToken);
// }
//
// public override Task SaveChangesFailedAsync(DbContextErrorEventData eventData,
// CancellationToken cancellationToken = new CancellationToken())
// {
// PendingEvents.Remove(eventData.EventId, out _);
// return base.SaveChangesFailedAsync(eventData, cancellationToken);
// }
//
//
// }
//
using System.Text.Json;
using AsyncKeyedLock;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core;
public class DatabaseConfigProvider: IConfigProvider
public class DatabaseConfigProvider(
IDbContextFactory<AppDbContext> dbContextFactory,
ILogger<DatabaseConfigProvider> logger)
: ConfigProvider
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly AsyncKeyedLocker<string> _lock = new();
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory)
public override async Task<T?> Get<T>(string key) where T : default
{
_dbContextFactory = dbContextFactory;
}
public async Task<T?> Get<T>(string key)
{
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);
return config is null ? default : JsonSerializer.Deserialize<T>(config.Value);
}
public async Task Set<T>(string key, T? value)
public override async Task Set<T>(string key, T? value, bool backup) where T : default
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
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();
if (value is null)
{
try
@ -40,13 +125,13 @@ public class DatabaseConfigProvider: IConfigProvider
}
var newValue = typeof(T) == typeof(byte[])? value as byte[]:JsonSerializer.SerializeToUtf8Bytes(value);
await dbContext.Upsert(new Setting {Key = key, Value = newValue}).RunAsync();
var setting = new Setting {Key = key, Value = newValue, Backup = backup};
await dbContext.Upsert(setting, CancellationToken.None);
}
public async Task<IEnumerable<string>> List(string prefix)
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

@ -0,0 +1,76 @@
#if DEBUG
using System.Net.Security;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace BTCPayApp.Core.Extensions;
public class DangerousHttpClientFactory : IHttpClientFactory
{
public static bool ServerValidate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
{
if (errors == SslPolicyErrors.None) return true;
return certificate?.Subject.Equals("CN=localhost") is true;
}
private static HttpClientHandler GetInsecureHandler()
{
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = ServerValidate;
return handler;
}
public HttpClient CreateClient(string name)
{
return new HttpClient(GetInsecureHandler());
}
}
#if ANDROID
public class DangerousAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
{
protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
=> new CustomHostnameVerifier();
private sealed class CustomHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier
{
public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
{
return session?.PeerPrincipal?.Name == "CN=localhost";
}
}
}
#endif
public static class DebugExtensions
{
public static IServiceCollection AddDangerousSSLSettingsForDev(this IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton<IHttpClientFactory, DangerousHttpClientFactory>());
services.AddSingleton<Func<HttpMessageHandler, HttpMessageHandler>>(handler =>
{
if (handler is HttpClientHandler clientHandler)
{
// always verify the SSL certificate
clientHandler.ServerCertificateCustomValidationCallback += DangerousHttpClientFactory.ServerValidate;
return clientHandler;
}
#if ANDROID
return new DangerousAndroidMessageHandler();
#else
return handler;
#endif
});
services.AddSingleton<Action<ClientWebSocketOptions>>(provider => wsc =>
{
wsc.RemoteCertificateValidationCallback = DangerousHttpClientFactory.ServerValidate;
});
return services;
}
}
#endif

View File

@ -1,16 +1,20 @@
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Attempt2;
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 Laraue.EfCoreTriggers.SqlLite.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core;
namespace BTCPayApp.Core.Extensions;
public static class StartupExtensions
{
@ -20,10 +24,19 @@ public static class StartupExtensions
{
var dir = provider.GetRequiredService<IDataDirectoryProvider>().GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
options.UseSqlite($"Data Source={dir}/app.db");
options.UseSqlLiteTriggers();
});
// 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>();
@ -32,38 +45,13 @@ public static class StartupExtensions
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<IConfigProvider, DatabaseConfigProvider>();
serviceCollection.AddSingleton<IAuthorizationHandler, AuthorizationHandler>();
serviceCollection.AddAuthorizationCore(options => options.AddPolicies());
serviceCollection.AddLDK();
return serviceCollection;
}
}
public class AppDatabaseMigrator: 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);
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
if (pendingMigrationsAsync.Any())
{
_logger.LogInformation($"Applying {pendingMigrationsAsync.Length} migrations");
await dbContext.Database.MigrateAsync(cancellationToken);
_logger.LogInformation("Migrations applied: " + string.Join(", ", pendingMigrationsAsync));
}
}
public async Task StopAsync(CancellationToken cancellationToken) { }
}

View File

@ -6,16 +6,24 @@ namespace BTCPayApp.Core.Helpers;
public static class AsyncExtensions
{
public static async Task RunSync(this Task task)
public static async Task RunInOtherThread(Action action)
{
task.GetAwaiter().GetResult();
await Task.Factory.StartNew(action);
}
public static async Task<T> RunSync<T>(this Task<T> task)
public static async Task<T> RunInOtherThread<T>(Func<T> action)
{
return task.GetAwaiter().GetResult();
return await Task.Factory.StartNew(action);
}
public static async Task RunInOtherThread(this Task task)
{
await Task.Factory.StartNew(async () => await task).Unwrap();
}
public static async Task<T> RunInOtherThread<T>(this Task<T> task)
{
return await Task.Factory.StartNew(async () => await task).Unwrap();
}
/// <summary>
/// Allows a cancellation token to be awaited.
@ -46,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.");
@ -64,4 +72,4 @@ public static class AsyncExtensions
public void UnsafeOnCompleted(Action continuation) =>
CancellationToken.Register(continuation);
}
}
}

View File

@ -0,0 +1,36 @@
using BTCPayApp.Core.Auth;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayApp.Core.Helpers;
// Copied from BTCPayServer
public static class AuthorizationOptionsExtensions
{
public static AuthorizationOptions AddPolicies(this AuthorizationOptions options)
{
// BTCPay policies
foreach (var p in Policies.AllPolicies)
{
options.AddPolicy(p);
}
options.AddPolicy(Policies.CanModifyStoreSettingsUnscoped);
options.AddPolicy(CanGetRates.Key);
// app policies
foreach (var p in AppPolicies.AllPolicies)
{
options.AddPolicy(p);
}
return options;
}
private static void AddPolicy(this AuthorizationOptions options, string policy)
{
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
}
private class CanGetRates
{
public const string Key = "btcpay.store.cangetrates";
}
}

View File

@ -1,38 +1,42 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayApp.Core.Helpers;
public abstract class BaseHostedService : IHostedService, IDisposable
public abstract class BaseHostedService(ILogger logger) : IHostedService, IDisposable
{
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 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)
{
await _cancellationTokenSource.CancelAsync();
await _controlSemaphore.WaitAsync(cancellationToken);
logger.LogInformation("Stopping service");
await CancellationTokenSource.CancelAsync();
await ControlSemaphore.WaitAsync(cancellationToken);
try
{
await ExecuteStopAsync(_cancellationTokenSource.Token);
await ExecuteStopAsync(CancellationTokenSource.Token);
logger.LogInformation("Stopped");
}
finally
{
_controlSemaphore.Release();
ControlSemaphore.Release();
}
}
@ -41,7 +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);
try
{
await act();
}
finally
{
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)
{
@ -23,47 +23,45 @@ public static class ChannelExtensions
await channel.Writer.WriteAsync(evt, cancellationToken);
}
add(new AsyncEventHandler<TEvent>(OnEvent));
_ = ProcessChannel(channel, processor, cancellationToken);
add(OnEvent);
_ = channel.ProcessChannel(processor, cancellationToken);
return new DisposableWrapper(async () =>
return new DisposableWrapper(() =>
{
remove(new AsyncEventHandler<TEvent>(OnEvent));
remove(OnEvent);
channel.Writer.Complete();
return Task.CompletedTask;
});
}
private static async Task ProcessChannel<TEvent>(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();
}
@ -81,20 +79,25 @@ public static class ChannelExtensions
public static UserConfig AsLDKUserConfig(this LightningConfig config)
{
var result = UserConfig.with_default();
// var channelConfig = ChannelConfig.with_default();
// channelConfig.set
// result.set_channel_config(channelConfig);
result.set_accept_intercept_htlcs(true);
result.set_accept_mpp_keysend(true);
//result.set_accept_mpp_keysend(true);
result.set_manually_accept_inbound_channels(true);
var channelHandshakeConfig = ChannelHandshakeConfig.with_default();
channelHandshakeConfig.set_announced_channel(false);
//channelHandshakeConfig.set_announced_channel(false);
// channelHandshakeConfig.set(false);
channelHandshakeConfig.set_negotiate_anchors_zero_fee_htlc_tx(true);
channelHandshakeConfig.set_minimum_depth(1);
result.set_channel_handshake_config(channelHandshakeConfig);
var channelHandshakeLimits = ChannelHandshakeLimits.with_default();
channelHandshakeLimits.set_force_announced_channel_preference(true);
channelHandshakeLimits.set_max_funding_satoshis(Money.Coins(100m).Satoshi);
result.set_channel_handshake_limits(channelHandshakeLimits);
return result;
}
// public static async Task Process<T>(this Channel<T> channel, Func<T, CancellationToken, Task> processor,
// CancellationToken cancellationToken)
// {
@ -172,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

@ -0,0 +1,27 @@
using BTCPayApp.Core.Contracts;
using NBitcoin;
namespace BTCPayApp.Core.Helpers;
public static class ConfigExtensions
{
private const string ConfigDeviceIdentifierKey = "deviceIdentifier";
/*
public static async Task<long> GetDeviceIdentifier(this ISecureConfigProvider configProvider)
{
var id = await configProvider.Get<long>(ConfigDeviceIdentifierKey);
if (id == 0)
{
id = RandomUtils.GetInt64();
await configProvider.Set(ConfigDeviceIdentifierKey, id);
}
return id;
}
*/
public static async Task<long> GetDeviceIdentifier(this ConfigProvider configProvider)
{
return await configProvider.GetOrSet(ConfigDeviceIdentifierKey, () => Task.FromResult(RandomUtils.GetInt64()), false);
}
}

View File

@ -0,0 +1,24 @@
using BTCPayApp.Core.Contracts;
namespace BTCPayApp.Core.Helpers;
public static class ConfigHelpers
{
public static async Task<T?> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key, Func<Task<T>> factory)
{
var value = await secureConfigProvider.Get<T>(key);
if (!Equals(value, default(T))) return value;
value = await factory();
await secureConfigProvider.Set(key, value);
return value;
}
public static async Task<T?> GetOrSet<T>(this ConfigProvider configProvider, string key, Func<Task<T>> factory, bool backup)
{
var value = await configProvider.Get<T>(key);
if (!Equals(value, default(T))) return value;
value = await factory();
await configProvider.Set(key, value, backup);
return value;
}
}

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

@ -11,9 +11,9 @@ namespace BTCPayApp.Core.Helpers;
/// </summary>
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged where TKey : notnull
{
private const string IndexerName = "Item[]";
private const string? IndexerName = "Item[]";
/// <summary>
/// Initializes a new instance of the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> class that is empty, has the
@ -109,10 +109,10 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
}
/// <summary>Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.</summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
/// <summary>Occurs when a property value changes.</summary>
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Uses the specified functions to add a key/value pair to the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> if the
@ -275,7 +275,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
/// the default value of the <typeparamref name="TValue"/> type if <paramref name="key"/> does not exist.
/// </param>
/// <returns><see langword="true"/> if the object was removed successfully; otherwise, <see langword="false"/>.</returns>
public new bool TryRemove(TKey key, out TValue value)
public new bool TryRemove(TKey key, out TValue? value)
{
if (base.TryRemove(key, out value))
{
@ -319,5 +319,5 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
/// <summary>Raises the <see cref="PropertyChanged" /> event.</summary>
/// <param name="propertyName">Name of the property that has changed.</param>
private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Authorization;
namespace BTCPayApp.Core.Helpers;
// Copied from BTCPayServer
public class PolicyRequirement : IAuthorizationRequirement
{
public PolicyRequirement(string policy)
{
ArgumentNullException.ThrowIfNull(policy);
Policy = policy;
}
public string Policy { get; }
}

View File

@ -0,0 +1,99 @@
using System.Text.Json;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.LDK;
using BTCPayApp.Core.Wallet;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.Helpers;
public static class StoreHelpers
{
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)>
GetCurrentStorePaymentMethods(this IAccountManager accountManager)
{
var storeId = accountManager.CurrentStore?.Id;
var pms = await accountManager.GetClient().GetStorePaymentMethods(storeId, includeConfig: true);
var onchain = pms.FirstOrDefault(pm => pm.PaymentMethodId == OnChainWalletManager.PaymentMethodId);
var lightning = pms.FirstOrDefault(pm => pm.PaymentMethodId == LightningNodeManager.PaymentMethodId);
return (onchain, lightning);
}
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)?> TryApplyingAppPaymentMethodsToCurrentStore(
this IAccountManager accountManager,
OnChainWalletManager onChainWalletManager, LightningNodeManager lightningNodeService, bool applyOnchain, bool applyLighting)
{
var storeId = accountManager.CurrentStore?.Id;
var userId = accountManager.UserInfo?.UserId;
var config = await onChainWalletManager.GetConfig();
if (// are user and store present?
string.IsNullOrEmpty(userId) ||
string.IsNullOrEmpty(storeId) ||
// is user permitted? (store owner)
!await accountManager.IsAuthorized(Policies.CanModifyStoreSettings, storeId) ||
// is the onchain wallet configured?
!OnChainWalletManager.IsConfigured(config)) return null;
// check the store's payment methods
var (onchain, lightning) = await GetCurrentStorePaymentMethods(accountManager);
// onchain
if (applyOnchain && config?.Derivations.TryGetValue(WalletDerivation.NativeSegwit, out var derivation) is true && onchain is null)
{
onchain = await accountManager.GetClient().UpdateStorePaymentMethod(storeId, OnChainWalletManager.PaymentMethodId, new UpdatePaymentMethodRequest
{
Enabled = true,
Config = derivation.Descriptor
});
}
// lightning
if (applyLighting && lightning is null && lightningNodeService is { IsActive: true, Node.ApiKeyManager: { } apiKeyManager })
{
var key = await apiKeyManager.GetKeyForStore(storeId, APIKeyPermission.Write);
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId,
LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
{
Enabled = true,
Config = key.ConnectionString(userId)
});
}
return (onchain, lightning);
}
public static async Task<bool> IsOnChainOurs(this OnChainWalletManager onChainWalletManager, GenericPaymentMethodData? onchain)
{
if (!string.IsNullOrEmpty(onchain?.Config.ToString()))
{
var config = await onChainWalletManager.GetConfig();
using var jsonDoc = JsonDocument.Parse(onchain.Config.ToString());
if (jsonDoc.RootElement.TryGetProperty("accountDerivation", out var derivationSchemeElement) &&
derivationSchemeElement.GetString() is { } derivationScheme &&
config?.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}") is true)
return true;
}
return false;
}
public static async Task<bool> IsLightningOurs(this LightningNodeManager lightningNodeManager, GenericPaymentMethodData? lightning)
{
if (!string.IsNullOrEmpty(lightning?.Config.ToString()))
{
var node = lightningNodeManager.Node;
var apiKeyManager = node?.ApiKeyManager;
if (apiKeyManager == null) return false;
using var jsonDoc = JsonDocument.Parse(lightning.Config.ToString());
if (jsonDoc.RootElement.TryGetProperty("connectionString", out var connectionStringElement) &&
connectionStringElement.GetString() is { } connectionString &&
LightningConnectionStringHelper.ExtractValues(connectionString, out var lnConnectionString) is { } lnValues &&
lnConnectionString == "app" && lnValues.TryGetValue("key", out var key) && key is not null &&
await node!.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
return true;
}
return false;
}
}

View File

@ -0,0 +1,81 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace BTCPayApp.Core.Helpers
{
/// <summary>
/// from https://github.com/dotnet/runtime/issues/31433#issuecomment-2148885279
/// </summary>
public static class SystemTextJsonMergeExtensions
{
/// <summary>
/// Merges the specified Json Node into the base JsonNode for which this method is called.
/// It is null safe and can be easily used with null-check & null coalesce operators for fluent calls.
/// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects
/// specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all
/// fields so that they can be added to the base.
///
/// Source taken directly from the open-source Gist here:
/// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f
///
/// </summary>
/// <param name="jsonBase"></param>
/// <param name="jsonMerge"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static JsonNode Merge(this JsonNode jsonBase, JsonNode? jsonMerge)
{
if (jsonMerge == null)
return jsonBase;
switch (jsonBase)
{
case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj:
{
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be
// re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach...
var mergeNodesArray = jsonMergeObj.ToArray();
jsonMergeObj.Clear();
foreach (var prop in mergeNodesArray)
{
jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch
{
JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge(jsonMergeChildObj),
JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge(jsonMergeChildArray),
_ => prop.Value
};
}
break;
}
case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray:
{
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array,
// so they can then be re-assigned to the target/base Json...
var mergeNodesArray = jsonMergeArray.ToArray();
jsonMergeArray.Clear();
foreach(var mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode);
break;
}
default:
throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " +
$"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same.");
}
return jsonBase;
}
/// <summary>
/// Merges the specified Dictionary of values into the base JsonNode for which this method is called.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="jsonBase"></param>
/// <param name="dictionary"></param>
/// <param name="options"></param>
/// <returns></returns>
public static JsonNode MergeDictionary<TKey, TValue>(this JsonNode jsonBase, IDictionary<TKey, TValue> dictionary, JsonSerializerOptions? options = null)
=> jsonBase.Merge(JsonSerializer.SerializeToNode(dictionary, options));
}
}

View File

@ -0,0 +1,11 @@
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.JsonConverters;
public class BOLT11PaymentRequestJsonConverter : GenericStringJsonConverter<BOLT11PaymentRequest>
{
public override BOLT11PaymentRequest Create(string str)
{
return NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network));
}
}

View File

@ -0,0 +1,23 @@
using NBitcoin;
namespace BTCPayApp.Core.JsonConverters;
public class BitcoinSerializableJsonConverter<T> : GenericStringJsonConverter<T> where T : IBitcoinSerializable
{
public override T Create(string str)
{
var bytes = Convert.FromHexString(str);
var instance = Activator.CreateInstance<T>();
return NetworkHelper.Try(network =>
{
instance.ReadWrite(bytes, network);
return instance;
});
}
public override string? ToString(T? instance)
{
return Convert.ToHexString(instance.ToBytes()).ToLowerInvariant();
}
}

View File

@ -0,0 +1,19 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using NBitcoin;
namespace BTCPayApp.Core.JsonConverters;
public class BitcoinSerializableJsonConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeof(IBitcoinSerializable).IsAssignableFrom(typeToConvert);
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var converterType = typeof(BitcoinSerializableJsonConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter) Activator.CreateInstance(converterType)!;
}
}

View File

@ -0,0 +1,27 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.JsonConverters;
public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return default;
case JsonTokenType.Number:
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
case JsonTokenType.String:
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()!));
}
throw new JsonException("Expected number or string with a unix timestamp value");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value.ToUnixTimeSeconds());
}
}

View File

@ -0,0 +1,37 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BTCPayApp.Core.JsonConverters;
public abstract class GenericStringJsonConverter<T> : JsonConverter<T>
{
public abstract T Create(string str);
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return default;
if (reader.TokenType != JsonTokenType.String ||
reader.GetString() is not { } str ||
string.IsNullOrEmpty(str))
throw new JsonException("Expected string");
return Create(str);
}
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
writer.WriteStringValue(ToString(value));
}
public virtual string? ToString(T? value)
{
return value?.ToString() ?? string.Empty;
}
}

View File

@ -0,0 +1,11 @@
using NBitcoin;
namespace BTCPayApp.Core.JsonConverters;
public class KeyPathJsonConverter : GenericStringJsonConverter<KeyPath>
{
public override KeyPath Create(string str)
{
return new KeyPath(str);
}
}

View File

@ -0,0 +1,11 @@
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.JsonConverters;
public class LightMoneyJsonConverter : GenericStringJsonConverter<LightMoney>
{
public override LightMoney Create(string str)
{
return LightMoney.Parse(str);
}
}

View File

@ -0,0 +1,22 @@
using NBitcoin;
namespace BTCPayApp.Core.JsonConverters;
public static class NetworkHelper
{
public static T Try<T>(Func<Network, T> func)
{
Exception? lastException = null;
foreach (var network in Network.GetNetworks())
try
{
return func.Invoke(network);
}
catch (Exception e)
{
lastException = e;
}
throw lastException!;
}
}

Some files were not shown because too many files have changed in this diff Show More