Compare commits

...

87 Commits

Author SHA1 Message Date
913ff62f2d Fix migration crash 2024-04-04 22:50:12 +09:00
6cc1751924 The Big Cleanup: Refactor BTCPay internals (#5809) 2024-04-04 16:31:04 +09:00
69b589a401 Adding Tether as BTCPay Server Foundation Supporter (#5891)
* Adding Tether as BTCPay Server Foundation Supporter

* Adding Tether to _BTCPaySupporters partial as well

* Modfying supporter_strike.svg to have white backgroundf or dark mode

* Modifying supporter_tether.svg to fit in the 150x100 box

* Centering Tether shape
2024-04-02 19:06:24 -05:00
db73b1f268 Fix test CheckJSContent 2024-04-01 12:11:00 +09:00
9ac0e982d6 chore: fix typos (#5883) 2024-03-30 10:20:24 +01:00
cb25c225e9 Remove custodians (#5863)
* Remove custodians

* Hide Experimental checkbox in the server policies

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-03-29 00:28:27 +09:00
5b31d4de20 v1.13: Update changelog (#5879) 2024-03-28 22:40:18 +09:00
14f8c73b08 POS: Increase size of quantity buttons (#5877)
Closes #5873.
2024-03-28 09:01:56 +09:00
529075f64c Make "Employee" default role on store settings (#5874)
* Refactoring: Use property rather than injecting StoreRepository

* Update info text

* Make "Employee" default role on store settings

Closes #5867.
2024-03-28 09:01:34 +09:00
dba102e74f Template Editor: Fix mobile view (#5871)
Fixes #5869.
2024-03-27 19:20:49 +09:00
0f3f8b6bf9 Contact Us improvements (#5872)
* Add contact link to sidebar

Closes #5866.

* Obfuscate contact email on public pages

Closes #5870.

* Fix
2024-03-27 19:19:39 +09:00
83028b9b73 Adding introduction, Authentication and Usage examples sections to the API docs. (#5858) 2024-03-24 00:02:01 +09:00
1fe766cb16 Keypad: Fix images (#5857) 2024-03-22 15:16:59 +01:00
6b45eb0d3d Do not throw when local node is not synced and using external ln node (#5859)
* Do not throw when local node is not synced and using external ln node

* Fix additional bug when ln conn strings without server would crash
2024-03-22 10:06:38 +01:00
6b0087ab69 Update version 2024-03-21 21:40:09 +09:00
e60fd8d6ab Changelog v1.13 (#5840) 2024-03-21 21:39:06 +09:00
88a1d83323 Support bbqr psbts (#5852)
* Support bbqr psbts

https://bbqr.org/ @nvk

* add js test for bbqr
2024-03-21 10:30:23 +01:00
e21a8df0f3 smaller printed receipts (#5856) 2024-03-21 17:30:34 +09:00
93f37b506b UI: Improve Create First Store view (#5854)
Unifies the width and display with the login view.
2024-03-21 07:37:15 +01:00
fca3480e37 Specify mailto: prefix for emails in Server Settings (#5844)
* Specify mailto: prefix for emails in Server Settings

* resolve test failure

* Update wording

* Apply mailto-prefix on setting change

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-03-19 15:04:09 +01:00
966547db54 Template Editor: Apply item changes directly (#5849)
Closes #5847.
2024-03-19 14:59:26 +01:00
09dbe44bca Onboarding: Invite new users on store level (#5719)
* Onboarding: Invite new users

- Separates the user self-registration and invite cases
- Adds invitation email for users created by the admin
- Adds invitation tokens to verify user was invited
- Adds handler action for invite links
- Refactors `UserEventHostedService`
- Fixes #5726.

* Add permissioned form tag helper

* Better way of changing a user's role

* Test fixes
2024-03-19 14:58:33 +01:00
b7ce6b7400 Providing additional parameter for info message (#5756)
* Providing additional parameter for info message

* Refactoring code to remove parameter and only set status message in LoadFromBIP21 if not present

* Update BTCPayServer/Controllers/UIWalletsController.cs

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-03-18 12:17:30 +01:00
78f169cd24 chore: remove repetitive words (#5842)
chore: remove repetitive words

Signed-off-by: soonsouth <cuibuwei@163.com>
2024-03-18 10:49:07 +01:00
d0e11f1ec4 changing check box to toggle in various setting views (#5769)
* Resolves: check box to toggle in various setting views

* resolve conflicts

* Notification logic reversal

* remove transform property in the toggle

* Handle email tls certificate check

* Unifications and fixes

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-03-14 15:16:48 +01:00
912a706de9 Make Payouts and PullPayments columns JSONB (#5800) 2024-03-14 11:13:26 +01:00
9b5c8a8254 POS: Add item list to keypad (#5814)
* Add admin option to show item list for keypad view

* Refactor common POS Vue mixin

* Add item list to POS keypad

* Add recent transactions to cart

* Keypad: Pass tip and discount as cart does

* Keypad and cart tests

* Improve offcanvas button

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-03-14 11:11:54 +01:00
e5adc630af If pull payment opened in mobile, use deeplink to setup card (#5613)
* If pull payment opened in mobile, use deeplink to setup card

* Allow passing LNURLW to register boltcard

* debug

* debug

* debug

* Only show setup/reset when the page is fully loaded

* Apply suggestions from code review

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-03-14 11:07:11 +01:00
c56c6401d6 (feat) monero settlement thresholds (#5807)
* (bug) treat xmr wallet directory as required

The wallet directory configuration setting is required
because the `UIMoneroLikeStoreController`'s
`GetMoneroLikePaymentMethodViewModel` method checks if the wallet file
exists, and to do that in needs the directory.

* (feat) xmr settlement thresholds

Adds the ability to select zero, 1, 10, or a custom number of
confirmations as the payment settlement threshold.

* (review) fix validation message not showing

---------

Co-authored-by: Henry Hollingworth <henry.hollingworth@alcoa.com>
2024-03-14 10:31:27 +01:00
0e64df3bbf Parallel payout ln (#5781) 2024-03-14 10:29:14 +01:00
e497903bf4 Support Admin being able to view stores (#5782)
* Support Admin being able to view stores

* fix null check

* Delete obsolete empty view

* Add test

* Apply CanViewStoreSettings policy changes

Taken from #5719

* Fix Selenium tests

* Update dashboard permission requirement

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-03-14 10:25:40 +01:00
f1ff913cbe PoS app to show POS view for easy setup (#5825)
* PoS app to show POS view for easy setup

* update selenium test

* Updates

* Add QR code icon

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-03-14 08:52:33 +01:00
3a00d32ce0 Fix flaky 2024-03-13 22:51:55 +09:00
f0f698f411 Fix tests (Fix #5831) 2024-03-13 22:29:39 +09:00
22c6468a5d Wallet: Label filter dropdown (#5802)
Uses a dropdown component for the label filter, which also work on mobile. Closes #5722.
2024-03-12 10:48:37 +01:00
a60072a431 do not have report name conflict with old plugin (#5826)
* do not have report name conflict with old plugin

* tryadd instead of add

* Apply #5816 to crowdfund too
2024-03-11 14:18:47 +01:00
dcc6f17c9c POS: Fix exception when asking for data with a top up item (#5816)
Fixes #5811.
2024-03-11 11:05:44 +01:00
15ce148b99 Apps: Make app name the default title (#5779)
* Apps: Make app name the default title

Successor of #5762 with a way simpler approach. Allows the user-facing title to be set, but defaults it to the app name instead of "Tea shop".

* Test fixes
2024-03-11 11:04:41 +01:00
3b73d5a5cb bump client version 2024-03-08 19:56:04 +09:00
1fd3054006 Fix: Old payments not showing up in reports (#5812) 2024-03-05 16:10:54 +09:00
2db1434929 Fix currency-api link (#5803) 2024-03-04 10:10:30 +09:00
a171671fe5 Update .NET 8.0 in vscode launch.json (#5805) 2024-03-01 22:07:34 +01:00
9160a1d71e Store Logo: Remove restriction of square dimension (#5738)
As discussed on #5718, there is no need for the store logo to be provided in square dimension. As it populates its own row on the public page layouts, we can remove that restriction and make it adaptable by providing only maximum height and width.
2024-02-29 09:34:28 +01:00
a896560a3c Lightning Setup page fixes (#5796)
* Lightning Setup: Fix missing headline

Fixes #5795.

* Lightning Setup: Fix tab switching UI glitch

Fixes #5778.
2024-02-29 06:56:34 +01:00
e43b4ed540 Onboarding: Invite new users (#5714)
* Server Users: More precise message when inviting users

This lets the admin who invited a new user know whether or not an email has been sent. If the SMTP server hasn't been set up, they need to share the invite link with the user.

* Onboarding: Invite new users

- Separates the user self-registration and invite cases
- Adds invitation email for users created by the admin
- Adds invitation tokens to verify user was invited
- Adds handler action for invite links
- Refactors `UserEventHostedService`

* Remove duplicate status message from views that use the wizard layout

* Auto-approve users created by an admin

* Notify admins via email if a new account requires approval

* Update wording

* Fix update user error

* Fix redirect to email confirmation in invite action

* Fix precondition checks after signup

* Improve admin notification

Send notification only if the user does not require email confirmation or when they confirmed their email address. Rationale: We want to inform admins only about qualified users and not annoy them with bot registrations.

* Allow approval alongside resending confirm email

* Use user email in log messages instead of ID

* Prevent unnecessary notification after email confirmation

* Use ApplicationUser type explicitly

* Fix after rebase

* Refactoring: Do not subclass UserRegisteredEvent
2024-02-28 20:43:18 +09:00
8b446e2791 Reposition the camera scan icon in the wallet > send functionality (#5790)
* Reposition the camera scan icon in the wallet > send functionality

* refactored changes

* Minor adjustments

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-02-28 10:41:17 +01:00
d72b0e4cee Merge pull request #5610 from NicolasDorier/warnignremov
Remove disclaimer for lightning being in experimentation
2024-02-24 21:33:26 +05:00
22996ea21e UI: Deprecate the custom CSS options (#5735)
* UI: Deprecate the custom CSS options

As discussed with @pavlenex we are deprecating the custom CSS options for particular entities (payemnt request, pull payment, POS, crowdfund) in favor of the store branding approach.

This displays the custom CSS section only if these values are set already.

* Add IDs to html element

This will allow styling of individual entities.
2024-02-23 13:42:00 +01:00
d55770cc16 Admin overview of the stores on the instance (#5745)
* Admin overview of the stores on the instance

POC/Draft for #5674.

* Enable admin to access foreign stores

* Remove stores list link

* UI updates

* Grant admins guest access to foreign stores

* Optimize cookie auth handler

* Test fix

* Revert changes related to StoreRepository.FindStore with isAdmin
2024-02-23 09:51:41 +01:00
5c98ca180a Webhook tests + FIXES + DOCS (#5686)
* webhook tests

* fixes and add docs

* Do not update FormResponse and StoreId in update/create PullPayment

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-02-23 09:44:42 +01:00
10bb75ce0e Add debug flaky test statement 2024-02-22 19:08:01 +09:00
b9e3686fcf Fix build warnings and flaky tests (#5780)
* Make checkout v2 selenium tests more robust

* Fix build warnings

* Make payjoin test more robust

* Make LNURL test more robust
2024-02-22 09:38:06 +09:00
4ae1046571 Server Settings: Customize instance name and add contact URL (#5718)
* Server Settings: Customize instance name and add contact URL

- The custom instance name would improve #5563
- Added contact URL closes #4806

* Fix custom logo display
2024-02-21 20:54:39 +01:00
147c6c4548 HTML Sanitizer updates (#5736)
* Update HTML sanitizer package

* Remove unused sanitizer from apps

* Allow mailto: links

Fixes #5728.
2024-02-21 20:53:24 +01:00
354338180b Store: Move support URL to Checkout Appearance and improve wording (#5717)
As discussed in the recent design meeting.
2024-02-21 18:50:38 +01:00
5939e19f72 Lightning: Replace user info in server URL when logging (#5750)
* Lightning: Replace user info in server URL when logging

Fixes #5747.

* Fix empty user info case
2024-02-21 14:45:05 +01:00
f72a6df55a Add legacy report (#5740) 2024-02-21 14:44:49 +01:00
9c95b98f3a Policies: Cleanup and improvements (#5731)
* Policies: Turn checkboxes into toggles

* Move email policy to server email settings

* Move maintenance policies to server maintenance settings

* Policies: Adjust spacings

* Policies: Remove DisableInstantNotifications setting

* Wording updates

* Move maintenance settings back
2024-02-21 14:43:44 +01:00
55a8ba0905 Adding link to API usage examples in docs. (#5772) 2024-02-21 14:42:15 +01:00
04037b3d2d Crowdfund : Add Buyer information / Additional information(forms) like POS (#5659)
* Crowfund : Add Buyer information / Additional information(forms) like POS

* PR 5659 - changes

* Cleanups

* fix perk

* Crowdfund form tests

* Add Selenium test for Crowfund

* Selenium update

* update Selenium

* selenium update

* update selenium

* Test fixes and view improvements

* Cleanups

* do not use hacky form element for form detection

---------

Co-authored-by: nisaba <infos@nisaba.solutions>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: Kukks <evilkukka@gmail.com>
2024-02-21 14:41:21 +01:00
4943c84655 Invoice: Improve events display (#5775)
Closes #5773.

- Adds seconds to the displayed date and time
- Adds a tooltip that displays the full date and time including milliseconds
- Reintroduced the colored text in case of unusual events/states (this didn't work before)
2024-02-21 14:08:28 +01:00
42a8160768 UI: Make store selector list scrollable if necessary (#5760)
Fixes #5754.
2024-02-21 13:34:47 +01:00
33d3a25928 Apps: Don't redirect .onion requests to canonical domain (#5776)
Fixes #5729.
2024-02-21 13:34:12 +01:00
c2acff81c6 Fix: Labels wouldn't be properly applied to some wallet's transactions (#5770) 2024-02-20 18:42:38 +09:00
214d4b0c3f Support 16mb psbts. Potentially fixes #5768 2024-02-19 14:14:41 +01:00
bd4cf61c2b potentially fix #5764 2024-02-19 13:01:08 +01:00
b592ee2fed Bumping LND to 0.17.4-beta (#5739)
* Clarifying that only onchain funds will be restored to the wallet

Off chain recovery would need to be done with channel.backup file which is not part of this process

* Adding powershell version of lncli invoker

* Bumping LND to 0.17.4-beta-rc1

* Bumping LND to 0.17.4-beta
2024-02-16 08:43:51 -06:00
c57e1cca25 Shopify: Improve instruction display (#5752) 2024-02-16 09:36:41 +01:00
335f345ce3 Update GeneralSettings.cshtml (#5748)
Removed trailing data
2024-02-13 09:48:16 +01:00
b7be93c569 Update NTag424 lib 2024-02-08 19:12:14 +09:00
cd01a7b727 Improve performance of payout db queries 2024-02-08 16:44:03 +09:00
b96e73a002 Fix: Payouts state could turn cancelled even if payment was successful 2024-02-08 16:32:41 +09:00
0bf22ddf29 Do not require user approval by default (#5733)
As discussed on Mattermost.
2024-02-06 17:04:18 +09:00
1c4dc382a8 Merge pull request #5683 from pavlenex/release-cycles-doc
Create RELEASE-CYCLES.md
2024-02-05 19:42:15 +05:00
71c5566f2b Dashboard: Tooltip for balance on a particular day (#5650)
Closes #5617.
2024-02-02 11:29:35 +01:00
6621859567 remove decimals for Colombian (COP) and Argentina's Peso (ARS) (#5710)
* remove decimals for Colombian (COP) and Argentina's Peso (ARS)

* remove js currency hardcoding

* Fixes removal of columbia and argentina's peso

* Refactor

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-02-02 17:16:13 +09:00
6437967e60 Fix: Closing Balance in Dashboard was showing incorrect value (#5716) 2024-02-01 15:13:05 +09:00
c5a926c50c Fix Kraken rate for LTC 2024-02-01 14:45:59 +09:00
85ab691b68 bump version 2024-02-01 14:17:14 +09:00
4d3e0ab599 Changelog 2024-02-01 10:13:18 +09:00
02663a149e Fix Kraken API 2024-02-01 10:09:32 +09:00
a8fdc4798d Remove randomize RBF from wallet UI advanced settings (#5709)
* Remove randomize RBF from wallet UI advanced settings

* remove support RBF and allow bump fee from wallet send model

* update psbt RBF
2024-01-31 21:04:19 +09:00
6290b0f3bf Admins can approve registered users (#5647)
* Users list: Cleanups

* Policies: Flip registration settings

* Policies: Add RequireUserApproval setting

* Add approval to user

* Require approval on login and for API key

* API handling

* AccountController cleanups

* Test fix

* Apply suggestions from code review

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>

* Add missing imports

* Communicate login requirements to user on account creation

* Add login requirements to basic auth handler

* Cleanups and test fix

* Encapsulate approval logic in user service and log approval changes

* Send follow up "Account approved" email

Closes #5656.

* Add notification for admins

* Fix creating a user via the admin view

* Update list: Unify flags into status column, add approve action

* Adjust "Resend email" wording

* Incorporate feedback from code review

* Remove duplicate test server policy reset

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-01-31 14:45:54 +09:00
411e0334d0 Bitnob rate provider (#5705)
* Bitnob rate provider

* Add Bitnob as recommended exchange for NGN
2024-01-30 10:18:42 +09:00
b174977bc7 Store Email Settings: Improve configuration (#5629)
* Store Email Settings: Improve configuration

This works with the existing settings and provides better guidance about the different store email cases. Closes #5623.

* Split email and notification settings
2024-01-26 10:28:50 +01:00
95bf60c252 Create RELEASE-CYCLES.md
This PR adds documentation around release cycles in BTCPay and tends to outline processes and ensures there's documented structure on roles and responsibilities. Feedback welcome.
2024-01-20 13:44:39 +01:00
f9a43b537f Remove disclaimer for lightning being in experimentation 2023-12-24 11:12:51 +09:00
545 changed files with 16523 additions and 17260 deletions

1
.gitignore vendored
View File

@ -300,3 +300,4 @@ Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json
BTCPayServer/appsettings.dev.json
BTCPayServer.Tests/monero_wallet
/BTCPayServer.Tests/NewBlocks.bat

2
.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net6.0/BTCPayServer.dll",
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net8.0/BTCPayServer.dll",
"args": [],
"cwd": "${workspaceFolder}/BTCPayServer",
"stopAtEntry": false,

View File

@ -31,7 +31,7 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />

View File

@ -1,5 +1,4 @@
using System;
using System.Data.Common;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

View File

@ -1,19 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians.Client;
public class AssetQuoteResult
{
public string FromAsset { get; set; }
public string ToAsset { get; set; }
public decimal Bid { get; set; }
public decimal Ask { get; set; }
public AssetQuoteResult() { }
public AssetQuoteResult(string fromAsset, string toAsset, decimal bid, decimal ask)
{
FromAsset = fromAsset;
ToAsset = toAsset;
Bid = bid;
Ask = ask;
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class AssetBalancesUnavailableException : CustodianApiException
{
public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e)
{
}
public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}")
{
}
}

View File

@ -1,13 +0,0 @@
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians;
public class AssetQuoteUnavailableException : CustodianApiException
{
public AssetPairData AssetPair { get; }
public AssetQuoteUnavailableException(AssetPairData assetPair) : base(400, "asset-price-unavailable", "Cannot find a quote for pair " + assetPair)
{
this.AssetPair = assetPair;
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace BTCPayServer.Abstractions.Custodians;
public class BadConfigException : CustodianApiException
{
public string[] BadConfigKeys { get; set; }
public BadConfigException(string[] badConfigKeys) : base(500, "bad-custodian-account-config", "Wrong config values: " + String.Join(", ", badConfigKeys))
{
this.BadConfigKeys = badConfigKeys;
}
}

View File

@ -1,13 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class CannotWithdrawException : CustodianApiException
{
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string message) : base(403, "cannot-withdraw", message)
{
}
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "cannot-withdraw", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
{
}
}

View File

@ -1,18 +0,0 @@
using System;
namespace BTCPayServer.Abstractions.Custodians;
public class CustodianApiException : Exception
{
public int HttpStatus { get; }
public string Code { get; }
public CustodianApiException(int httpStatus, string code, string message, System.Exception ex) : base(message, ex)
{
HttpStatus = httpStatus;
Code = code;
}
public CustodianApiException(int httpStatus, string code, string message) : this(httpStatus, code, message, null)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class CustodianFeatureNotImplementedException : CustodianApiException
{
public CustodianFeatureNotImplementedException(string message) : base(400, "not-implemented", message)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class DepositsUnavailableException : CustodianApiException
{
public DepositsUnavailableException(string message) : base(404, "deposits-unavailable", message)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class InsufficientFundsException : CustodianApiException
{
public InsufficientFundsException(string message) : base(400, "insufficient-funds", message)
{
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class InvalidWithdrawalTargetException : CustodianApiException
{
public InvalidWithdrawalTargetException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "invalid-withdrawal-target", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
{
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class PermissionDeniedCustodianApiException : CustodianApiException
{
public PermissionDeniedCustodianApiException(ICustodian custodian) : base(403, "custodian-api-permission-denied", $"{custodian.Name}'s API reported that you don't have permission.")
{
}
}

View File

@ -1,11 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class TradeNotFoundException : CustodianApiException
{
private string tradeId { get; }
public TradeNotFoundException(string tradeId) : base(404, "trade-not-found", "Could not find trade ID " + tradeId)
{
this.tradeId = tradeId;
}
}

View File

@ -1,11 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class WithdrawalNotFoundException : CustodianApiException
{
private string WithdrawalId { get; }
public WithdrawalNotFoundException(string withdrawalId) : base(404, "withdrawal-not-found", $"Could not find withdrawal ID {withdrawalId}.")
{
WithdrawalId = withdrawalId;
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class WrongTradingPairException : CustodianApiException
{
public const int HttpCode = 404;
public WrongTradingPairException(string fromAsset, string toAsset) : base(HttpCode, "wrong-trading-pair", $"Cannot find a trading pair for converting {fromAsset} into {toAsset}.")
{
}
}

View File

@ -1,29 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians.Client;
/**
* The result of a market trade. Used as a return type for custodians implementing ICanTrade
*/
public class MarketTradeResult
{
public string FromAsset { get; }
public string ToAsset { get; }
/**
* The ledger entries that show the balances that were affected by the trade.
*/
public List<LedgerEntryData> LedgerEntries { get; }
/**
* The unique ID of the trade that was executed.
*/
public string TradeId { get; }
public MarketTradeResult(string fromAsset, string toAsset, List<LedgerEntryData> ledgerEntries, string tradeId)
{
this.FromAsset = fromAsset;
this.ToAsset = toAsset;
this.LedgerEntries = ledgerEntries;
this.TradeId = tradeId;
}
}

View File

@ -1,28 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class SimulateWithdrawalResult
{
public string PaymentMethod { get; }
public string Asset { get; }
public decimal MinQty { get; }
public decimal MaxQty { get; }
public List<LedgerEntryData> LedgerEntries { get; }
// Fee can be NULL if unknown.
public decimal? Fee { get; }
public SimulateWithdrawalResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries,
decimal minQty, decimal maxQty)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
MinQty = minQty;
MaxQty = maxQty;
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class WithdrawResult
{
public string PaymentMethod { get; }
public string Asset { get; set; }
public List<LedgerEntryData> LedgerEntries { get; }
public string WithdrawalId { get; }
public WithdrawalResponseData.WithdrawalStatus Status { get; }
public DateTimeOffset CreatedTime { get; }
public string TargetAddress { get; }
public string TransactionId { get; }
public WithdrawResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, WithdrawalResponseData.WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
WithdrawalId = withdrawalId;
CreatedTime = createdTime;
Status = status;
TargetAddress = targetAddress;
TransactionId = transactionId;
}
}

View File

@ -1,17 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICanDeposit
{
/**
* Get the address where we can deposit for the chosen payment method (crypto code + network).
* The result can be a string in different formats like a bitcoin address or even a LN invoice.
*/
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken);
public string[] GetDepositablePaymentMethods();
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICanTrade
{
/**
* A list of tradable asset pairs, or NULL if the custodian cannot trade/convert assets. if thr asset pair contains fiat, fiat is always put last. If both assets are a cyrptocode or both are fiat, the pair is written alphabetically. Always in uppercase. Example: ["BTC/EUR","BTC/USD", "EUR/USD", "BTC/ETH",...]
*/
public List<AssetPairData> GetTradableAssetPairs();
/**
* Execute a market order right now.
*/
public Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken);
/**
* Get the details about a previous market trade.
*/
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken);
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken);
}

View File

@ -1,20 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
/// <summary>
/// Interface for custodians that can move funds to the store wallet.
/// </summary>
public interface ICanWithdraw
{
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);
public string[] GetWithdrawablePaymentMethods();
}

View File

@ -1,26 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICustodian
{
/**
* Get the unique code that identifies this custodian.
*/
string Code { get; }
string Name { get; }
/**
* Get a list of assets and their qty in custody.
*/
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
public Task<Form.Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default);
}

View File

@ -1,14 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Custodians;
namespace BTCPayServer.Abstractions.Extensions;
public static class CustodianExtensions
{
public static ICustodian? GetCustodianByCode(this IEnumerable<ICustodian> custodians, string code)
{
return custodians.FirstOrDefault(custodian => custodian.Code == code);
}
}

View File

@ -101,6 +101,14 @@ namespace BTCPayServer.Abstractions.Extensions
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
{
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
var displayDate = date.ToString(netFormat, CultureInfo.InvariantCulture);
var tooltip = dateTime.Replace("T", " ");
return new HtmlString($"<time datetime=\"{dateTime}\" data-date-style=\"{jsDateFormat}\" data-time-style=\"{jsTimeFormat}\" data-initial=\"localized\" data-bs-toggle=\"tooltip\" data-bs-title=\"{tooltip}\">{displayDate}</time>");
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
{
var relative = date.ToTimeAgo();

View File

@ -0,0 +1,35 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement("form", Attributes = "[permissioned]")]
public partial class PermissionedFormTagHelper(
IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor)
: TagHelper
{
public string Permissioned { get; set; }
public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (httpContextAccessor.HttpContext is null || string.IsNullOrEmpty(Permissioned))
return;
var res = await authorizationService.AuthorizeAsync(httpContextAccessor.HttpContext.User,
PermissionResource, Permissioned);
if (!res.Succeeded)
{
var content = await output.GetChildContentAsync();
var html = SubmitButtonRegex().Replace(content.GetContent(), "");
output.Content.SetHtmlContent($"<fieldset disabled>{html}</fieldset>");
}
}
[GeneratedRegex("<(button|input).*?type=\"submit\".*?>.*?</\\1>")]
private static partial Regex SubmitButtonRegex();
}

View File

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>

View File

@ -1,102 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<CustodianAccountData>> GetCustodianAccounts(string storeId, bool includeAssetBalances = false, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includeAssetBalances)
{
queryPayload.Add("assetBalances", "true");
}
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", queryPayload), token);
return await HandleResponse<IEnumerable<CustodianAccountData>>(response);
}
public virtual async Task<CustodianAccountResponse> GetCustodianAccount(string storeId, string accountId, bool includeAssetBalances = false, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includeAssetBalances)
{
queryPayload.Add("assetBalances", "true");
}
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", queryPayload), token);
return await HandleResponse<CustodianAccountResponse>(response);
}
public virtual async Task<CustodianAccountData> CreateCustodianAccount(string storeId, CreateCustodianAccountRequest request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<CustodianAccountData>(response);
}
public virtual async Task<CustodianAccountData> UpdateCustodianAccount(string storeId, string accountId, CreateCustodianAccountRequest request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", bodyPayload: request, method: HttpMethod.Put), token);
return await HandleResponse<CustodianAccountData>(response);
}
public virtual async Task DeleteCustodianAccount(string storeId, string accountId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<DepositAddressData> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token);
return await HandleResponse<DepositAddressData>(response);
}
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
{
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
//return await HandleResponse<ApplicationUserData>(response);
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
request, HttpMethod.Post);
var response = await _httpClient.SendAsync(internalRequest, token);
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token);
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<TradeQuoteResponseData> GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
queryPayload.Add("fromAsset", fromAsset);
queryPayload.Add("toAsset", toAsset);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote", queryPayload), token);
return await HandleResponse<TradeQuoteResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalResponseData>(response);
}
public virtual async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalSimulationResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token);
return await HandleResponse<WithdrawalResponseData>(response);
}
}
}

View File

@ -1,16 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<CustodianData>> GetCustodians(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/custodians"), token);
return await HandleResponse<IEnumerable<CustodianData>>(response);
}
}
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay",
query), token);
return await HandleResponse<IEnumerable<LNURLPayPaymentMethodData>>(response);
}
public virtual async Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, LNURLPayPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
}
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork",
query), token);
return await HandleResponse<IEnumerable<LightningNetworkPaymentMethodData>>(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> GetStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}"), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLightningNetworkPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, UpdateLightningNetworkPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
}
}

View File

@ -3,92 +3,47 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
public partial class BTCPayServerClient
{
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, string derivationScheme, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
bodyPayload: new UpdatePaymentMethodRequest() { Config = JValue.CreateString(derivationScheme) },
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain",
query), token);
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodData> GetStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<GenerateOnChainWalletResponse>(response);
}
public virtual async Task RemoveStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
bodyPayload: paymentMethod,
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);
}
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
@ -7,21 +8,60 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId,
bool? enabled = null,
public virtual async Task<GenericPaymentMethodData> UpdateStorePaymentMethod(
string storeId,
string paymentMethodId,
UpdatePaymentMethodRequest request,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", bodyPayload: request, method: HttpMethod.Put),
token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task RemoveStorePaymentMethod(string storeId, string paymentMethodId)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", method: HttpMethod.Delete),
CancellationToken.None);
await HandleResponse(response);
}
public virtual async Task<GenericPaymentMethodData> GetStorePaymentMethod(string storeId,
string paymentMethodId, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
if (includeConfig != null)
{
query.Add(nameof(enabled), enabled);
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}",
query), token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task<GenericPaymentMethodData[]> GetStorePaymentMethods(string storeId,
bool? onlyEnabled = null, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (onlyEnabled != null)
{
query.Add(nameof(onlyEnabled), onlyEnabled);
}
if (includeConfig != null)
{
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods",
query), token);
return await HandleResponse<Dictionary<string, GenericPaymentMethodData>>(response);
return await HandleResponse<GenericPaymentMethodData[]>(response);
}
}
}

View File

@ -41,6 +41,14 @@ namespace BTCPayServer.Client
return response.IsSuccessStatusCode;
}
public virtual async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/approve", null,
new ApproveUserRequest { Approved = approved }, HttpMethod.Post), token);
await HandleResponse(response);
return response.IsSuccessStatusCode;
}
public virtual async Task<ApplicationUserData[]> GetUsers(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);

View File

@ -0,0 +1,43 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
namespace BTCPayServer.Client.JsonConverters
{
public class SaneOutpointJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(OutPoint).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
try
{
if (!OutPoint.TryParse((string)reader.Value, out var outpoint))
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
return outpoint;
}
catch (EndOfStreamException)
{
}
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is { })
writer.WriteValue(value.ToString());
}
}
}

View File

@ -1,36 +0,0 @@
using System;
using System.Globalization;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.JsonConverters
{
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
{
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.String:
if (TradeQuantity.TryParse(token.ToString(), out var q))
return q;
break;
case JTokenType.Null:
return null;
}
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
}
public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer)
{
if (value is not null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -25,6 +25,16 @@ namespace BTCPayServer.Client.Models
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
/// <summary>
/// Whether the user was approved by an admin
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// whether the user needed approval on account creation
/// </summary>
public bool RequiresApproval { get; set; }
/// <summary>
/// the roles of the user
/// </summary>

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client;
public class ApproveUserRequest
{
public bool Approved { get; set; }
}

View File

@ -26,6 +26,7 @@ namespace BTCPayServer.Client.Models
public string Template { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public PosViewType DefaultView { get; set; }
public bool ShowItems { get; set; } = false;
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = false;
public bool ShowSearch { get; set; } = true;

View File

@ -1,12 +0,0 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class CreateCustodianAccountRequest
{
public string CustodianCode { get; set; }
public string Name { get; set; }
public JObject Config { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -21,7 +22,7 @@ namespace BTCPayServer.Client.Models
public bool ProceedWithPayjoin { get; set; } = true;
public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
[JsonProperty(ItemConverterType = typeof(SaneOutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")]

View File

@ -1,16 +0,0 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public abstract class CustodianAccountBaseData
{
public string CustodianCode { get; set; }
public string Name { get; set; }
public string StoreId { get; set; }
public JObject Config { get; set; }
}
}

View File

@ -1,7 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class CustodianAccountData : CustodianAccountBaseData
{
public string Id { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class CustodianAccountResponse : CustodianAccountData
{
public IDictionary<string, decimal> AssetBalances { get; set; }
public CustodianAccountResponse()
{
}
}

View File

@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class CustodianData
{
public string Code { get; set; }
public string Name { get; set; }
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
public string[] WithdrawablePaymentMethods { get; set; }
public string[] DepositablePaymentMethods { get; set; }
}

View File

@ -1,15 +0,0 @@
namespace BTCPayServer.Client.Models;
public class DepositAddressData
{
// /**
// * Example: P2PKH, P2SH, P2WPKH, P2TR, BOLT11, ...
// */
// public string Type { get; set; }
/**
* Format depends hugely on the type.
*/
public string Address { get; set; }
}

View File

@ -1,3 +1,5 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class EmailSettingsData
@ -26,4 +28,11 @@ public class EmailSettingsData
get; set;
}
public bool DisableCertificateCheck { get; set; }
[JsonIgnore]
public bool EnabledCertificateCheck
{
get => !DisableCertificateCheck;
set { DisableCertificateCheck = !value; }
}
}

View File

@ -1,7 +1,10 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
@ -22,4 +25,16 @@ namespace BTCPayServer.Client
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
}
public class GenerateOnChainWalletResponse : GenericPaymentMethodData
{
public class ConfigData
{
public string AccountDerivation { get; set; }
[JsonExtensionData]
IDictionary<string, JToken> AdditionalData { get; set; }
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
public new ConfigData Config { get; set; }
}
}

View File

@ -1,9 +1,22 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class GenericPaymentMethodData
{
public bool Enabled { get; set; }
public object Data { get; set; }
public string CryptoCode { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public JToken Config { get; set; }
public string PaymentMethodId { get; set; }
}
public class UpdatePaymentMethodRequest
{
public UpdatePaymentMethodRequest()
{
}
public bool? Enabled { get; set; }
public JToken Config { get; set; }
}
}

View File

@ -29,13 +29,12 @@ namespace BTCPayServer.Client.Models
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal NetworkFee { get; set; }
public decimal PaymentMethodFee { get; set; }
public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public JObject AdditionalData { get; set; }
public string PaymentMethodId { get; set; }
public JToken AdditionalData { get; set; }
public string Currency { get; set; }
public class Payment
{

View File

@ -1,17 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
[JsonProperty("lud12Enabled")]
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()
{
}
}
}

View File

@ -1,27 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodData : LNURLPayPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LNURLPayPaymentMethodData()
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
LUD12Enabled = lud12Enabled;
}
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodBaseData
{
public string ConnectionString { get; set; }
public LightningNetworkPaymentMethodBaseData()
{
}
}
}

View File

@ -1,29 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodData : LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LightningNetworkPaymentMethodData()
{
}
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled, string paymentMethod)
{
Enabled = enabled;
CryptoCode = cryptoCode;
ConnectionString = connectionString;
PaymentMethod = paymentMethod;
}
public string PaymentMethod { get; set; }
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class MarketTradeResponseData
{
public string FromAsset { get; }
public string ToAsset { get; }
/**
* The ledger entries that show the balances that were affected by the trade.
*/
public List<LedgerEntryData> LedgerEntries { get; }
/**
* The unique ID of the trade that was executed.
*/
public string TradeId { get; }
public string AccountId { get; }
public string CustodianCode { get; }
public MarketTradeResponseData(string fromAsset, string toAsset, List<LedgerEntryData> ledgerEntries, string tradeId, string accountId, string custodianCode)
{
FromAsset = fromAsset;
ToAsset = toAsset;
LedgerEntries = ledgerEntries;
TradeId = tradeId;
AccountId = accountId;
CustodianCode = custodianCode;
}
}

View File

@ -1,24 +0,0 @@
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodBaseData
{
/// <summary>
/// The derivation scheme
/// </summary>
public string DerivationScheme { get; set; }
public string Label { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public RootedKeyPath AccountKeyPath { get; set; }
public OnChainPaymentMethodBaseData()
{
}
}
}

View File

@ -1,47 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataPreview : OnChainPaymentMethodBaseData
{
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public OnChainPaymentMethodDataPreview()
{
}
public OnChainPaymentMethodDataPreview(string cryptoCode, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Label = label;
AccountKeyPath = accountKeyPath;
CryptoCode = cryptoCode;
DerivationScheme = derivationScheme;
}
}
public class OnChainPaymentMethodData : OnChainPaymentMethodDataPreview
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public string PaymentMethod { get; set; }
public OnChainPaymentMethodData()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath, string paymentMethod) :
base(cryptoCode, derivationScheme, label, accountKeyPath)
{
Enabled = enabled;
PaymentMethod = paymentMethod;
}
}
}

View File

@ -1,23 +0,0 @@
using BTCPayServer.Client.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataWithSensitiveData : OnChainPaymentMethodData
{
public OnChainPaymentMethodDataWithSensitiveData()
{
}
public OnChainPaymentMethodDataWithSensitiveData(string cryptoCode, string derivationScheme, bool enabled,
string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic, string paymentMethod) : base(cryptoCode, derivationScheme, enabled,
label, accountKeyPath, paymentMethod)
{
Mnemonic = mnemonic;
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -12,7 +13,7 @@ namespace BTCPayServer.Client.Models
public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))]
[JsonConverter(typeof(SaneOutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete

View File

@ -19,6 +19,7 @@ namespace BTCPayServer.Client.Models
{
public string Title { get; set; }
public string DefaultView { get; set; }
public bool ShowItems { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool ShowSearch { get; set; }

View File

@ -4,6 +4,7 @@ using System.Text;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -14,11 +15,15 @@ namespace BTCPayServer.Client.Models
}
public class RegisterBoltcardRequest
{
[JsonProperty("LNURLW")]
public string LNURLW { get; set; }
[JsonConverter(typeof(HexJsonConverter))]
[JsonProperty("UID")]
public byte[] UID { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public OnExistingBehavior? OnExisting { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
public class RegisterBoltcardResponse
{

View File

@ -1,22 +0,0 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeQuoteResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Bid { get; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Ask { get; }
public string ToAsset { get; }
public string FromAsset { get; }
public TradeQuoteResponseData(string fromAsset, string toAsset, decimal bid, decimal ask)
{
FromAsset = fromAsset;
ToAsset = toAsset;
Bid = bid;
Ask = ask;
}
}

View File

@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeRequestData
{
public string FromAsset { set; get; }
public string ToAsset { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
}

View File

@ -1,20 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class UpdateLightningNetworkPaymentMethodRequest : LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateLightningNetworkPaymentMethodRequest()
{
}
public UpdateLightningNetworkPaymentMethodRequest(string connectionString, bool enabled)
{
Enabled = enabled;
ConnectionString = connectionString;
}
}
}

View File

@ -1,25 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class UpdateOnChainPaymentMethodRequest : OnChainPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateOnChainPaymentMethodRequest()
{
}
public UpdateOnChainPaymentMethodRequest(bool enabled, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
DerivationScheme = derivationScheme;
}
}
}

View File

@ -7,11 +7,11 @@ namespace BTCPayServer.Client.Models
{
public class WebhookPayoutEvent : StoreWebhookEvent
{
public WebhookPayoutEvent(string evtType, string storeId)
public WebhookPayoutEvent(string type, string storeId)
{
if (!evtType.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType));
Type = evtType;
if (!type.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}
@ -21,11 +21,11 @@ namespace BTCPayServer.Client.Models
}
public class WebhookPaymentRequestEvent : StoreWebhookEvent
{
public WebhookPaymentRequestEvent(string evtType, string storeId)
public WebhookPaymentRequestEvent(string type, string storeId)
{
if (!evtType.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType));
Type = evtType;
if (!type.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}
@ -91,7 +91,7 @@ namespace BTCPayServer.Client.Models
}
public bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; }
public string PaymentMethodId { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
}

View File

@ -1,85 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http.Headers;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawRequestData
{
public string PaymentMethod { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
public WithdrawRequestData()
{
}
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
{
PaymentMethod = paymentMethod;
Qty = qty;
}
}
#nullable enable
public record TradeQuantity
{
public TradeQuantity(decimal value, ValueType type)
{
Type = type;
Value = value;
}
public enum ValueType
{
Exact,
Percent
}
public ValueType Type { get; }
public decimal Value { get; set; }
public override string ToString()
{
if (Type == ValueType.Exact)
return Value.ToString(CultureInfo.InvariantCulture);
else
return Value.ToString(CultureInfo.InvariantCulture) + "%";
}
public static TradeQuantity Parse(string str)
{
if (!TryParse(str, out var r))
throw new FormatException("Invalid TradeQuantity");
return r;
}
public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity)
{
if (str is null)
throw new ArgumentNullException(nameof(str));
quantity = null;
str = str.Trim();
str = str.Replace(" ", "");
if (str.Length == 0)
return false;
if (str[^1] == '%')
{
if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent);
}
else
{
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact);
}
return true;
}
}

View File

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public abstract class WithdrawalBaseResponseData
{
public string Asset { get; }
public string PaymentMethod { get; }
public List<LedgerEntryData> LedgerEntries { get; }
public string AccountId { get; }
public string CustodianCode { get; }
public WithdrawalBaseResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string accountId,
string custodianCode)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
AccountId = accountId;
CustodianCode = custodianCode;
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public class WithdrawalResponseData : WithdrawalBaseResponseData
{
[JsonConverter(typeof(StringEnumConverter))]
public WithdrawalStatus Status { get; }
public string WithdrawalId { get; }
public DateTimeOffset CreatedTime { get; }
public string TransactionId { get; }
public string TargetAddress { get; }
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
custodianCode)
{
WithdrawalId = withdrawalId;
TargetAddress = targetAddress;
TransactionId = transactionId;
Status = status;
CreatedTime = createdTime;
}
public enum WithdrawalStatus
{
Unknown = 0,
Queued = 1,
Complete = 2,
Failed = 3
}
}

View File

@ -1,21 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MinQty { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MaxQty { get; set; }
public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId,
string custodianCode, List<LedgerEntryData> ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod,
asset, ledgerEntries, accountId, custodianCode)
{
MinQty = minQty;
MaxQty = maxQty;
}
}

View File

@ -40,11 +40,6 @@ namespace BTCPayServer.Client
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanViewPullPayments = "btcpay.store.canviewpullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts";
public const string CanDepositToCustodianAccounts = "btcpay.store.candeposittocustodianaccount";
public const string CanWithdrawFromCustodianAccounts = "btcpay.store.canwithdrawfromcustodianaccount";
public const string CanTradeCustodianAccount = "btcpay.store.cantradecustodianaccount";
public const string Unrestricted = "unrestricted";
public static IEnumerable<string> AllPolicies
{
@ -79,11 +74,6 @@ namespace BTCPayServer.Client
yield return CanCreatePullPayments;
yield return CanViewPullPayments;
yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts;
yield return CanManageCustodianAccounts;
yield return CanDepositToCustodianAccounts;
yield return CanWithdrawFromCustodianAccounts;
yield return CanTradeCustodianAccount;
yield return CanManageUsers;
yield return CanManagePayouts;
yield return CanViewPayouts;
@ -254,7 +244,6 @@ namespace BTCPayServer.Client
{
var policyMap = new Dictionary<string, HashSet<string>>();
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
Policies.CanManageCustodianAccounts,
Policies.CanManagePullPayments,
Policies.CanModifyInvoices,
Policies.CanViewStoreSettings,
@ -275,7 +264,6 @@ namespace BTCPayServer.Client
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts);
PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts);

View File

@ -130,6 +130,11 @@ namespace BTCPayServer
{
return transactionInformationSet;
}
public string GetTrackedDestination(Script scriptPubKey)
{
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
}
}
public abstract class BTCPayNetworkBase

View File

@ -39,7 +39,6 @@ namespace BTCPayServer.Data
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
public DbSet<APIKeyData> ApiKeys { get; set; }
public DbSet<AppData> Apps { get; set; }
public DbSet<CustodianAccountData> CustodianAccount { get; set; }
public DbSet<StoredFile> Files { get; set; }
public DbSet<InvoiceEventData> InvoiceEvents { get; set; }
public DbSet<InvoiceSearchData> InvoiceSearches { get; set; }
@ -94,7 +93,6 @@ namespace BTCPayServer.Data
AddressInvoiceData.OnModelCreating(builder);
APIKeyData.OnModelCreating(builder, Database);
AppData.OnModelCreating(builder, Database);
CustodianAccountData.OnModelCreating(builder, Database);
//StoredFile.OnModelCreating(builder);
InvoiceEventData.OnModelCreating(builder);
InvoiceSearchData.OnModelCreating(builder);
@ -107,10 +105,10 @@ namespace BTCPayServer.Data
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PayoutData.OnModelCreating(builder);
PayoutData.OnModelCreating(builder, Database);
PendingInvoiceData.OnModelCreating(builder);
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder, Database);
RefundData.OnModelCreating(builder);
SettingData.OnModelCreating(builder, Database);
StoreSettingData.OnModelCreating(builder, Database);

View File

@ -17,6 +17,7 @@ namespace BTCPayServer.Data
public override ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options);
}

View File

@ -8,6 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.23" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -6,11 +6,6 @@ namespace BTCPayServer.Data
{
public class AddressInvoiceData
{
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetHash instead")]
public string Address { get; set; }
public InvoiceData InvoiceData { get; set; }
public string InvoiceDataId { get; set; }

View File

@ -11,6 +11,8 @@ namespace BTCPayServer.Data
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
{
public bool RequiresEmailConfirmation { get; set; }
public bool RequiresApproval { get; set; }
public bool Approved { get; set; }
public List<StoredFile> StoredFiles { get; set; }
[Obsolete("U2F support has been replace with FIDO2")]
public List<U2FDevice> U2FDevices { get; set; }

View File

@ -1,54 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data;
public class CustodianAccountData : IHasBlob<JObject>
{
[Required]
[MaxLength(50)]
public string Id { get; set; }
[Required]
[MaxLength(50)]
public string StoreId { get; set; }
[Required]
[MaxLength(50)]
public string CustodianCode { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[JsonIgnore]
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
[JsonIgnore]
public string Blob2 { get; set; }
[JsonIgnore]
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<CustodianAccountData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.CustodianAccounts)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<CustodianAccountData>()
.HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<CustodianAccountData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}

View File

@ -0,0 +1,370 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.IO.Compression;
using System.IO;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Globalization;
using Newtonsoft.Json;
using Microsoft.EntityFrameworkCore.Diagnostics;
using BTCPayServer.Migrations;
using Newtonsoft.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
namespace BTCPayServer.Data
{
public partial class InvoiceData
{
/// <summary>
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
/// </summary>
public class MigrationInterceptor : IMaterializationInterceptor
{
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is InvoiceData invoiceData && invoiceData.Currency is null)
{
invoiceData.Migrate();
}
else if (entity is PaymentData paymentData && paymentData.Currency is null)
{
paymentData.Migrate();
}
return entity;
}
}
static HashSet<string> superflousProperties = new HashSet<string>()
{
"availableAddressHashes",
"events",
"refunds",
"paidAmount",
"historicalAddresses",
"refundable",
"status",
"exceptionStatus",
"storeId",
"id",
"txFee",
"refundMail",
"rate",
"depositAddress",
"currency",
"price",
"payments",
"orderId",
"buyerInformation",
"productInformation",
"derivationStrategy",
"archived",
"isUnderPaid",
"requiresRefundEmail",
"invoiceTime"
};
#pragma warning disable CS0618 // Type or member is obsolete
public void Migrate()
{
if (Currency is not null)
return;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoData"]?["BTC"] is not (null or { Type: JTokenType.Null }))
{
blob.Move(["rate"], ["cryptoData", "BTC", "rate"]);
blob.Move(["txFee"], ["cryptoData", "BTC", "txFee"]);
}
blob.Move(["customerEmail"], ["metadata", "buyerEmail"]);
foreach (var prop in (blob["cryptoData"] as JObject)?.Properties()?.ToList() ?? [])
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
{
if (prop.Value is JObject pm)
{
pm.Remove("depositAddress");
pm.Remove("feeRate");
pm.Remove("txFee");
}
continue;
}
if (prop.Value is JObject o)
{
o.ConvertNumberToString("rate");
if (o["paymentMethod"] is JObject pm)
{
if (pm["networkFeeRate"] is null)
pm["networkFeeRate"] = o["feeRate"] ?? 0.0m;
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
pm.Remove("networkFeeMode");
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 2 or 2L })
pm["networkFeeRate"] = 0.0m;
}
}
}
var metadata = blob.Property("metadata")?.Value as JObject;
if (metadata is null)
{
metadata = new JObject();
blob.Add("metadata", metadata);
}
foreach (var prop in (blob["buyerInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Value?.Value<string>() is not null)
blob.Move(["buyerInformation", prop.Name], ["metadata", prop.Name]);
}
foreach (var prop in (blob["productInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Name is "price" or "currency")
blob.Move(["productInformation", prop.Name], [prop.Name]);
else if (prop.Value?.Value<string>() is not null)
blob.Move(["productInformation", prop.Name], ["metadata", prop.Name]);
}
blob.Move(["orderId"], ["metadata", "orderId"]);
foreach (string prop in new string[] { "posData", "checkoutType", "defaultLanguage", "notificationEmail", "notificationURL", "storeSupportUrl", "redirectURL" })
{
blob.RemoveIfNull(prop);
}
blob.RemoveIfValue<bool>("fullNotifications", false);
if (blob["receiptOptions"] is JObject receiptOptions)
{
foreach (string prop in new string[] { "showQR", "enabled", "showPayments" })
{
receiptOptions.RemoveIfNull(prop);
}
}
{
if (blob.Property("paymentTolerance") is JProperty { Value: { Type: JTokenType.Float } pv } prop)
{
if (pv.Value<decimal>() == 0.0m)
prop.Remove();
}
}
var posData = blob.Move(["posData"], ["metadata", "posData"]);
if (posData is not null && posData.Value?.Type is JTokenType.String)
{
try
{
posData.Value = JObject.Parse(posData.Value<string>());
}
catch
{
posData.Remove();
}
}
if (posData?.Type is JTokenType.Null)
posData.Remove();
if (blob["derivationStrategies"] is JValue { Type: JTokenType.String } v)
blob["derivationStrategies"] = JObject.Parse(v.Value<string>());
if (blob["derivationStrategies"] is JObject derivations)
{
foreach (var prop in derivations.Properties().ToList())
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
continue;
if (prop.Value is JValue
{
Type: JTokenType.String,
Value: String { Length: > 0 } val
})
{
if (val[0] == '{')
derivations[prop.Name] = JObject.Parse(val);
else
{
if (val.Contains('-', StringComparison.OrdinalIgnoreCase))
derivations[prop.Name] = new JObject() { ["accountDerivation"] = val };
else
derivations[prop.Name] = null;
}
}
if (prop.Value is JObject derivation)
{
derivations[prop.Name] = derivation["accountDerivation"];
}
}
}
if (blob["derivationStrategies"] is null && blob["derivationStrategy"] is not null)
{
// If it's NBX derivation strategy, keep it. Else just give up, it might be Electrum format and we shouldn't support
// that anymore in the backend for long...
if (blob["derivationStrategy"]?.Value<string>().Contains('-', StringComparison.OrdinalIgnoreCase) is true)
blob.Move(["derivationStrategy"], ["derivationStrategies", "BTC"]);
else
{
blob.Remove("derivationStrategy");
blob.Add("derivationStrategies", new JObject() { ["BTC"] = null });
}
}
if (blob["type"]?.Value<string>() is "Standard")
blob.Remove("type");
foreach (var prop in new string[] { "extendedNotifications", "lazyPaymentMethods", "lazyPaymentMethods", "redirectAutomatically" })
{
if (blob[prop]?.Value<bool>() is false)
blob.Remove(prop);
}
blob.ConvertNumberToString("price");
Currency = blob["currency"].Value<string>();
var isTopup = blob["type"]?.Value<string>() is "TopUp";
var amount = decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
Amount = isTopup && amount == 0 ? null : decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
CustomerEmail = null;
foreach (var prop in superflousProperties)
blob.Property(prop)?.Remove();
if (blob["speedPolicy"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
blob.Remove("speedPolicy");
blob.TryAdd("internalTags", new JArray());
blob.TryAdd("receiptOptions", new JObject());
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
if (prop.Name.EndsWith("_LightningLike", StringComparison.OrdinalIgnoreCase) ||
prop.Name.EndsWith("_LNURLPAY", StringComparison.OrdinalIgnoreCase))
{
if (prop.Value["paymentMethod"]?["PaymentHash"] is JObject)
prop.Value["paymentMethod"]["PaymentHash"] = JValue.CreateNull();
if (prop.Value["paymentMethod"]?["Preimage"] is JObject)
prop.Value["paymentMethod"]["Preimage"] = JValue.CreateNull();
}
}
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
var crypto = prop.Name.Split(['_', '-']).First();
if (blob.Move(["cryptoData", prop.Name, "rate"], ["rates", crypto]) is not null)
((JObject)blob["rates"]).ConvertNumberToString(crypto);
}
blob.Move(["cryptoData"], ["prompts"]);
var prompts = ((JObject)blob["prompts"]);
foreach (var prop in prompts.Properties().ToList())
{
((JObject)blob["prompts"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
blob["derivationStrategies"] = blob["derivationStrategies"] ?? new JObject();
foreach (var prop in ((JObject)blob["derivationStrategies"]).Properties().ToList())
{
((JObject)blob["derivationStrategies"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
foreach (var prop in prompts.Properties())
{
var prompt = prop.Value as JObject;
if (prompt is null)
continue;
prompt["currency"] = prop.Name.Split('-').First();
prompt.RemoveIfNull("depositAddress");
prompt.RemoveIfNull("txFee");
prompt.RemoveIfNull("feeRate");
prompt.RenameProperty("depositAddress", "destination");
prompt.RenameProperty("txFee", "paymentMethodFee");
var divisibility = MigrationExtensions.GetDivisibility(prop.Name);
prompt.Add("divisibility", divisibility);
if (prompt["paymentMethodFee"] is { Type: JTokenType.Integer } paymentMethodFee)
{
prompt["paymentMethodFee"] = ((decimal)paymentMethodFee.Value<long>() / (decimal)Math.Pow(10, divisibility)).ToString(CultureInfo.InvariantCulture);
prompt.RemoveIfValue<string>("paymentMethodFee", "0");
}
prompt.Move(["paymentMethod"], ["details"]);
prompt.Move(["feeRate"], ["details", "recommendedFeeRate"]);
prompt.Move(["details", "networkFeeRate"], ["details", "paymentMethodFeeRate"]);
prompt.Move(["details", "networkFeeMode"], ["details", "feeMode"]);
if ((prompt["details"]?["Activated"])?.Value<bool>() is bool activated)
{
((JObject)prompt["details"]).Remove("Activated");
prompt["inactive"] = !activated;
prompt.RemoveIfValue<bool>("inactive", false);
}
if ((prompt["details"]?["activated"])?.Value<bool>() is bool activated2)
{
((JObject)prompt["details"]).Remove("activated");
prompt["inactive"] = !activated2;
prompt.RemoveIfValue<bool>("inactive", false);
}
var details = prompt["details"] as JObject ?? new JObject();
details.RemoveIfValue<bool>("payjoinEnabled", false);
details.RemoveIfNull("feeMode");
if (details["feeMode"] is not (null or { Type: JTokenType.Null }))
{
details["feeMode"] = details["feeMode"].Value<int>() switch
{
1 => "Always",
2 => "Never",
_ => null
};
details.RemoveIfNull("feeMode");
}
details.RemoveIfNull("BOLT11");
details.RemoveIfNull("address");
details.RemoveIfNull("Address");
prompt.Move(["details", "BOLT11"], ["destination"]);
prompt.Move(["details", "address"], ["destination"]);
prompt.Move(["details", "Address"], ["destination"]);
prompt.RenameProperty("Address", "destination");
prompt.RenameProperty("BOLT11", "destination");
details.Remove("LightningSupportedPaymentMethod");
foreach (var o in detailsRemoveDefault)
details.RemoveIfNull(o);
details.RemoveIfValue<decimal>("recommendedFeeRate", 0.0m);
details.RemoveIfValue<decimal>("paymentMethodFeeRate", 0.0m);
if (prop.Name.EndsWith("-CHAIN"))
blob.Move(["derivationStrategies", prop.Name], ["prompts", prop.Name, "details", "accountDerivation"]);
var camel = new CamelCaseNamingStrategy();
foreach (var p in details.Properties().ToList())
{
var camelName = camel.GetPropertyName(p.Name, false);
if (camelName != p.Name)
details.RenameProperty(p.Name, camelName);
}
}
if (blob["defaultPaymentMethod"] is not (null or { Type : JTokenType.Null }))
blob["defaultPaymentMethod"] = MigrationExtensions.MigratePaymentMethodId(blob["defaultPaymentMethod"].Value<string>());
blob.Remove("derivationStrategies");
blob["version"] = 3;
Blob2 = blob.ToString(Formatting.None);
}
static string[] detailsRemoveDefault =
[
"paymentMethodFeeRate",
"keyPath",
"BOLT11",
"NodeInfo",
"Preimage",
"InvoiceId",
"PaymentHash",
"ProvidedComment",
"GeneratedBoltAmount",
"ConsumedLightningAddress",
"PayRequest"
];
#pragma warning restore CS0618 // Type or member is obsolete
}
}

View File

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class InvoiceData : IHasBlobUntyped
public partial class InvoiceData : IHasBlobUntyped
{
public string Id { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
@ -25,6 +25,7 @@ namespace BTCPayServer.Data
public string OrderId { get; set; }
public string Status { get; set; }
public string ExceptionStatus { get; set; }
[Obsolete("Unused")]
public string CustomerEmail { get; set; }
public List<AddressInvoiceData> AddressInvoices { get; set; }
public bool Archived { get; set; }
@ -43,12 +44,14 @@ namespace BTCPayServer.Data
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql())
{
builder.Entity<InvoiceData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<InvoiceData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View File

@ -40,7 +40,6 @@ namespace BTCPayServer.Data
{
return Severity switch
{
EventSeverity.Info => "info",
EventSeverity.Error => "danger",
EventSeverity.Success => "success",
EventSeverity.Warning => "warning",

View File

@ -0,0 +1,151 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public static class MigrationExtensions
{
public static JProperty? Move(this JObject blob, string[] pathFrom, string[] pathTo)
{
var from = GetProperty(blob, pathFrom, false);
if (from is null)
return null;
var to = GetProperty(blob, pathTo, true);
to!.Value = from.Value;
from.Remove();
return to;
}
public static void RenameProperty(this JObject o, string oldName, string newName)
{
var p = o.Property(oldName);
if (p is null)
return;
RenameProperty(ref p, newName);
}
public static void RenameProperty(ref JProperty ls, string newName)
{
if (ls.Name != newName)
{
var parent = ls.Parent;
ls.Remove();
ls = new JProperty(newName, ls.Value);
parent!.Add(ls);
}
}
public static JProperty? GetProperty(this JObject blob, string[] pathFrom, bool createIfNotExists)
{
var current = blob;
for (int i = 0; i < pathFrom.Length - 1; i++)
{
if (current.TryGetValue(pathFrom[i], out var value) && value is JObject jObject)
{
current = jObject;
}
else
{
if (!createIfNotExists)
return null;
JProperty? prop = null;
for (int ii = i; ii < pathFrom.Length; ii++)
{
var newProp = new JProperty(pathFrom[ii], new JObject());
if (prop is null)
current.Add(newProp);
else
prop.Value = new JObject(newProp);
prop = newProp;
}
return prop;
}
}
var result = current.Property(pathFrom[pathFrom.Length - 1]);
if (result is null && createIfNotExists)
{
result = new JProperty(pathFrom[pathFrom.Length - 1], null as object);
current.Add(result);
}
return result;
}
public static CamelCaseNamingStrategy Camel = new CamelCaseNamingStrategy();
public static void RemoveIfNull(this JObject blob, string propName)
{
if (blob.Property(propName)?.Value.Type is JTokenType.Null)
blob.Remove(propName);
}
public static void RemoveIfValue<T>(this JObject conf, string propName, T v)
{
var p = conf.Property(propName);
if (p is null)
return;
if (p.Value is JValue { Type: JTokenType.Null })
{
if (EqualityComparer<T>.Default.Equals(default, v))
p.Remove();
}
else if (p.Value is JValue jv)
{
if (EqualityComparer<T>.Default.Equals(jv.Value<T>(), v))
{
p.Remove();
}
}
}
public static void ConvertNumberToString(this JObject o, string prop)
{
if (o[prop]?.Type is JTokenType.Float)
o[prop] = o[prop]!.Value<decimal>().ToString(CultureInfo.InvariantCulture);
if (o[prop]?.Type is JTokenType.Integer)
o[prop] = o[prop]!.Value<long>().ToString(CultureInfo.InvariantCulture);
}
public static string Unzip(byte[] bytes)
{
MemoryStream ms = new MemoryStream(bytes);
using GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress);
StreamReader reader = new StreamReader(gzip, Encoding.UTF8);
var unzipped = reader.ReadToEnd();
return unzipped;
}
public static int GetDivisibility(string paymentMethodId)
{
var splitted = paymentMethodId.Split('-');
return (CryptoCode: splitted[0], Type: splitted[1]) switch
{
{ Type: "LN" } or { Type: "LNURL" } => 11,
{ Type: "CHAIN", CryptoCode: var code } when code == "XMR" => 12,
{ Type: "CHAIN" } => 8,
_ => 8
};
}
public static string MigratePaymentMethodId(string paymentMethodId)
{
var splitted = paymentMethodId.Split(new[] { '_', '-' });
if (splitted is [var cryptoCode, var paymentType])
{
return paymentType switch
{
"BTCLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
};
}
if (splitted.Length == 1)
return $"{splitted[0]}-CHAIN";
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
}
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Migrations;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentData
{
public void Migrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Currency is not null)
return;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoPaymentDataType"] is null)
blob["cryptoPaymentDataType"] = "BTCLike";
if (blob["cryptoCode"] is null)
blob["cryptoCode"] = "BTC";
if (blob["receivedTime"] is null)
blob.Move(["receivedTimeMs"], ["receivedTime"]);
else
{
// Convert number of seconds to number of milliseconds
var timeSeconds = (ulong)(long)blob["receivedTime"].Value<long>();
var date = NBitcoin.Utils.UnixTimeToDateTime(timeSeconds);
blob["receivedTime"] = DateTimeToMilliUnixTime(date.UtcDateTime);
}
var cryptoCode = blob["cryptoCode"].Value<string>();
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
Type = MigrationExtensions.MigratePaymentMethodId(Type);
var divisibility = MigrationExtensions.GetDivisibility(Type);
Currency = blob["cryptoCode"].Value<string>();
blob.Remove("cryptoCode");
blob.Remove("cryptoPaymentDataType");
JObject cryptoData;
if (blob["cryptoPaymentData"] is null)
{
cryptoData = new JObject();
blob["cryptoPaymentData"] = cryptoData;
cryptoData["RBF"] = true;
cryptoData["confirmationCount"] = 0;
}
else
{
cryptoData = JObject.Parse(blob["cryptoPaymentData"].Value<string>());
foreach (var prop in cryptoData.Properties().ToList())
{
if (prop.Name is "rbf")
cryptoData.RenameProperty("rbf", "RBF");
else if (prop.Name is "bolT11")
cryptoData.RenameProperty("bolT11", "BOLT11");
else
cryptoData.RenameProperty(prop.Name, MigrationExtensions.Camel.GetPropertyName(prop.Name, false));
}
}
blob.Remove("cryptoPaymentData");
cryptoData["outpoint"] = blob["outpoint"];
if (blob["output"] is not (null or { Type: JTokenType.Null }))
{
// Old versions didn't track addresses, so we take it from output.
// We don't know the network for sure but better having something than nothing in destination.
// If signet/testnet crash we don't really care anyway.
// Also, only LTC was supported at this time.
Network network = (cryptoCode switch { "LTC" => (INetworkSet)Litecoin.Instance, _ => Bitcoin.Instance }).Mainnet;
var txout = network.Consensus.ConsensusFactory.CreateTxOut();
txout.ReadWrite(Encoders.Hex.DecodeData(blob["output"].Value<string>()), network);
cryptoData["value"] = txout.Value.Satoshi;
blob["destination"] = txout.ScriptPubKey.GetDestinationAddress(network)?.ToString();
}
blob.Remove("output");
blob.Remove("outpoint");
// Convert from sats to btc
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["value"].Value<long>();
Amount = (decimal)v / (decimal)Money.COIN;
cryptoData.Remove("value");
blob["paymentMethodFee"] = blob["networkFee"];
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
blob.ConvertNumberToString("paymentMethodFee");
blob.Remove("networkFee");
blob.RemoveIfNull("paymentMethodFee");
}
// Convert from millisats to btc
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["amount"].Value<long>();
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
cryptoData.Remove("amount");
}
if (cryptoData["address"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["address"];
cryptoData.Remove("address");
}
if (cryptoData["BOLT11"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["BOLT11"];
cryptoData.Remove("BOLT11");
}
if (cryptoData["outpoint"] is not (null or { Type: JTokenType.Null }))
{
// Convert to format txid-n
cryptoData["outpoint"] = OutPoint.Parse(cryptoData["outpoint"].Value<string>()).ToString();
}
if (Accounted is false)
Status = PaymentStatus.Unaccounted;
else if (cryptoData["confirmationCount"] is { Type: JTokenType.Integer })
{
var confirmationCount = cryptoData["confirmationCount"].Value<int>();
// Technically, we should use the invoice's speed policy, however it's not on our
// scope and is good enough for majority of cases.
Status = confirmationCount > 0 ? PaymentStatus.Settled : PaymentStatus.Processing;
if (cryptoData["LockTime"] is { Type: JTokenType.Integer })
{
var lockTime = cryptoData["LockTime"].Value<int>();
if (confirmationCount < lockTime)
Status = PaymentStatus.Processing;
}
}
else
{
Status = PaymentStatus.Settled;
}
Created = MilliUnixTimeToDateTime(blob["receivedTime"].Value<long>());
cryptoData.RemoveIfValue<bool>("rbf", false);
cryptoData.Remove("legacy");
cryptoData.Remove("networkFee");
cryptoData.Remove("paymentType");
cryptoData.RemoveIfNull("outpoint");
cryptoData.RemoveIfValue<bool>("RBF", false);
blob.Remove("receivedTime");
blob.Remove("accounted");
blob.Remove("networkFee");
blob["details"] = cryptoData;
blob["divisibility"] = divisibility;
blob["version"] = 2;
Blob2 = blob.ToString(Formatting.None);
Accounted = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public static long DateTimeToMilliUnixTime(in DateTime time)
{
var date = ((DateTimeOffset)time).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return v;
}
public static DateTimeOffset MilliUnixTimeToDateTime(long value)
{
var v = value;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return unixRef + TimeSpan.FromMilliseconds(v);
}
}
}

View File

@ -4,17 +4,32 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentData : IHasBlobUntyped
public enum PaymentStatus
{
Processing,
Settled,
Unaccounted
}
public partial class PaymentData : IHasBlobUntyped
{
/// <summary>
/// The date of creation of the payment
/// Note that while it is a nullable field, our migration
/// process ensure it is populated.
/// </summary>
public DateTimeOffset? Created { get; set; }
public string Id { get; set; }
public string InvoiceDataId { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public InvoiceData InvoiceData { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public bool Accounted { get; set; }
[Obsolete("Use Status instead")]
public bool? Accounted { get; set; }
public PaymentStatus? Status { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -23,11 +38,17 @@ namespace BTCPayServer.Data
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<PaymentData>()
.Property(o => o.Status)
.HasConversion<string>();
if (databaseFacade.IsNpgsql())
{
builder.Entity<PaymentData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<PaymentData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View File

@ -1,8 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
namespace BTCPayServer.Data
@ -21,14 +24,14 @@ namespace BTCPayServer.Data
[MaxLength(20)]
[Required]
public string PaymentMethodId { get; set; }
public byte[] Blob { get; set; }
public byte[] Proof { get; set; }
public string Blob { get; set; }
public string Proof { get; set; }
#nullable enable
public string? Destination { get; set; }
#nullable restore
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<PayoutData>()
.HasOne(o => o.PullPaymentData)
@ -43,6 +46,33 @@ namespace BTCPayServer.Data
.HasIndex(o => o.State);
builder.Entity<PayoutData>()
.HasIndex(x => new { DestinationId = x.Destination, x.State });
if (databaseFacade.IsNpgsql())
{
builder.Entity<PayoutData>()
.Property(o => o.Blob)
.HasColumnType("JSONB");
builder.Entity<PayoutData>()
.Property(o => o.Proof)
.HasColumnType("JSONB");
}
else if (databaseFacade.IsMySql())
{
builder.Entity<PayoutData>()
.Property(o => o.Blob)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
builder.Entity<PayoutData>()
.Property(o => o.Proof)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
}
}
// utility methods

View File

@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
namespace BTCPayServer.Data
@ -24,16 +27,33 @@ namespace BTCPayServer.Data
public DateTimeOffset? EndDate { get; set; }
public bool Archived { get; set; }
public List<PayoutData> Payouts { get; set; }
public byte[] Blob { get; set; }
public string Blob { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<PullPaymentData>()
.HasIndex(o => o.StoreId);
builder.Entity<PullPaymentData>()
.HasOne(o => o.StoreData)
.HasOne(o => o.StoreData)
.WithMany(o => o.PullPayments).OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<PullPaymentData>()
.Property(o => o.Blob)
.HasColumnType("JSONB");
}
else if (databaseFacade.IsMySql())
{
builder.Entity<PullPaymentData>()
.Property(o => o.Blob)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
}
}
public (DateTimeOffset Start, DateTimeOffset? End)? GetPeriod(DateTimeOffset now)

View File

@ -5,6 +5,7 @@ using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
@ -44,7 +45,6 @@ namespace BTCPayServer.Data
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
public IEnumerable<PayoutData> Payouts { get; set; }
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }

View File

@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240104155620_AddApprovalToApplicationUser")]
public partial class AddApprovalToApplicationUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Approved",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "RequiresApproval",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Approved",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "RequiresApproval",
table: "AspNetUsers");
}
}
}

View File

@ -0,0 +1,26 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240220000000_FixWalletObjectsWithEmptyWalletId")]
public partial class FixWalletObjectsWithEmptyWalletId : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("DELETE FROM \"WalletObjects\" WHERE \"WalletId\"='';");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,28 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240229000000_PayoutAndPullPaymentToJsonBlob")]
public partial class PayoutAndPullPaymentToJsonBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Proof\" TYPE JSONB USING regexp_replace(convert_from(\"Proof\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
migrationBuilder.Sql("ALTER TABLE \"PullPayments\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,98 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Newtonsoft.Json;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240229092905_AddManagerAndEmployeeToStoreRoles")]
public partial class AddManagerAndEmployeeToStoreRoles : Migration
{
object GetPermissionsData(MigrationBuilder migrationBuilder, string[] permissions)
{
return migrationBuilder.IsNpgsql()
? permissions
: JsonConvert.SerializeObject(permissions);
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
migrationBuilder.InsertData(
"StoreRoles",
columns: new[] { "Id", "Role", "Permissions" },
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
values: new object[,]
{
{
"Manager", "Manager", GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canviewstoresettings",
"btcpay.store.canmodifyinvoices",
"btcpay.store.webhooks.canmodifywebhooks",
"btcpay.store.canmodifypaymentrequests",
"btcpay.store.canmanagepullpayments",
"btcpay.store.canmanagepayouts"
})
},
{
"Employee", "Employee", GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canmodifyinvoices",
"btcpay.store.canmodifypaymentrequests",
"btcpay.store.cancreatenonapprovedpullpayments",
"btcpay.store.canviewpayouts",
"btcpay.store.canviewpullpayments"
})
}
});
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Guest" },
columns: new[] { "Permissions" },
columnTypes: new[] { permissionsType },
values: new object[]
{
GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canmodifyinvoices",
"btcpay.store.canviewpaymentrequests",
"btcpay.store.canviewpullpayments",
"btcpay.store.canviewpayouts"
})
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData("StoreRoles", "Id", "Manager");
migrationBuilder.DeleteData("StoreRoles", "Id", "Employee");
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Guest" },
columns: new[] { "Permissions" },
columnTypes: new[] { permissionsType },
values: new object[]
{
GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canviewstoresettings",
"btcpay.store.canmodifyinvoices",
"btcpay.store.canviewcustodianaccounts",
"btcpay.store.candeposittocustodianaccount"
})
});
}
}
}

View File

@ -0,0 +1,43 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
/// <inheritdoc />
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240304003640_addinvoicecolumns")]
public partial class addinvoicecolumns : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Invoices",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Invoices",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Invoices");
migrationBuilder.DropColumn(
name: "Currency",
table: "Invoices");
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240317024757_payments_refactor")]
public partial class payments_refactor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Payments",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "Payments",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Payments",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Payments",
type: "TEXT",
nullable: true);
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.AlterColumn<bool?>(
name: "Accounted",
table: "Payments",
nullable: true);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Payments");
migrationBuilder.DropColumn(
name: "Created",
table: "Payments");
migrationBuilder.DropColumn(
name: "Currency",
table: "Payments");
migrationBuilder.DropColumn(
name: "Status",
table: "Payments");
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240325095923_RemoveCustodian")]
public partial class RemoveCustodian : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CustodianAccount");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CustodianAccount",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
StoreId = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
Blob2 = table.Column<string>(type: "TEXT", nullable: true),
CustodianCode = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CustodianAccount", x => x.Id);
table.ForeignKey(
name: "FK_CustodianAccount_Stores_StoreId",
column: x => x.StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CustodianAccount_StoreId",
table: "CustodianAccount",
column: "StoreId");
}
}
}

View File

@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
@ -112,6 +112,9 @@ namespace BTCPayServer.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<bool>("Approved")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -158,6 +161,9 @@ namespace BTCPayServer.Migrations
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresApproval")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresEmailConfirmation")
.HasColumnType("INTEGER");
@ -183,40 +189,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("CustodianCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("CustodianAccount");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.Property<string>("Id")
@ -275,6 +247,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
@ -287,6 +262,9 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail")
.HasColumnType("TEXT");
@ -533,15 +511,27 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Accounted")
.HasColumnType("INTEGER");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
@ -592,8 +582,8 @@ namespace BTCPayServer.Migrations
.HasMaxLength(30)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Date")
.HasColumnType("TEXT");
@ -606,8 +596,8 @@ namespace BTCPayServer.Migrations
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Proof")
.HasColumnType("BLOB");
b.Property<string>("Proof")
.HasColumnType("TEXT");
b.Property<string>("PullPaymentDataId")
.HasColumnType("TEXT");
@ -697,8 +687,8 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("EndDate")
.HasColumnType("TEXT");
@ -1213,17 +1203,6 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("CustodianAccounts")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
@ -1632,8 +1611,6 @@ namespace BTCPayServer.Migrations
b.Navigation("Apps");
b.Navigation("CustodianAccounts");
b.Navigation("Forms");
b.Navigation("Invoices");

View File

@ -1,4 +1,4 @@
[
[
{
"name":"Afghan Afghani",
"code":"AFN",
@ -58,7 +58,7 @@
{
"name":"Argentine Peso",
"code":"ARS",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},
@ -289,7 +289,7 @@
{
"name":"Colombian Peso",
"code":"COP",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},

View File

@ -77,7 +77,15 @@ namespace BTCPayServer.Services.Rates
continue;
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
var symbol = new RegionInfo(culture.LCID).ISOCurrencySymbol;
var c = symbol switch
{
// ARS and COP are officially 2 digits, but due to depreciation,
// nobody really use those anymore. (See https://github.com/btcpayserver/btcpayserver/issues/5708)
"ARS" or "COP" => ModifyCurrencyDecimalDigit(culture, 0),
_ => culture
};
_CurrencyProviders.TryAdd(symbol, c);
}
catch { }
}
@ -91,6 +99,15 @@ namespace BTCPayServer.Services.Rates
}
}
private CultureInfo ModifyCurrencyDecimalDigit(CultureInfo culture, int decimals)
{
var modifiedCulture = new CultureInfo(culture.Name);
NumberFormatInfo modifiedNumberFormat = (NumberFormatInfo)modifiedCulture.NumberFormat.Clone();
modifiedNumberFormat.CurrencyDecimalDigits = decimals;
modifiedCulture.NumberFormat = modifiedNumberFormat;
return modifiedCulture;
}
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
{
var culture = new CultureInfo("en-US");

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Rating.Providers
{
public class BitnobRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public BitnobRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("bitnob", "Bitnob", "https://api.bitnob.co/api/v1/rates/bitcoin/price");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync("https://api.bitnob.co/api/v1/rates/bitcoin/price", cancellationToken);
JObject jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var dataObject = jobj["data"] as JObject;
if (dataObject == null)
{
return Array.Empty<PairRate>();
}
var pairRates = new List<PairRate>();
foreach (var property in dataObject.Properties())
{
string[] parts = property.Name.Split('_');
decimal value = property.Value.Value<decimal>();
pairRates.Add(new PairRate(new CurrencyPair("BTC", parts[1]), new BidAsk(value)));
}
return pairRates.ToArray();
}
}
}

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