2020-03-13 11:47:22 +01:00
using System ;
2021-03-11 13:34:52 +01:00
using System.Collections.Generic ;
2022-05-18 07:59:56 +02:00
using System.Globalization ;
2020-03-12 14:59:24 +01:00
using System.Linq ;
2020-02-24 18:43:28 +01:00
using System.Net.Http ;
2020-06-24 03:34:09 +02:00
using System.Threading ;
2020-02-24 18:43:28 +01:00
using System.Threading.Tasks ;
2021-07-27 14:11:47 +02:00
using BTCPayServer.Abstractions.Contracts ;
2022-05-18 07:59:56 +02:00
using BTCPayServer.Abstractions.Custodians ;
2020-03-02 16:50:28 +01:00
using BTCPayServer.Client ;
2020-03-13 11:47:22 +01:00
using BTCPayServer.Client.Models ;
2020-05-28 09:48:47 +02:00
using BTCPayServer.Controllers ;
using BTCPayServer.Events ;
2020-08-19 14:46:45 +02:00
using BTCPayServer.Lightning ;
2020-12-10 15:34:50 +01:00
using BTCPayServer.Models.InvoicingModels ;
2021-07-23 10:05:15 +02:00
using BTCPayServer.Payments ;
2022-08-17 09:45:51 +02:00
using BTCPayServer.Payments.Lightning ;
2023-04-27 03:59:19 +02:00
using BTCPayServer.PayoutProcessors ;
using BTCPayServer.PayoutProcessors.OnChain ;
2023-07-20 15:05:14 +02:00
using BTCPayServer.Plugins ;
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
using BTCPayServer.Services ;
2023-01-06 14:18:07 +01:00
using BTCPayServer.Services.Custodian.Client.MockCustodian ;
2020-12-11 15:11:08 +01:00
using BTCPayServer.Services.Notifications ;
using BTCPayServer.Services.Notifications.Blobs ;
2023-05-26 16:49:32 +02:00
using BTCPayServer.Services.Stores ;
2020-05-28 09:48:47 +02:00
using Microsoft.AspNetCore.Mvc ;
2020-06-12 11:26:20 +02:00
using Microsoft.EntityFrameworkCore ;
2023-04-27 03:59:19 +02:00
using Microsoft.Extensions.Hosting ;
2020-05-28 09:48:47 +02:00
using NBitcoin ;
using NBitpayClient ;
2020-05-19 19:59:23 +02:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2020-02-24 18:43:28 +01:00
using Xunit ;
using Xunit.Abstractions ;
2020-03-13 11:47:22 +01:00
using CreateApplicationUserRequest = BTCPayServer . Client . Models . CreateApplicationUserRequest ;
2020-02-24 18:43:28 +01:00
namespace BTCPayServer.Tests
{
2021-11-23 05:57:45 +01:00
[Collection(nameof(NonParallelizableCollectionDefinition))]
2021-11-22 09:16:08 +01:00
public class GreenfieldAPITests : UnitTestBase
2020-02-24 18:43:28 +01:00
{
public const int TestTimeout = TestUtils . TestTimeout ;
2021-11-22 09:16:08 +01:00
public GreenfieldAPITests ( ITestOutputHelper helper ) : base ( helper )
2020-02-24 18:43:28 +01:00
{
}
2021-07-27 14:11:47 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2022-06-06 15:57:55 +02:00
[Trait("Lightning", "Lightning")]
2021-07-27 14:11:47 +02:00
public async Task LocalClientTests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2022-06-06 15:57:55 +02:00
tester . ActivateLightning ( ) ;
2021-07-27 14:11:47 +02:00
await tester . StartAsync ( ) ;
2022-06-06 15:57:55 +02:00
await tester . EnsureChannelsSetup ( ) ;
2021-07-27 14:11:47 +02:00
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
await user . MakeAdmin ( ) ;
2022-06-06 15:57:55 +02:00
user . RegisterLightningNode ( "BTC" , LightningConnectionType . CLightning ) ;
2021-07-27 14:11:47 +02:00
var factory = tester . PayTester . GetService < IBTCPayServerClientFactory > ( ) ;
Assert . NotNull ( factory ) ;
2022-06-06 15:57:55 +02:00
var client = await factory . Create ( user . UserId , user . StoreId ) ;
2021-07-27 14:11:47 +02:00
var u = await client . GetCurrentUser ( ) ;
var s = await client . GetStores ( ) ;
2022-06-06 15:57:55 +02:00
var store = await client . GetStore ( user . StoreId ) ;
Assert . NotNull ( store ) ;
2022-07-19 19:53:14 +02:00
var addr = await client . GetLightningDepositAddress ( user . StoreId , "BTC" ) ;
2022-06-06 15:57:55 +02:00
Assert . NotNull ( BitcoinAddress . Create ( addr , Network . RegTest ) ) ;
2022-07-19 19:53:14 +02:00
await user . CreateStoreAsync ( ) ;
var store1 = user . StoreId ;
await user . CreateStoreAsync ( ) ;
var store2 = user . StoreId ;
var store1Client = await factory . Create ( null , store1 ) ;
var store2Client = await factory . Create ( null , store2 ) ;
var store1Res = await store1Client . GetStore ( store1 ) ;
var store2Res = await store2Client . GetStore ( store2 ) ;
Assert . Equal ( store1 , store1Res . Id ) ;
Assert . Equal ( store2 , store2Res . Id ) ;
2021-07-27 14:11:47 +02:00
}
2021-12-16 15:04:06 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task MissingPermissionTest ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
var clientWithWrongPermissions = await user . CreateClient ( Policies . CanViewProfile ) ;
var e = await AssertAPIError ( "missing-permission" , ( ) = > clientWithWrongPermissions . CreateStore ( new CreateStoreRequest ( ) { Name = "mystore" } ) ) ;
Assert . Equal ( "missing-permission" , e . APIError . Code ) ;
Assert . NotNull ( e . APIError . Message ) ;
GreenfieldPermissionAPIError permissionError = Assert . IsType < GreenfieldPermissionAPIError > ( e . APIError ) ;
2022-10-07 07:53:30 +02:00
Assert . Equal ( Policies . CanModifyStoreSettings , permissionError . MissingPermission ) ;
2021-12-16 15:04:06 +01:00
}
2021-07-27 14:11:47 +02:00
2020-03-12 14:59:24 +01:00
[Fact(Timeout = TestTimeout)]
2020-02-24 18:43:28 +01:00
[Trait("Integration", "Integration")]
public async Task ApiKeysControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
await user . MakeAdmin ( ) ;
var client = await user . CreateClient ( Policies . CanViewProfile ) ;
var clientBasic = await user . CreateClient ( ) ;
//Get current api key
var apiKeyData = await client . GetCurrentAPIKeyInfo ( ) ;
Assert . NotNull ( apiKeyData ) ;
Assert . Equal ( client . APIKey , apiKeyData . ApiKey ) ;
Assert . Single ( apiKeyData . Permissions ) ;
//a client using Basic Auth has no business here
await AssertHttpError ( 401 , async ( ) = > await clientBasic . GetCurrentAPIKeyInfo ( ) ) ;
//revoke current api key
await client . RevokeCurrentAPIKeyInfo ( ) ;
await AssertHttpError ( 401 , async ( ) = > await client . GetCurrentAPIKeyInfo ( ) ) ;
//a client using Basic Auth has no business here
await AssertHttpError ( 401 , async ( ) = > await clientBasic . RevokeCurrentAPIKeyInfo ( ) ) ;
2020-02-24 18:43:28 +01:00
}
2020-05-19 19:59:23 +02:00
2021-07-08 07:34:10 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseMiscAPIs ( )
{
2021-11-22 09:16:08 +01:00
using ( var tester = CreateServerTester ( ) )
2021-07-08 07:34:10 +02:00
{
await tester . StartAsync ( ) ;
var acc = tester . NewAccount ( ) ;
await acc . GrantAccessAsync ( ) ;
var unrestricted = await acc . CreateClient ( ) ;
var langs = await unrestricted . GetAvailableLanguages ( ) ;
Assert . NotEmpty ( langs ) ;
Assert . NotNull ( langs [ 0 ] . Code ) ;
Assert . NotNull ( langs [ 0 ] . DisplayName ) ;
var perms = await unrestricted . GetPermissionMetadata ( ) ;
Assert . NotEmpty ( perms ) ;
var p = perms . First ( p = > p . PermissionName = = "unrestricted" ) ;
Assert . True ( p . SubPermissions . Count > 6 ) ;
}
}
2020-06-27 08:34:03 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task SpecificCanModifyStoreCantCreateNewStore ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var acc = tester . NewAccount ( ) ;
await acc . GrantAccessAsync ( ) ;
var unrestricted = await acc . CreateClient ( ) ;
var response = await unrestricted . CreateStore ( new CreateStoreRequest ( ) { Name = "mystore" } ) ;
var apiKey = ( await unrestricted . CreateAPIKey ( new CreateApiKeyRequest ( ) { Permissions = new [ ] { Permission . Create ( "btcpay.store.canmodifystoresettings" , response . Id ) } } ) ) . ApiKey ;
var restricted = new BTCPayServerClient ( unrestricted . Host , apiKey ) ;
// Unscoped permission should be required for create store
await this . AssertHttpError ( 403 , async ( ) = > await restricted . CreateStore ( new CreateStoreRequest ( ) { Name = "store2" } ) ) ;
// Unrestricted should work fine
await unrestricted . CreateStore ( new CreateStoreRequest ( ) { Name = "store2" } ) ;
// Restricted but unscoped should work fine
apiKey = ( await unrestricted . CreateAPIKey ( new CreateApiKeyRequest ( ) { Permissions = new [ ] { Permission . Create ( "btcpay.store.canmodifystoresettings" ) } } ) ) . ApiKey ;
restricted = new BTCPayServerClient ( unrestricted . Host , apiKey ) ;
await restricted . CreateStore ( new CreateStoreRequest ( ) { Name = "store2" } ) ;
2020-06-27 08:34:03 +02:00
}
2020-03-27 06:17:31 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2020-03-27 06:46:51 +01:00
public async Task CanCreateAndDeleteAPIKeyViaAPI ( )
2020-03-27 06:17:31 +01:00
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var acc = tester . NewAccount ( ) ;
await acc . GrantAccessAsync ( ) ;
var unrestricted = await acc . CreateClient ( ) ;
var apiKey = await unrestricted . CreateAPIKey ( new CreateApiKeyRequest ( )
2020-03-27 06:17:31 +01:00
{
2022-01-14 09:50:29 +01:00
Label = "Hello world" ,
Permissions = new Permission [ ] { Permission . Create ( Policies . CanViewProfile ) }
} ) ;
Assert . Equal ( "Hello world" , apiKey . Label ) ;
var p = Assert . Single ( apiKey . Permissions ) ;
Assert . Equal ( Policies . CanViewProfile , p . Policy ) ;
var restricted = acc . CreateClientFromAPIKey ( apiKey . ApiKey ) ;
await AssertHttpError ( 403 ,
async ( ) = > await restricted . CreateAPIKey ( new CreateApiKeyRequest ( )
2020-03-27 06:17:31 +01:00
{
2022-01-14 09:50:29 +01:00
Label = "Hello world2" ,
2020-06-27 08:34:03 +02:00
Permissions = new Permission [ ] { Permission . Create ( Policies . CanViewProfile ) }
2022-01-14 09:50:29 +01:00
} ) ) ;
2020-03-27 06:46:51 +01:00
2022-01-14 09:50:29 +01:00
await unrestricted . RevokeAPIKey ( apiKey . ApiKey ) ;
await AssertAPIError ( "apikey-not-found" , ( ) = > unrestricted . RevokeAPIKey ( apiKey . ApiKey ) ) ;
2023-02-24 08:19:03 +01:00
// Admin create API key to new user
acc = tester . NewAccount ( ) ;
await acc . GrantAccessAsync ( isAdmin : true ) ;
unrestricted = await acc . CreateClient ( ) ;
var newUser = await unrestricted . CreateUser ( new CreateApplicationUserRequest ( ) { Email = Utils . GenerateEmail ( ) , Password = "Kitten0@" } ) ;
var newUserAPIKey = await unrestricted . CreateAPIKey ( newUser . Id , new CreateApiKeyRequest ( )
{
Label = "Hello world" ,
Permissions = new Permission [ ] { Permission . Create ( Policies . CanViewProfile ) }
} ) ;
var newUserClient = acc . CreateClientFromAPIKey ( newUserAPIKey . ApiKey ) ;
Assert . Equal ( newUser . Id , ( await newUserClient . GetCurrentUser ( ) ) . Id ) ;
// Admin delete it
await unrestricted . RevokeAPIKey ( newUser . Id , newUserAPIKey . ApiKey ) ;
await Assert . ThrowsAsync < GreenfieldAPIException > ( ( ) = > newUserClient . GetCurrentUser ( ) ) ;
// Admin create store
var store = await unrestricted . CreateStore ( new CreateStoreRequest ( ) { Name = "Pouet lol" } ) ;
// Grant right to another user
2023-03-03 13:24:27 +01:00
newUserAPIKey = await unrestricted . CreateAPIKey ( newUser . Email , new CreateApiKeyRequest ( )
2023-02-24 08:19:03 +01:00
{
Label = "Hello world" ,
Permissions = new Permission [ ] { Permission . Create ( Policies . CanViewInvoices , store . Id ) } ,
} ) ;
2023-03-03 12:30:54 +01:00
await AssertAPIError ( "user-not-found" , ( ) = > unrestricted . CreateAPIKey ( "fewiofwuefo" , new CreateApiKeyRequest ( ) ) ) ;
2023-02-24 08:19:03 +01:00
// Despite the grant, the user shouldn't be able to get the invoices!
newUserClient = acc . CreateClientFromAPIKey ( newUserAPIKey . ApiKey ) ;
await Assert . ThrowsAsync < GreenfieldAPIException > ( ( ) = > newUserClient . GetInvoices ( store . Id ) ) ;
// if user is a guest or owner, then it should be ok
2023-05-26 16:49:32 +02:00
await unrestricted . AddStoreUser ( store . Id , new StoreUserData ( ) { UserId = newUser . Id } ) ;
2023-02-24 08:19:03 +01:00
await newUserClient . GetInvoices ( store . Id ) ;
2020-03-27 06:17:31 +01:00
}
2020-03-18 15:10:15 +01:00
2022-05-02 07:28:27 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2022-07-18 07:23:22 +02:00
public async Task CanCreateReadUpdateAndDeletePointOfSaleApp ( )
2022-05-02 07:28:27 +02:00
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var client = await user . CreateClient ( ) ;
2022-06-27 03:14:16 +02:00
2022-07-18 07:23:22 +02:00
// Test validation for creating the app
await AssertValidationError ( new [ ] { "AppName" } ,
2023-01-06 14:18:07 +01:00
async ( ) = > await client . CreatePointOfSaleApp ( user . StoreId , new CreatePointOfSaleAppRequest ( ) { } ) ) ;
2022-07-18 07:23:22 +02:00
await AssertValidationError ( new [ ] { "AppName" } ,
async ( ) = > await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "this is a really long app name this is a really long app name this is a really long app name" ,
}
)
) ;
await AssertValidationError ( new [ ] { "Currency" } ,
async ( ) = > await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "good name" ,
Currency = "fake currency"
}
)
) ;
await AssertValidationError ( new [ ] { "Template" } ,
async ( ) = > await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "good name" ,
Template = "lol invalid template"
}
)
) ;
await AssertValidationError ( new [ ] { "AppName" , "Currency" , "Template" } ,
async ( ) = > await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
Currency = "fake currency" ,
Template = "lol invalid template"
}
)
) ;
// Test creating a POS app successfully
var app = await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "test app from API" ,
2023-02-07 09:01:53 +01:00
Currency = "JPY" ,
Title = "test app title"
2022-07-18 07:23:22 +02:00
}
) ;
2022-05-02 07:28:27 +02:00
Assert . Equal ( "test app from API" , app . Name ) ;
Assert . Equal ( user . StoreId , app . StoreId ) ;
Assert . Equal ( "PointOfSale" , app . AppType ) ;
2023-02-07 09:01:53 +01:00
Assert . Equal ( "test app title" , app . Title ) ;
2022-06-27 03:14:16 +02:00
// Make sure we return a 404 if we try to get an app that doesn't exist
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 404 , async ( ) = >
{
2022-06-27 03:14:16 +02:00
await client . GetApp ( "some random ID lol" ) ;
} ) ;
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = >
{
2023-02-07 09:01:53 +01:00
await client . GetPosApp ( "some random ID lol" ) ;
} ) ;
2022-06-27 03:14:16 +02:00
// Test that we can retrieve the app data
var retrievedApp = await client . GetApp ( app . Id ) ;
Assert . Equal ( app . Name , retrievedApp . Name ) ;
Assert . Equal ( app . StoreId , retrievedApp . StoreId ) ;
Assert . Equal ( app . AppType , retrievedApp . AppType ) ;
2022-07-18 07:23:22 +02:00
// Test that we can update the app data
2023-02-07 09:01:53 +01:00
await client . UpdatePointOfSaleApp (
app . Id ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "new app name" ,
Title = "new app title"
}
) ;
// Test generic GET app endpoint first
2022-07-18 07:23:22 +02:00
retrievedApp = await client . GetApp ( app . Id ) ;
Assert . Equal ( "new app name" , retrievedApp . Name ) ;
2023-02-07 09:01:53 +01:00
// Test the POS-specific endpoint also
var retrievedPosApp = await client . GetPosApp ( app . Id ) ;
Assert . Equal ( "new app name" , retrievedPosApp . Name ) ;
Assert . Equal ( "new app title" , retrievedPosApp . Title ) ;
2022-06-27 03:14:16 +02:00
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError ( 404 , async ( ) = >
{
await client . DeleteApp ( "some random ID lol" ) ;
} ) ;
// Test deleting the newly created app
await client . DeleteApp ( retrievedApp . Id ) ;
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 404 , async ( ) = >
{
2022-06-27 03:14:16 +02:00
await client . GetApp ( retrievedApp . Id ) ;
} ) ;
2022-05-02 07:28:27 +02:00
}
2022-11-18 06:20:07 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2023-02-07 09:01:53 +01:00
public async Task CanCreateReadAndDeleteCrowdfundApp ( )
2022-11-18 06:20:07 +01:00
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var client = await user . CreateClient ( ) ;
// Test validation for creating the app
await AssertValidationError ( new [ ] { "AppName" } ,
2023-01-06 14:18:07 +01:00
async ( ) = > await client . CreateCrowdfundApp ( user . StoreId , new CreateCrowdfundAppRequest ( ) { } ) ) ;
2022-11-18 06:20:07 +01:00
await AssertValidationError ( new [ ] { "AppName" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "this is a really long app name this is a really long app name this is a really long app name" ,
}
)
) ;
await AssertValidationError ( new [ ] { "TargetCurrency" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
TargetCurrency = "fake currency"
}
)
) ;
await AssertValidationError ( new [ ] { "PerksTemplate" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
PerksTemplate = "lol invalid template"
}
)
) ;
await AssertValidationError ( new [ ] { "AppName" , "TargetCurrency" , "PerksTemplate" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
TargetCurrency = "fake currency" ,
PerksTemplate = "lol invalid template"
}
)
) ;
await AssertValidationError ( new [ ] { "AnimationColors" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
2023-01-06 14:18:07 +01:00
AnimationColors = new string [ ] { }
2022-11-18 06:20:07 +01:00
}
)
) ;
await AssertValidationError ( new [ ] { "AnimationColors" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
AnimationColors = new string [ ] { " " , " " }
}
)
) ;
await AssertValidationError ( new [ ] { "Sounds" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
Sounds = new string [ ] { " " }
}
)
) ;
await AssertValidationError ( new [ ] { "Sounds" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
2023-01-06 14:18:07 +01:00
Sounds = new string [ ] { " " , " " , " " }
2022-11-18 06:20:07 +01:00
}
)
) ;
await AssertValidationError ( new [ ] { "EndDate" } ,
async ( ) = > await client . CreateCrowdfundApp (
user . StoreId ,
new CreateCrowdfundAppRequest ( )
{
AppName = "good name" ,
StartDate = DateTime . Parse ( "1998-01-01" ) ,
EndDate = DateTime . Parse ( "1997-12-31" )
}
)
) ;
// Test creating a crowdfund app
2023-02-07 09:01:53 +01:00
var app = await client . CreateCrowdfundApp (
user . StoreId ,
2023-04-10 04:07:03 +02:00
new CreateCrowdfundAppRequest ( )
2023-02-07 09:01:53 +01:00
{
AppName = "test app from API" ,
Title = "test app title"
}
) ;
2022-11-18 06:20:07 +01:00
Assert . Equal ( "test app from API" , app . Name ) ;
Assert . Equal ( user . StoreId , app . StoreId ) ;
Assert . Equal ( "Crowdfund" , app . AppType ) ;
2023-02-07 09:01:53 +01:00
// Make sure we return a 404 if we try to get an app that doesn't exist
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = >
{
2023-02-07 09:01:53 +01:00
await client . GetApp ( "some random ID lol" ) ;
} ) ;
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = >
{
2023-02-07 09:01:53 +01:00
await client . GetCrowdfundApp ( "some random ID lol" ) ;
} ) ;
// Test that we can retrieve the app data
var retrievedApp = await client . GetApp ( app . Id ) ;
Assert . Equal ( app . Name , retrievedApp . Name ) ;
Assert . Equal ( app . StoreId , retrievedApp . StoreId ) ;
Assert . Equal ( app . AppType , retrievedApp . AppType ) ;
// Test the crowdfund-specific endpoint also
var retrievedPosApp = await client . GetCrowdfundApp ( app . Id ) ;
Assert . Equal ( app . Name , retrievedPosApp . Name ) ;
Assert . Equal ( app . Title , retrievedPosApp . Title ) ;
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError ( 404 , async ( ) = >
{
await client . DeleteApp ( "some random ID lol" ) ;
} ) ;
// Test deleting the newly created app
await client . DeleteApp ( retrievedApp . Id ) ;
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = >
{
2023-02-07 09:01:53 +01:00
await client . GetApp ( retrievedApp . Id ) ;
} ) ;
2022-11-18 06:20:07 +01:00
}
2023-01-06 14:18:07 +01:00
2023-01-30 01:42:24 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanGetAllApps ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var client = await user . CreateClient ( ) ;
var posApp = await client . CreatePointOfSaleApp (
user . StoreId ,
new CreatePointOfSaleAppRequest ( )
{
AppName = "test app from API" ,
Currency = "JPY"
}
) ;
var crowdfundApp = await client . CreateCrowdfundApp ( user . StoreId , new CreateCrowdfundAppRequest ( ) { AppName = "test app from API" } ) ;
2023-04-10 04:07:03 +02:00
// Create another store and one app on it so we can get all apps from all stores for the user below
2023-01-30 01:42:24 +01:00
var newStore = await client . CreateStore ( new CreateStoreRequest ( ) { Name = "A" } ) ;
var newApp = await client . CreateCrowdfundApp ( newStore . Id , new CreateCrowdfundAppRequest ( ) { AppName = "new app" } ) ;
Assert . NotEqual ( newApp . Id , user . StoreId ) ;
// Get all apps for a specific store first
var apps = await client . GetAllApps ( user . StoreId ) ;
Assert . Equal ( 2 , apps . Length ) ;
Assert . Equal ( posApp . Name , apps [ 0 ] . Name ) ;
Assert . Equal ( posApp . StoreId , apps [ 0 ] . StoreId ) ;
Assert . Equal ( posApp . AppType , apps [ 0 ] . AppType ) ;
Assert . Equal ( crowdfundApp . Name , apps [ 1 ] . Name ) ;
Assert . Equal ( crowdfundApp . StoreId , apps [ 1 ] . StoreId ) ;
Assert . Equal ( crowdfundApp . AppType , apps [ 1 ] . AppType ) ;
// Get all apps for all store now
apps = await client . GetAllApps ( ) ;
Assert . Equal ( 3 , apps . Length ) ;
Assert . Equal ( posApp . Name , apps [ 0 ] . Name ) ;
Assert . Equal ( posApp . StoreId , apps [ 0 ] . StoreId ) ;
Assert . Equal ( posApp . AppType , apps [ 0 ] . AppType ) ;
Assert . Equal ( crowdfundApp . Name , apps [ 1 ] . Name ) ;
Assert . Equal ( crowdfundApp . StoreId , apps [ 1 ] . StoreId ) ;
Assert . Equal ( crowdfundApp . AppType , apps [ 1 ] . AppType ) ;
2023-04-10 04:07:03 +02:00
2023-01-30 01:42:24 +01:00
Assert . Equal ( newApp . Name , apps [ 2 ] . Name ) ;
Assert . Equal ( newApp . StoreId , apps [ 2 ] . StoreId ) ;
Assert . Equal ( newApp . AppType , apps [ 2 ] . AppType ) ;
}
2020-03-18 15:10:15 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2021-04-11 07:36:34 +02:00
public async Task CanDeleteUsersViaApi ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( newDb : true ) ;
2021-06-04 12:20:45 +02:00
await tester . StartAsync ( ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
// Should not be authorized to perform this action
await AssertHttpError ( 401 ,
async ( ) = > await unauthClient . DeleteUser ( "lol user id" ) ) ;
2021-04-11 07:36:34 +02:00
2021-06-04 12:20:45 +02:00
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
await user . MakeAdmin ( ) ;
var adminClient = await user . CreateClient ( Policies . Unrestricted ) ;
//can't delete if the only admin
await AssertHttpError ( 403 ,
async ( ) = > await adminClient . DeleteCurrentUser ( ) ) ;
// Should 404 if user doesn't exist
await AssertHttpError ( 404 ,
async ( ) = > await adminClient . DeleteUser ( "lol user id" ) ) ;
2021-07-14 16:32:20 +02:00
2021-06-04 12:20:45 +02:00
user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
var badClient = await user . CreateClient ( Policies . CanCreateInvoice ) ;
await AssertHttpError ( 403 ,
async ( ) = > await badClient . DeleteCurrentUser ( ) ) ;
var goodClient = await user . CreateClient ( Policies . CanDeleteUser , Policies . CanViewProfile ) ;
await goodClient . DeleteCurrentUser ( ) ;
await AssertHttpError ( 404 ,
async ( ) = > await adminClient . DeleteUser ( user . UserId ) ) ;
tester . Stores . Remove ( user . StoreId ) ;
2021-04-11 07:36:34 +02:00
}
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanViewUsersViaApi ( )
{
using var tester = CreateServerTester ( newDb : true ) ;
await tester . StartAsync ( ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Should be 401 for all calls because we don't have permission
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetUsers ( ) ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetUserByIdOrEmail ( "non_existing_id" ) ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetUserByIdOrEmail ( "someone@example.com" ) ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
var adminUser = tester . NewAccount ( ) ;
await adminUser . GrantAccessAsync ( ) ;
await adminUser . MakeAdmin ( ) ;
var adminClient = await adminUser . CreateClient ( Policies . Unrestricted ) ;
// Should be 404 if user doesn't exist
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 404 , async ( ) = > await adminClient . GetUserByIdOrEmail ( "non_existing_id" ) ) ;
await AssertHttpError ( 404 , async ( ) = > await adminClient . GetUserByIdOrEmail ( "doesnotexist@example.com" ) ) ;
2022-02-15 16:19:52 +01:00
// Try listing all users, should be fine
await adminClient . GetUsers ( ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Try loading 1 user by ID. Loading myself.
await adminClient . GetUserByIdOrEmail ( adminUser . UserId ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Try loading 1 user by email. Loading myself.
await adminClient . GetUserByIdOrEmail ( adminUser . Email ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// var badClient = await user.CreateClient(Policies.CanCreateInvoice);
// await AssertHttpError(403,
// async () => await badClient.DeleteCurrentUser());
var goodUser = tester . NewAccount ( ) ;
await goodUser . GrantAccessAsync ( ) ;
await goodUser . MakeAdmin ( ) ;
var goodClient = await goodUser . CreateClient ( Policies . CanViewUsers ) ;
// Try listing all users, should be fine
await goodClient . GetUsers ( ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Should be 404 if user doesn't exist
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 404 , async ( ) = > await goodClient . GetUserByIdOrEmail ( "non_existing_id" ) ) ;
await AssertHttpError ( 404 , async ( ) = > await goodClient . GetUserByIdOrEmail ( "doesnotexist@example.com" ) ) ;
2022-02-15 16:19:52 +01:00
// Try listing all users, should be fine
await goodClient . GetUsers ( ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Try loading 1 user by ID. Loading myself.
await goodClient . GetUserByIdOrEmail ( goodUser . UserId ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Try loading 1 user by email. Loading myself.
await goodClient . GetUserByIdOrEmail ( goodUser . Email ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
var badUser = tester . NewAccount ( ) ;
await badUser . GrantAccessAsync ( ) ;
await badUser . MakeAdmin ( ) ;
2023-01-06 14:18:07 +01:00
2022-02-15 16:19:52 +01:00
// Bad user has a permission, but it's the wrong one.
var badClient = await goodUser . CreateClient ( Policies . CanCreateInvoice ) ;
// Try listing all users, should be fine
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUsers ( ) ) ;
2022-02-15 16:19:52 +01:00
// Should be 404 if user doesn't exist
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUserByIdOrEmail ( "non_existing_id" ) ) ;
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUserByIdOrEmail ( "doesnotexist@example.com" ) ) ;
2022-02-15 16:19:52 +01:00
// Try listing all users, should be fine
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUsers ( ) ) ;
2022-02-15 16:19:52 +01:00
// Try loading 1 user by ID. Loading myself.
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUserByIdOrEmail ( badUser . UserId ) ) ;
2022-02-15 16:19:52 +01:00
// Try loading 1 user by email. Loading myself.
2023-01-06 14:18:07 +01:00
await AssertHttpError ( 403 , async ( ) = > await badClient . GetUserByIdOrEmail ( badUser . Email ) ) ;
2022-02-15 16:19:52 +01:00
// Why is this line needed? I saw it in "CanDeleteUsersViaApi" as well. Is this part of the cleanup?
tester . Stores . Remove ( adminUser . StoreId ) ;
}
2023-01-06 14:18:07 +01:00
2021-04-11 07:36:34 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2020-03-18 15:10:15 +01:00
public async Task CanCreateUsersViaAPI ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( newDb : true ) ;
tester . PayTester . DisableRegistration = true ;
await tester . StartAsync ( ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
2023-02-15 09:11:39 +01:00
await AssertValidationError ( new [ ] { "Email" } ,
2022-01-14 09:50:29 +01:00
async ( ) = > await unauthClient . CreateUser ( new CreateApplicationUserRequest ( ) ) ) ;
// We have no admin, so it should work
var user1 = await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test@gmail.com" , Password = "abceudhqw" } ) ;
Assert . Empty ( user1 . Roles ) ;
// We have no admin, so it should work
var user2 = await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test2@gmail.com" , Password = "abceudhqw" } ) ;
Assert . Empty ( user2 . Roles ) ;
// Duplicate email
await AssertValidationError ( new [ ] { "Email" } ,
async ( ) = > await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test2@gmail.com" , Password = "abceudhqw" } ) ) ;
// Let's make an admin
var admin = await unauthClient . CreateUser ( new CreateApplicationUserRequest ( )
2020-03-18 15:10:15 +01:00
{
2022-01-14 09:50:29 +01:00
Email = "admin@gmail.com" ,
Password = "abceudhqw" ,
IsAdministrator = true
} ) ;
Assert . Contains ( "ServerAdmin" , admin . Roles ) ;
Assert . NotNull ( admin . Created ) ;
Assert . True ( ( DateTimeOffset . Now - admin . Created ) . Value . Seconds < 10 ) ;
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
var ex = await AssertAPIError ( "unauthenticated" ,
async ( ) = > await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test3@gmail.com" , Password = "afewfoiewiou" } ) ) ;
Assert . Equal ( "New user creation isn't authorized to users who are not admin" , ex . APIError . Message ) ;
// But should be ok with subscriptions unlocked
var settings = tester . PayTester . GetService < SettingsRepository > ( ) ;
await settings . UpdateSetting < PoliciesSettings > ( new PoliciesSettings ( ) { LockSubscription = false } ) ;
await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test3@gmail.com" , Password = "afewfoiewiou" } ) ;
// But it should be forbidden to create an admin without being authenticated
await AssertHttpError ( 401 ,
async ( ) = > await unauthClient . CreateUser ( new CreateApplicationUserRequest ( )
2020-05-19 19:59:23 +02:00
{
2022-01-14 09:50:29 +01:00
Email = "admin2@gmail.com" ,
Password = "afewfoiewiou" ,
2020-06-27 08:34:03 +02:00
IsAdministrator = true
2022-01-14 09:50:29 +01:00
} ) ) ;
await settings . UpdateSetting < PoliciesSettings > ( new PoliciesSettings ( ) { LockSubscription = true } ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
var adminAcc = tester . NewAccount ( ) ;
adminAcc . UserId = admin . Id ;
adminAcc . IsAdmin = true ;
var adminClient = await adminAcc . CreateClient ( Policies . CanModifyProfile ) ;
// We should be forbidden to create a new user without proper admin permissions
await AssertHttpError ( 403 ,
async ( ) = > await adminClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test4@gmail.com" , Password = "afewfoiewiou" } ) ) ;
await AssertAPIError ( "missing-permission" ,
async ( ) = > await adminClient . CreateUser ( new CreateApplicationUserRequest ( )
2020-05-19 19:59:23 +02:00
{
2022-01-14 09:50:29 +01:00
Email = "test4@gmail.com" ,
2020-06-27 08:34:03 +02:00
Password = "afewfoiewiou" ,
IsAdministrator = true
2022-01-14 09:50:29 +01:00
} ) ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
// However, should be ok with the unrestricted permissions of an admin
adminClient = await adminAcc . CreateClient ( Policies . Unrestricted ) ;
await adminClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test4@gmail.com" , Password = "afewfoiewiou" } ) ;
// Even creating new admin should be ok
await adminClient . CreateUser ( new CreateApplicationUserRequest ( )
{
Email = "admin4@gmail.com" ,
Password = "afewfoiewiou" ,
IsAdministrator = true
} ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
var user1Acc = tester . NewAccount ( ) ;
user1Acc . UserId = user1 . Id ;
user1Acc . IsAdmin = false ;
var user1Client = await user1Acc . CreateClient ( Policies . CanModifyServerSettings ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
// User1 trying to get server management would still fail to create user
await AssertHttpError ( 403 ,
async ( ) = > await user1Client . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test8@gmail.com" , Password = "afewfoiewiou" } ) ) ;
2020-03-20 17:59:14 +01:00
2022-01-14 09:50:29 +01:00
// User1 should be able to create user if subscription unlocked
await settings . UpdateSetting < PoliciesSettings > ( new PoliciesSettings ( ) { LockSubscription = false } ) ;
await user1Client . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test8@gmail.com" , Password = "afewfoiewiou" } ) ;
2020-12-10 15:34:50 +01:00
2022-01-14 09:50:29 +01:00
// But not an admin
await AssertHttpError ( 403 ,
async ( ) = > await user1Client . CreateUser ( new CreateApplicationUserRequest ( )
{
Email = "admin8@gmail.com" ,
Password = "afewfoiewiou" ,
IsAdministrator = true
} ) ) ;
2020-12-08 08:12:29 +01:00
2022-01-14 09:50:29 +01:00
// If we set DisableNonAdminCreateUserApi = true, it should always fail to create a user unless you are an admin
await settings . UpdateSetting ( new PoliciesSettings ( ) { LockSubscription = false , DisableNonAdminCreateUserApi = true } ) ;
await AssertHttpError ( 403 ,
async ( ) = >
await unauthClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test9@gmail.com" , Password = "afewfoiewiou" } ) ) ;
await AssertHttpError ( 403 ,
async ( ) = >
await user1Client . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test9@gmail.com" , Password = "afewfoiewiou" } ) ) ;
await adminClient . CreateUser (
new CreateApplicationUserRequest ( ) { Email = "test9@gmail.com" , Password = "afewfoiewiou" } ) ;
2020-03-18 15:10:15 +01:00
}
2020-05-19 19:59:23 +02:00
2020-06-24 03:34:09 +02:00
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePullPaymentViaAPI ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
2023-01-26 06:43:07 +01:00
tester . ActivateLightning ( ) ;
2022-01-14 09:50:29 +01:00
await tester . StartAsync ( ) ;
2023-01-26 06:43:07 +01:00
await tester . EnsureChannelsSetup ( ) ;
2022-01-14 09:50:29 +01:00
var acc = tester . NewAccount ( ) ;
2023-01-26 06:43:07 +01:00
await acc . GrantAccessAsync ( true ) ;
acc . RegisterLightningNode ( "BTC" , LightningConnectionType . CLightning , false ) ;
2022-01-14 09:50:29 +01:00
var storeId = ( await acc . RegisterDerivationSchemeAsync ( "BTC" , importKeysToNBX : true ) ) . StoreId ;
var client = await acc . CreateClient ( ) ;
2022-04-24 05:19:34 +02:00
var result = await client . CreatePullPayment ( storeId , new CreatePullPaymentRequest ( )
2020-06-24 03:34:09 +02:00
{
2022-01-14 09:50:29 +01:00
Name = "Test" ,
2022-02-10 06:54:00 +01:00
Description = "Test description" ,
2022-01-14 09:50:29 +01:00
Amount = 12.3 m ,
Currency = "BTC" ,
PaymentMethods = new [ ] { "BTC" }
} ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
void VerifyResult ( )
{
Assert . Equal ( "Test" , result . Name ) ;
2022-02-10 06:54:00 +01:00
Assert . Equal ( "Test description" , result . Description ) ;
2022-01-14 09:50:29 +01:00
Assert . Null ( result . Period ) ;
// If it contains ? it means that we are resolving an unknown route with the link generator
Assert . DoesNotContain ( "?" , result . ViewLink ) ;
Assert . False ( result . Archived ) ;
Assert . Equal ( "BTC" , result . Currency ) ;
Assert . Equal ( 12.3 m , result . Amount ) ;
}
VerifyResult ( ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var unauthenticated = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
result = await unauthenticated . GetPullPayment ( result . Id ) ;
VerifyResult ( ) ;
await AssertHttpError ( 404 , async ( ) = > await unauthenticated . GetPullPayment ( "lol" ) ) ;
// Can't list pull payments unauthenticated
await AssertHttpError ( 401 , async ( ) = > await unauthenticated . GetPullPayments ( storeId ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var pullPayments = await client . GetPullPayments ( storeId ) ;
result = Assert . Single ( pullPayments ) ;
VerifyResult ( ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var test2 = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test 2" ,
Amount = 12.3 m ,
Currency = "BTC" ,
2022-01-24 12:17:09 +01:00
PaymentMethods = new [ ] { "BTC" } ,
BOLT11Expiration = TimeSpan . FromDays ( 31.0 )
2022-01-14 09:50:29 +01:00
} ) ;
2022-01-24 12:17:09 +01:00
Assert . Equal ( TimeSpan . FromDays ( 31.0 ) , test2 . BOLT11Expiration ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Can't archive without knowing the walletId" ) ;
var ex = await AssertAPIError ( "missing-permission" , async ( ) = > await client . ArchivePullPayment ( "lol" , result . Id ) ) ;
Assert . Equal ( "btcpay.store.canmanagepullpayments" , ( ( GreenfieldPermissionAPIError ) ex . APIError ) . MissingPermission ) ;
TestLogs . LogInformation ( "Can't archive without permission" ) ;
await AssertAPIError ( "unauthenticated" , async ( ) = > await unauthenticated . ArchivePullPayment ( storeId , result . Id ) ) ;
await client . ArchivePullPayment ( storeId , result . Id ) ;
result = await unauthenticated . GetPullPayment ( result . Id ) ;
2022-01-24 12:17:09 +01:00
Assert . Equal ( TimeSpan . FromDays ( 30.0 ) , result . BOLT11Expiration ) ;
2022-01-14 09:50:29 +01:00
Assert . True ( result . Archived ) ;
var pps = await client . GetPullPayments ( storeId ) ;
result = Assert . Single ( pps ) ;
Assert . Equal ( "Test 2" , result . Name ) ;
pps = await client . GetPullPayments ( storeId , true ) ;
Assert . Equal ( 2 , pps . Length ) ;
Assert . Equal ( "Test 2" , pps [ 0 ] . Name ) ;
Assert . Equal ( "Test" , pps [ 1 ] . Name ) ;
var payouts = await unauthenticated . GetPayouts ( pps [ 0 ] . Id ) ;
Assert . Empty ( payouts ) ;
var destination = ( await tester . ExplorerNode . GetNewAddressAsync ( ) ) . ToString ( ) ;
await this . AssertAPIError ( "overdraft" , async ( ) = > await unauthenticated . CreatePayout ( pps [ 0 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
Amount = 1_000_000 m ,
PaymentMethod = "BTC" ,
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
await this . AssertAPIError ( "archived" , async ( ) = > await unauthenticated . CreatePayout ( pps [ 1 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
PaymentMethod = "BTC"
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var payout = await unauthenticated . CreatePayout ( pps [ 0 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
PaymentMethod = "BTC"
} ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
payouts = await unauthenticated . GetPayouts ( pps [ 0 ] . Id ) ;
var payout2 = Assert . Single ( payouts ) ;
Assert . Equal ( payout . Amount , payout2 . Amount ) ;
Assert . Equal ( payout . Id , payout2 . Id ) ;
Assert . Equal ( destination , payout2 . Destination ) ;
Assert . Equal ( PayoutState . AwaitingApproval , payout . State ) ;
Assert . Equal ( "BTC" , payout2 . PaymentMethod ) ;
Assert . Equal ( "BTC" , payout2 . CryptoCode ) ;
Assert . Null ( payout . PaymentMethodAmount ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Can't overdraft" ) ;
2021-12-31 08:59:02 +01:00
2022-01-14 09:50:29 +01:00
var destination2 = ( await tester . ExplorerNode . GetNewAddressAsync ( ) ) . ToString ( ) ;
await this . AssertAPIError ( "overdraft" , async ( ) = > await unauthenticated . CreatePayout ( pps [ 0 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination2 ,
Amount = 0.00001 m ,
PaymentMethod = "BTC"
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Can't create too low payout" ) ;
await this . AssertAPIError ( "amount-too-low" , async ( ) = > await unauthenticated . CreatePayout ( pps [ 0 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination2 ,
PaymentMethod = "BTC"
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Can archive payout" ) ;
await client . CancelPayout ( storeId , payout . Id ) ;
payouts = await unauthenticated . GetPayouts ( pps [ 0 ] . Id ) ;
Assert . Empty ( payouts ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
payouts = await client . GetPayouts ( pps [ 0 ] . Id , true ) ;
payout = Assert . Single ( payouts ) ;
Assert . Equal ( PayoutState . Cancelled , payout . State ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Can create payout after cancelling" ) ;
payout = await unauthenticated . CreatePayout ( pps [ 0 ] . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
PaymentMethod = "BTC"
} ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var start = RoundSeconds ( DateTimeOffset . Now + TimeSpan . FromDays ( 7.0 ) ) ;
var inFuture = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Starts in the future" ,
Amount = 12.3 m ,
StartsAt = start ,
Currency = "BTC" ,
PaymentMethods = new [ ] { "BTC" }
} ) ;
Assert . Equal ( start , inFuture . StartsAt ) ;
Assert . Null ( inFuture . ExpiresAt ) ;
await this . AssertAPIError ( "not-started" , async ( ) = > await unauthenticated . CreatePayout ( inFuture . Id , new CreatePayoutRequest ( )
{
Amount = 1.0 m ,
Destination = destination ,
PaymentMethod = "BTC"
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
var expires = RoundSeconds ( DateTimeOffset . Now - TimeSpan . FromDays ( 7.0 ) ) ;
var inPast = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Will expires" ,
Amount = 12.3 m ,
ExpiresAt = expires ,
Currency = "BTC" ,
PaymentMethods = new [ ] { "BTC" }
} ) ;
await this . AssertAPIError ( "expired" , async ( ) = > await unauthenticated . CreatePayout ( inPast . Id , new CreatePayoutRequest ( )
{
Amount = 1.0 m ,
Destination = destination ,
PaymentMethod = "BTC"
} ) ) ;
2020-06-24 03:34:09 +02:00
2022-01-14 09:50:29 +01:00
await this . AssertValidationError ( new [ ] { "ExpiresAt" } , async ( ) = > await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test 2" ,
Amount = 12.3 m ,
StartsAt = DateTimeOffset . UtcNow ,
ExpiresAt = DateTimeOffset . UtcNow - TimeSpan . FromDays ( 1 )
} ) ) ;
2020-06-24 06:44:26 +02:00
2022-01-14 09:50:29 +01:00
TestLogs . LogInformation ( "Create a pull payment with USD" ) ;
var pp = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test USD" ,
Amount = 5000 m ,
Currency = "USD" ,
PaymentMethods = new [ ] { "BTC" }
} ) ;
2020-06-24 06:44:26 +02:00
2023-01-26 06:43:07 +01:00
await this . AssertAPIError ( "lnurl-not-supported" , async ( ) = > await unauthenticated . GetPullPaymentLNURL ( pp . Id ) ) ;
2022-01-14 09:50:29 +01:00
destination = ( await tester . ExplorerNode . GetNewAddressAsync ( ) ) . ToString ( ) ;
TestLogs . LogInformation ( "Try to pay it in BTC" ) ;
payout = await unauthenticated . CreatePayout ( pp . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
PaymentMethod = "BTC"
} ) ;
await this . AssertAPIError ( "old-revision" , async ( ) = > await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( )
{
Revision = - 1
} ) ) ;
await this . AssertAPIError ( "rate-unavailable" , async ( ) = > await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( )
{
RateRule = "DONOTEXIST(BTC_USD)"
} ) ) ;
payout = await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( )
{
Revision = payout . Revision
} ) ;
Assert . Equal ( PayoutState . AwaitingPayment , payout . State ) ;
Assert . NotNull ( payout . PaymentMethodAmount ) ;
Assert . Equal ( 1.0 m , payout . PaymentMethodAmount ) ; // 1 BTC == 5000 USD in tests
await this . AssertAPIError ( "invalid-state" , async ( ) = > await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( )
{
Revision = payout . Revision
} ) ) ;
2021-05-13 10:50:08 +02:00
2022-01-14 09:50:29 +01:00
// Create one pull payment with an amount of 9 decimals
var test3 = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test 2" ,
Amount = 12.303228134 m ,
Currency = "BTC" ,
PaymentMethods = new [ ] { "BTC" }
} ) ;
destination = ( await tester . ExplorerNode . GetNewAddressAsync ( ) ) . ToString ( ) ;
payout = await unauthenticated . CreatePayout ( test3 . Id , new CreatePayoutRequest ( )
{
Destination = destination ,
PaymentMethod = "BTC"
} ) ;
payout = await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( ) ) ;
// The payout should round the value of the payment down to the network of the payment method
Assert . Equal ( 12.30322814 m , payout . PaymentMethodAmount ) ;
Assert . Equal ( 12.303228134 m , payout . Amount ) ;
await client . MarkPayoutPaid ( storeId , payout . Id ) ;
payout = ( await client . GetPayouts ( payout . PullPaymentId ) ) . First ( data = > data . Id = = payout . Id ) ;
Assert . Equal ( PayoutState . Completed , payout . State ) ;
await AssertAPIError ( "invalid-state" , async ( ) = > await client . MarkPayoutPaid ( storeId , payout . Id ) ) ;
2023-01-26 06:43:07 +01:00
// Test LNURL values
var test4 = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test 3" ,
Amount = 12.303228134 m ,
Currency = "BTC" ,
PaymentMethods = new [ ] { "BTC" , "BTC-LightningNetwork" , "BTC_LightningLike" }
} ) ;
var lnrURLs = await unauthenticated . GetPullPaymentLNURL ( test4 . Id ) ;
Assert . IsType < string > ( lnrURLs . LNURLBech32 ) ;
Assert . IsType < string > ( lnrURLs . LNURLUri ) ;
2023-06-16 03:56:17 +02:00
Assert . Equal ( 12.303228134 m , test4 . Amount ) ;
Assert . Equal ( "BTC" , test4 . Currency ) ;
// Test with SATS denomination values
var testSats = await client . CreatePullPayment ( storeId , new Client . Models . CreatePullPaymentRequest ( )
{
Name = "Test SATS" ,
Amount = 21000 ,
Currency = "SATS" ,
PaymentMethods = new [ ] { "BTC" , "BTC-LightningNetwork" , "BTC_LightningLike" }
} ) ;
lnrURLs = await unauthenticated . GetPullPaymentLNURL ( testSats . Id ) ;
Assert . IsType < string > ( lnrURLs . LNURLBech32 ) ;
Assert . IsType < string > ( lnrURLs . LNURLUri ) ;
Assert . Equal ( 21000 , testSats . Amount ) ;
Assert . Equal ( "SATS" , testSats . Currency ) ;
2023-04-10 04:07:03 +02:00
2023-01-26 01:46:05 +01:00
//permission test around auto approved pps and payouts
var nonApproved = await acc . CreateClient ( Policies . CanCreateNonApprovedPullPayments ) ;
var approved = await acc . CreateClient ( Policies . CanCreatePullPayments ) ;
await AssertPermissionError ( Policies . CanCreatePullPayments , async ( ) = >
{
var pullPayment = await nonApproved . CreatePullPayment ( acc . StoreId , new CreatePullPaymentRequest ( )
{
Amount = 100 ,
Currency = "USD" ,
Name = "pull payment" ,
PaymentMethods = new [ ] { "BTC" } ,
AutoApproveClaims = true
} ) ;
} ) ;
await AssertPermissionError ( Policies . CanCreatePullPayments , async ( ) = >
{
var pullPayment = await nonApproved . CreatePayout ( acc . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 100 ,
PaymentMethod = "BTC" ,
Approved = true ,
Destination = new Key ( ) . GetAddress ( ScriptPubKeyType . TaprootBIP86 , Network . RegTest ) . ToString ( )
} ) ;
} ) ;
2023-04-10 04:07:03 +02:00
2023-01-26 01:46:05 +01:00
var pullPayment = await approved . CreatePullPayment ( acc . StoreId , new CreatePullPaymentRequest ( )
{
Amount = 100 ,
Currency = "USD" ,
Name = "pull payment" ,
PaymentMethods = new [ ] { "BTC" } ,
AutoApproveClaims = true
} ) ;
2023-04-10 04:07:03 +02:00
2023-01-26 01:46:05 +01:00
var p = await approved . CreatePayout ( acc . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 100 ,
PaymentMethod = "BTC" ,
Approved = true ,
Destination = new Key ( ) . GetAddress ( ScriptPubKeyType . TaprootBIP86 , Network . RegTest ) . ToString ( )
} ) ;
2020-06-24 03:34:09 +02:00
}
2022-11-15 10:40:57 +01:00
[Fact]
[Trait("Integration", "Integration")]
public async Task CanProcessPayoutsExternally ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var acc = tester . NewAccount ( ) ;
acc . Register ( ) ;
await acc . CreateStoreAsync ( ) ;
var storeId = ( await acc . RegisterDerivationSchemeAsync ( "BTC" , importKeysToNBX : true ) ) . StoreId ;
var client = await acc . CreateClient ( ) ;
var address = await tester . ExplorerNode . GetNewAddressAsync ( ) ;
var payout = await client . CreatePayout ( storeId , new CreatePayoutThroughStoreRequest ( )
{
2023-01-06 14:18:07 +01:00
Approved = false ,
PaymentMethod = "BTC" ,
Amount = 0.0001 m ,
2023-07-24 11:37:18 +02:00
Destination = address . ToString ( ) ,
2022-11-15 10:40:57 +01:00
} ) ;
await AssertAPIError ( "invalid-state" , async ( ) = >
{
2023-01-06 14:18:07 +01:00
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( ) { State = PayoutState . Completed } ) ;
2022-11-15 10:40:57 +01:00
} ) ;
await client . ApprovePayout ( storeId , payout . Id , new ApprovePayoutRequest ( ) ) ;
2023-01-06 14:18:07 +01:00
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( ) { State = PayoutState . Completed } ) ;
Assert . Equal ( PayoutState . Completed , ( await client . GetStorePayouts ( storeId , false ) ) . Single ( data = > data . Id = = payout . Id ) . State ) ;
Assert . Null ( ( await client . GetStorePayouts ( storeId , false ) ) . Single ( data = > data . Id = = payout . Id ) . PaymentProof ) ;
foreach ( var state in new [ ] { PayoutState . AwaitingApproval , PayoutState . Cancelled , PayoutState . Completed , PayoutState . AwaitingApproval , PayoutState . InProgress } )
2022-11-15 10:40:57 +01:00
{
await AssertAPIError ( "invalid-state" , async ( ) = >
{
2023-01-06 14:18:07 +01:00
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( ) { State = state } ) ;
2022-11-15 10:40:57 +01:00
} ) ;
}
payout = await client . CreatePayout ( storeId , new CreatePayoutThroughStoreRequest ( )
{
Approved = true ,
PaymentMethod = "BTC" ,
Amount = 0.0001 m ,
Destination = address . ToString ( )
} ) ;
payout = await client . GetStorePayout ( storeId , payout . Id ) ;
Assert . NotNull ( payout ) ;
Assert . Equal ( PayoutState . AwaitingPayment , payout . State ) ;
2023-01-06 14:18:07 +01:00
await AssertValidationError ( new [ ] { "PaymentProof" } , async ( ) = >
{
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( )
{
State = PayoutState . Completed ,
PaymentProof = JObject . FromObject ( new
{
test = "zyx"
} )
} ) ;
} ) ;
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( )
2022-11-15 10:40:57 +01:00
{
2023-01-06 14:18:07 +01:00
State = PayoutState . InProgress ,
PaymentProof = JObject . FromObject ( new
2022-11-15 10:40:57 +01:00
{
2023-01-06 14:18:07 +01:00
proofType = "external-proof"
} )
2022-11-15 10:40:57 +01:00
} ) ;
payout = await client . GetStorePayout ( storeId , payout . Id ) ;
Assert . NotNull ( payout ) ;
Assert . Equal ( PayoutState . InProgress , payout . State ) ;
Assert . True ( payout . PaymentProof . TryGetValue ( "proofType" , out var savedType ) ) ;
2023-01-06 14:18:07 +01:00
Assert . Equal ( "external-proof" , savedType ) ;
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( )
{
State = PayoutState . AwaitingPayment ,
PaymentProof = JObject . FromObject ( new
{
proofType = "external-proof" ,
id = "finality proof" ,
link = "proof.com"
} )
} ) ;
2022-11-15 10:40:57 +01:00
payout = await client . GetStorePayout ( storeId , payout . Id ) ;
Assert . NotNull ( payout ) ;
Assert . Null ( payout . PaymentProof ) ;
Assert . Equal ( PayoutState . AwaitingPayment , payout . State ) ;
2023-01-06 14:18:07 +01:00
await client . MarkPayout ( storeId , payout . Id , new MarkPayoutRequest ( )
2022-11-15 10:40:57 +01:00
{
2023-01-06 14:18:07 +01:00
State = PayoutState . Completed ,
PaymentProof = JObject . FromObject ( new
{
proofType = "external-proof" ,
id = "finality proof" ,
link = "proof.com"
} )
} ) ;
2022-11-15 10:40:57 +01:00
payout = await client . GetStorePayout ( storeId , payout . Id ) ;
Assert . NotNull ( payout ) ;
Assert . Equal ( PayoutState . Completed , payout . State ) ;
Assert . True ( payout . PaymentProof . TryGetValue ( "proofType" , out savedType ) ) ;
Assert . True ( payout . PaymentProof . TryGetValue ( "link" , out var savedLink ) ) ;
Assert . True ( payout . PaymentProof . TryGetValue ( "id" , out var savedId ) ) ;
2023-01-06 14:18:07 +01:00
Assert . Equal ( "external-proof" , savedType ) ;
Assert . Equal ( "finality proof" , savedId ) ;
Assert . Equal ( "proof.com" , savedLink ) ;
2022-11-15 10:40:57 +01:00
}
2020-06-24 03:34:09 +02:00
private DateTimeOffset RoundSeconds ( DateTimeOffset dateTimeOffset )
{
return new DateTimeOffset ( dateTimeOffset . Year , dateTimeOffset . Month , dateTimeOffset . Day , dateTimeOffset . Hour , dateTimeOffset . Minute , dateTimeOffset . Second , dateTimeOffset . Offset ) ;
}
2022-01-14 05:05:23 +01:00
private async Task < GreenfieldAPIException > AssertAPIError ( string expectedError , Func < Task > act )
2020-06-24 03:34:09 +02:00
{
2022-01-14 05:05:23 +01:00
var err = await Assert . ThrowsAsync < GreenfieldAPIException > ( async ( ) = > await act ( ) ) ;
2020-06-24 03:34:09 +02:00
Assert . Equal ( expectedError , err . APIError . Code ) ;
2021-12-16 15:04:06 +01:00
return err ;
2020-06-24 03:34:09 +02:00
}
2022-01-14 05:05:23 +01:00
private async Task < GreenfieldAPIException > AssertPermissionError ( string expectedPermission , Func < Task > act )
2022-01-11 09:22:10 +01:00
{
2022-01-14 05:05:23 +01:00
var err = await Assert . ThrowsAsync < GreenfieldAPIException > ( async ( ) = > await act ( ) ) ;
2022-01-11 09:22:10 +01:00
var err2 = Assert . IsType < GreenfieldPermissionAPIError > ( err . APIError ) ;
Assert . Equal ( expectedPermission , err2 . MissingPermission ) ;
return err ;
}
2020-06-24 03:34:09 +02:00
2020-03-24 16:18:43 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoresControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
await user . MakeAdmin ( ) ;
var client = await user . CreateClient ( Policies . Unrestricted ) ;
//create store
var newStore = await client . CreateStore ( new CreateStoreRequest ( ) { Name = "A" } ) ;
//update store
2023-02-21 15:31:11 +01:00
Assert . Empty ( newStore . PaymentMethodCriteria ) ;
await client . GenerateOnChainWallet ( newStore . Id , "BTC" , new GenerateOnChainWalletRequest ( ) ) ;
2023-04-10 04:07:03 +02:00
var updatedStore = await client . UpdateStore ( newStore . Id , new UpdateStoreRequest ( )
{
Name = "B" ,
PaymentMethodCriteria = new List < PaymentMethodCriteriaData > ( )
2023-02-21 15:31:11 +01:00
{
new ( )
{
Amount = 10 ,
Above = true ,
PaymentMethod = "BTC" ,
CurrencyCode = "USD"
}
2023-04-10 04:07:03 +02:00
}
} ) ;
2022-01-14 09:50:29 +01:00
Assert . Equal ( "B" , updatedStore . Name ) ;
2023-02-21 15:31:11 +01:00
var s = ( await client . GetStore ( newStore . Id ) ) ;
Assert . Equal ( "B" , s . Name ) ;
var pmc = Assert . Single ( s . PaymentMethodCriteria ) ;
//check that pmc equals the one we set
Assert . Equal ( 10 , pmc . Amount ) ;
Assert . True ( pmc . Above ) ;
Assert . Equal ( "BTC" , pmc . PaymentMethod ) ;
Assert . Equal ( "USD" , pmc . CurrencyCode ) ;
2023-04-10 04:07:03 +02:00
updatedStore = await client . UpdateStore ( newStore . Id , new UpdateStoreRequest ( ) { Name = "B" } ) ;
2023-02-21 15:31:11 +01:00
Assert . Empty ( newStore . PaymentMethodCriteria ) ;
2023-04-10 04:07:03 +02:00
2022-01-14 09:50:29 +01:00
//list stores
var stores = await client . GetStores ( ) ;
var storeIds = stores . Select ( data = > data . Id ) ;
var storeNames = stores . Select ( data = > data . Name ) ;
Assert . NotNull ( stores ) ;
Assert . Equal ( 2 , stores . Count ( ) ) ;
Assert . Contains ( newStore . Id , storeIds ) ;
Assert . Contains ( user . StoreId , storeIds ) ;
//get store
var store = await client . GetStore ( user . StoreId ) ;
Assert . Equal ( user . StoreId , store . Id ) ;
Assert . Contains ( store . Name , storeNames ) ;
//remove store
await client . RemoveStore ( newStore . Id ) ;
await AssertHttpError ( 403 , async ( ) = >
2020-03-24 16:18:43 +01:00
{
2022-01-14 09:50:29 +01:00
await client . GetStore ( newStore . Id ) ;
} ) ;
Assert . Single ( await client . GetStores ( ) ) ;
newStore = await client . CreateStore ( new CreateStoreRequest ( ) { Name = "A" } ) ;
var scopedClient =
await user . CreateClient ( Permission . Create ( Policies . CanViewStoreSettings , user . StoreId ) . ToString ( ) ) ;
Assert . Single ( await scopedClient . GetStores ( ) ) ;
// We strip the user's Owner right, so the key should not work
using var ctx = tester . PayTester . GetService < Data . ApplicationDbContextFactory > ( ) . CreateContext ( ) ;
var storeEntity = await ctx . UserStore . SingleAsync ( u = > u . ApplicationUserId = = user . UserId & & u . StoreDataId = = newStore . Id ) ;
2023-05-26 16:49:32 +02:00
var roleId = ( await tester . PayTester . GetService < StoreRepository > ( ) . GetStoreRoles ( null ) ) . Single ( r = > r . Role = = "Guest" ) . Id ;
storeEntity . StoreRoleId = roleId ;
2022-01-14 09:50:29 +01:00
await ctx . SaveChangesAsync ( ) ;
await AssertHttpError ( 403 , async ( ) = > await client . UpdateStore ( newStore . Id , new UpdateStoreRequest ( ) { Name = "B" } ) ) ;
2023-03-08 13:36:51 +01:00
client = await user . CreateClient ( Policies . Unrestricted ) ;
stores = await client . GetStores ( ) ;
foreach ( var s2 in stores )
{
await tester . PayTester . StoreRepository . DeleteStore ( s2 . Id ) ;
}
tester . DeleteStore = false ;
Assert . Empty ( await client . GetStores ( ) ) ;
2020-03-24 16:18:43 +01:00
}
2020-05-19 19:59:23 +02:00
2022-01-14 05:05:23 +01:00
private async Task < GreenfieldValidationException > AssertValidationError ( string [ ] fields , Func < Task > act )
2020-06-08 16:40:58 +02:00
{
var remainingFields = fields . ToHashSet ( ) ;
2022-01-14 05:05:23 +01:00
var ex = await Assert . ThrowsAsync < GreenfieldValidationException > ( act ) ;
2020-06-08 16:40:58 +02:00
foreach ( var field in fields )
{
Assert . Contains ( field , ex . ValidationErrors . Select ( e = > e . Path ) . ToArray ( ) ) ;
remainingFields . Remove ( field ) ;
}
Assert . Empty ( remainingFields ) ;
2021-03-02 03:11:58 +01:00
return ex ;
2020-06-08 16:40:58 +02:00
}
2020-03-18 15:10:15 +01:00
private async Task AssertHttpError ( int code , Func < Task > act )
{
2022-01-14 05:05:23 +01:00
var ex = await Assert . ThrowsAsync < GreenfieldAPIException > ( act ) ;
2021-12-16 15:04:06 +01:00
Assert . Equal ( code , ex . HttpCode ) ;
2020-03-18 15:10:15 +01:00
}
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
private async Task AssertApiError ( int httpStatus , string errorCode , Func < Task > act )
{
var ex = await Assert . ThrowsAsync < GreenfieldAPIException > ( act ) ;
Assert . Equal ( httpStatus , ex . HttpCode ) ;
Assert . Equal ( errorCode , ex . APIError . Code ) ;
}
2020-03-18 15:10:15 +01:00
2020-03-12 14:59:24 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task UsersControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( newDb : true ) ;
tester . PayTester . DisableRegistration = true ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
await user . MakeAdmin ( ) ;
var clientProfile = await user . CreateClient ( Policies . CanModifyProfile ) ;
var clientServer = await user . CreateClient ( Policies . CanCreateUser , Policies . CanViewProfile ) ;
var clientInsufficient = await user . CreateClient ( Policies . CanModifyStoreSettings ) ;
var clientBasic = await user . CreateClient ( ) ;
2020-03-16 08:36:55 +01:00
2020-03-12 14:59:24 +01:00
2022-01-14 09:50:29 +01:00
var apiKeyProfileUserData = await clientProfile . GetCurrentUser ( ) ;
Assert . NotNull ( apiKeyProfileUserData ) ;
Assert . Equal ( apiKeyProfileUserData . Id , user . UserId ) ;
Assert . Equal ( apiKeyProfileUserData . Email , user . RegisterDetails . Email ) ;
Assert . Contains ( "ServerAdmin" , apiKeyProfileUserData . Roles ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
await AssertHttpError ( 403 , async ( ) = > await clientInsufficient . GetCurrentUser ( ) ) ;
await clientServer . GetCurrentUser ( ) ;
await clientProfile . GetCurrentUser ( ) ;
await clientBasic . GetCurrentUser ( ) ;
2020-03-13 11:47:22 +01:00
2022-01-14 09:50:29 +01:00
await AssertHttpError ( 403 , async ( ) = >
await clientInsufficient . CreateUser ( new CreateApplicationUserRequest ( )
2020-03-13 11:47:22 +01:00
{
2020-06-27 08:34:03 +02:00
Email = $"{Guid.NewGuid()}@g.com" ,
Password = Guid . NewGuid ( ) . ToString ( )
2022-01-14 09:50:29 +01:00
} ) ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
var newUser = await clientServer . CreateUser ( new CreateApplicationUserRequest ( )
{
Email = $"{Guid.NewGuid()}@g.com" ,
Password = Guid . NewGuid ( ) . ToString ( )
} ) ;
Assert . NotNull ( newUser ) ;
var newUser2 = await clientBasic . CreateUser ( new CreateApplicationUserRequest ( )
{
Email = $"{Guid.NewGuid()}@g.com" ,
Password = Guid . NewGuid ( ) . ToString ( )
} ) ;
Assert . NotNull ( newUser2 ) ;
await AssertValidationError ( new [ ] { "Email" } , async ( ) = >
await clientServer . CreateUser ( new CreateApplicationUserRequest ( )
2020-03-20 17:14:47 +01:00
{
2022-01-14 09:50:29 +01:00
Email = $"{Guid.NewGuid()}" ,
2020-06-27 08:34:03 +02:00
Password = Guid . NewGuid ( ) . ToString ( )
2022-01-14 09:50:29 +01:00
} ) ) ;
2020-03-18 15:10:15 +01:00
2022-01-14 09:50:29 +01:00
await AssertValidationError ( new [ ] { "Email" } , async ( ) = >
await clientServer . CreateUser (
new CreateApplicationUserRequest ( ) { Password = Guid . NewGuid ( ) . ToString ( ) } ) ) ;
2020-03-12 14:59:24 +01:00
}
2020-05-19 19:59:23 +02:00
2020-11-13 06:01:51 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseWebhooks ( )
{
void AssertHook ( FakeServer fakeServer , Client . Models . StoreWebhookData hook )
{
Assert . True ( hook . Enabled ) ;
Assert . True ( hook . AuthorizedEvents . Everything ) ;
2020-11-13 08:28:15 +01:00
Assert . False ( hook . AutomaticRedelivery ) ;
2020-11-13 06:01:51 +01:00
Assert . Equal ( fakeServer . ServerUri . AbsoluteUri , hook . Url ) ;
}
2023-05-28 16:44:10 +02:00
using var tester = CreateServerTester ( newDb : true ) ;
2020-11-13 06:01:51 +01:00
using var fakeServer = new FakeServer ( ) ;
await fakeServer . Start ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
user . RegisterDerivationScheme ( "BTC" ) ;
var clientProfile = await user . CreateClient ( Policies . CanModifyStoreWebhooks , Policies . CanCreateInvoice ) ;
var hook = await clientProfile . CreateWebhook ( user . StoreId , new CreateStoreWebhookRequest ( )
{
Url = fakeServer . ServerUri . AbsoluteUri ,
AutomaticRedelivery = false
} ) ;
Assert . NotNull ( hook . Secret ) ;
AssertHook ( fakeServer , hook ) ;
hook = await clientProfile . GetWebhook ( user . StoreId , hook . Id ) ;
AssertHook ( fakeServer , hook ) ;
var hooks = await clientProfile . GetWebhooks ( user . StoreId ) ;
hook = Assert . Single ( hooks ) ;
AssertHook ( fakeServer , hook ) ;
await clientProfile . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( ) { Currency = "USD" , Amount = 100 } ) ;
var req = await fakeServer . GetNextRequest ( ) ;
req . Response . StatusCode = 200 ;
fakeServer . Done ( ) ;
hook = await clientProfile . UpdateWebhook ( user . StoreId , hook . Id , new UpdateStoreWebhookRequest ( )
{
Url = hook . Url ,
Secret = "lol" ,
AutomaticRedelivery = false
} ) ;
Assert . Null ( hook . Secret ) ;
AssertHook ( fakeServer , hook ) ;
2022-01-13 05:21:54 +01:00
WebhookDeliveryData delivery = null ;
await TestUtils . EventuallyAsync ( async ( ) = >
{
var deliveries = await clientProfile . GetWebhookDeliveries ( user . StoreId , hook . Id ) ;
delivery = Assert . Single ( deliveries ) ;
} ) ;
2023-01-06 14:18:07 +01:00
2020-11-13 06:01:51 +01:00
delivery = await clientProfile . GetWebhookDelivery ( user . StoreId , hook . Id , delivery . Id ) ;
Assert . NotNull ( delivery ) ;
Assert . Equal ( WebhookDeliveryStatus . HttpSuccess , delivery . Status ) ;
var newDeliveryId = await clientProfile . RedeliverWebhook ( user . StoreId , hook . Id , delivery . Id ) ;
req = await fakeServer . GetNextRequest ( ) ;
req . Response . StatusCode = 404 ;
2023-04-19 14:13:31 +02:00
Assert . StartsWith ( "BTCPayServer" , Assert . Single ( req . Request . Headers . UserAgent ) ) ;
2020-11-13 06:01:51 +01:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
2021-10-06 04:25:21 +02:00
// Releasing semaphore several times may help making this test less flaky
fakeServer . Done ( ) ;
2020-11-13 06:01:51 +01:00
var newDelivery = await clientProfile . GetWebhookDelivery ( user . StoreId , hook . Id , newDeliveryId ) ;
Assert . NotNull ( newDelivery ) ;
Assert . Equal ( 404 , newDelivery . HttpCode ) ;
2020-11-16 04:05:15 +01:00
var req = await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , hook . Id , newDeliveryId ) ;
2021-04-20 05:36:20 +02:00
Assert . Equal ( delivery . Id , req . OriginalDeliveryId ) ;
2020-11-16 04:05:15 +01:00
Assert . True ( req . IsRedelivery ) ;
2020-11-13 06:01:51 +01:00
Assert . Equal ( WebhookDeliveryStatus . HttpError , newDelivery . Status ) ;
} ) ;
2022-01-13 05:27:02 +01:00
var deliveries = await clientProfile . GetWebhookDeliveries ( user . StoreId , hook . Id ) ;
2020-11-13 06:01:51 +01:00
Assert . Equal ( 2 , deliveries . Length ) ;
Assert . Equal ( newDeliveryId , deliveries [ 0 ] . Id ) ;
var jObj = await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , hook . Id , newDeliveryId ) ;
Assert . NotNull ( jObj ) ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Should not be able to access webhook without proper auth" ) ;
2020-11-13 06:01:51 +01:00
var unauthorized = await user . CreateClient ( Policies . CanCreateInvoice ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await unauthorized . GetWebhookDeliveryRequest ( user . StoreId , hook . Id , newDeliveryId ) ;
} ) ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Can use btcpay.store.canmodifystoresettings to query webhooks" ) ;
2020-11-13 06:01:51 +01:00
clientProfile = await user . CreateClient ( Policies . CanModifyStoreSettings , Policies . CanCreateInvoice ) ;
await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , hook . Id , newDeliveryId ) ;
2023-05-28 16:44:10 +02:00
TestLogs . LogInformation ( "Can prune deliveries" ) ;
var cleanup = tester . PayTester . GetService < HostedServices . CleanupWebhookDeliveriesTask > ( ) ;
cleanup . BatchSize = 1 ;
cleanup . PruneAfter = TimeSpan . Zero ;
await cleanup . Do ( default ) ;
await AssertHttpError ( 409 , ( ) = > clientProfile . RedeliverWebhook ( user . StoreId , hook . Id , delivery . Id ) ) ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Testing corner cases" ) ;
2020-11-13 06:01:51 +01:00
Assert . Null ( await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , "lol" , newDeliveryId ) ) ;
Assert . Null ( await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , hook . Id , "lol" ) ) ;
Assert . Null ( await clientProfile . GetWebhookDeliveryRequest ( user . StoreId , "lol" , "lol" ) ) ;
Assert . Null ( await clientProfile . GetWebhook ( user . StoreId , "lol" ) ) ;
await AssertHttpError ( 404 , async ( ) = >
{
await clientProfile . UpdateWebhook ( user . StoreId , "lol" , new UpdateStoreWebhookRequest ( ) { Url = hook . Url } ) ;
} ) ;
Assert . True ( await clientProfile . DeleteWebhook ( user . StoreId , hook . Id ) ) ;
Assert . False ( await clientProfile . DeleteWebhook ( user . StoreId , hook . Id ) ) ;
}
2020-04-16 15:39:08 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task HealthControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
2020-04-16 15:39:08 +02:00
2022-01-14 09:50:29 +01:00
var apiHealthData = await unauthClient . GetHealth ( ) ;
Assert . NotNull ( apiHealthData ) ;
Assert . True ( apiHealthData . Synchronized ) ;
2020-04-16 15:39:08 +02:00
}
2020-05-19 19:59:23 +02:00
2020-05-16 23:57:49 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ServerInfoControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetServerInfo ( ) ) ;
2020-05-16 23:57:49 +02:00
2022-01-14 09:50:29 +01:00
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
var clientBasic = await user . CreateClient ( ) ;
var serverInfoData = await clientBasic . GetServerInfo ( ) ;
Assert . NotNull ( serverInfoData ) ;
Assert . NotNull ( serverInfoData . Version ) ;
Assert . NotNull ( serverInfoData . Onion ) ;
Assert . True ( serverInfoData . FullySynched ) ;
Assert . Contains ( "BTC" , serverInfoData . SupportedPaymentMethods ) ;
Assert . Contains ( "BTC_LightningLike" , serverInfoData . SupportedPaymentMethods ) ;
Assert . NotNull ( serverInfoData . SyncStatus ) ;
Assert . Single ( serverInfoData . SyncStatus . Select ( s = > s . CryptoCode = = "BTC" ) ) ;
2020-05-16 23:57:49 +02:00
}
2020-05-19 19:59:23 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task PaymentControllerTests ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
await user . MakeAdmin ( ) ;
var client = await user . CreateClient ( Policies . Unrestricted ) ;
var viewOnly = await user . CreateClient ( Policies . CanViewPaymentRequests ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//create payment request
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//validation errors
await AssertValidationError ( new [ ] { "Amount" } , async ( ) = >
{
await client . CreatePaymentRequest ( user . StoreId , new CreatePaymentRequestRequest ( ) { Title = "A" } ) ;
} ) ;
await AssertValidationError ( new [ ] { "Amount" } , async ( ) = >
{
await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Title = "A" , Currency = "BTC" , Amount = 0 } ) ;
} ) ;
await AssertValidationError ( new [ ] { "Currency" } , async ( ) = >
{
await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Title = "A" , Currency = "helloinvalid" , Amount = 1 } ) ;
} ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Title = "A" , Currency = "helloinvalid" , Amount = 1 } ) ;
} ) ;
var newPaymentRequest = await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Title = "A" , Currency = "USD" , Amount = 1 } ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//list payment request
var paymentRequests = await viewOnly . GetPaymentRequests ( user . StoreId ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
Assert . NotNull ( paymentRequests ) ;
Assert . Single ( paymentRequests ) ;
Assert . Equal ( newPaymentRequest . Id , paymentRequests . First ( ) . Id ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//get payment request
var paymentRequest = await viewOnly . GetPaymentRequest ( user . StoreId , newPaymentRequest . Id ) ;
Assert . Equal ( newPaymentRequest . Title , paymentRequest . Title ) ;
Assert . Equal ( newPaymentRequest . StoreId , user . StoreId ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//update payment request
var updateRequest = JObject . FromObject ( paymentRequest ) . ToObject < UpdatePaymentRequestRequest > ( ) ;
updateRequest . Title = "B" ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . UpdatePaymentRequest ( user . StoreId , paymentRequest . Id , updateRequest ) ;
} ) ;
await client . UpdatePaymentRequest ( user . StoreId , paymentRequest . Id , updateRequest ) ;
paymentRequest = await client . GetPaymentRequest ( user . StoreId , newPaymentRequest . Id ) ;
Assert . Equal ( updateRequest . Title , paymentRequest . Title ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
//archive payment request
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . ArchivePaymentRequest ( user . StoreId , paymentRequest . Id ) ;
} ) ;
2020-05-19 19:59:23 +02:00
2022-01-14 09:50:29 +01:00
await client . ArchivePaymentRequest ( user . StoreId , paymentRequest . Id ) ;
Assert . DoesNotContain ( paymentRequest . Id ,
( await client . GetPaymentRequests ( user . StoreId ) ) . Select ( data = > data . Id ) ) ;
2022-11-02 10:41:19 +01:00
var archivedPrId = paymentRequest . Id ;
//let's test some payment stuff with the UI
2022-01-14 09:50:29 +01:00
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var paymentTestPaymentRequest = await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Amount = 0.1 m , Currency = "BTC" , Title = "Payment test title" } ) ;
2020-05-28 09:48:47 +02:00
2022-01-14 09:50:29 +01:00
var invoiceId = Assert . IsType < string > ( Assert . IsType < OkObjectResult > ( await user . GetController < UIPaymentRequestController > ( )
. PayPaymentRequest ( paymentTestPaymentRequest . Id , false ) ) . Value ) ;
2022-11-02 10:41:19 +01:00
async Task Pay ( string invoiceId , bool partialPayment = false )
2022-01-14 09:50:29 +01:00
{
2022-11-02 10:41:19 +01:00
TestLogs . LogInformation ( $"Paying invoice {invoiceId}" ) ;
var invoice = user . BitPay . GetInvoice ( invoiceId ) ;
await tester . WaitForEvent < InvoiceDataChangedEvent > ( async ( ) = >
{
TestLogs . LogInformation ( $"Paying address {invoice.BitcoinAddress}" ) ;
await tester . ExplorerNode . SendToAddressAsync (
BitcoinAddress . Create ( invoice . BitcoinAddress , tester . ExplorerNode . Network ) , invoice . BtcDue ) ;
} ) ;
await TestUtils . EventuallyAsync ( async ( ) = >
{
Assert . Equal ( Invoice . STATUS_PAID , user . BitPay . GetInvoice ( invoiceId ) . Status ) ;
if ( ! partialPayment )
2023-01-06 14:18:07 +01:00
Assert . Equal ( PaymentRequestData . PaymentRequestStatus . Completed , ( await client . GetPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id ) ) . Status ) ;
2022-11-02 10:41:19 +01:00
} ) ;
}
await Pay ( invoiceId ) ;
//Same thing, but with the API
paymentTestPaymentRequest = await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Amount = 0.1 m , Currency = "BTC" , Title = "Payment test title" } ) ;
var paidPrId = paymentTestPaymentRequest . Id ;
var invoiceData = await client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) ) ;
await Pay ( invoiceData . Id ) ;
2022-12-14 06:01:48 +01:00
// Can't update amount once invoice has been created
await AssertValidationError ( new [ ] { "Amount" } , ( ) = > client . UpdatePaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new UpdatePaymentRequestRequest ( )
{
Amount = 294 m
} ) ) ;
2022-11-02 10:41:19 +01:00
// Let's tests some unhappy path
paymentTestPaymentRequest = await client . CreatePaymentRequest ( user . StoreId ,
new CreatePaymentRequestRequest ( ) { Amount = 0.1 m , AllowCustomPaymentAmounts = false , Currency = "BTC" , Title = "Payment test title" } ) ;
await AssertValidationError ( new [ ] { "Amount" } , ( ) = > client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { Amount = - 0.04 m } ) ) ;
await AssertValidationError ( new [ ] { "Amount" } , ( ) = > client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { Amount = 0.04 m } ) ) ;
await client . UpdatePaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new UpdatePaymentRequestRequest ( )
{
Amount = 0.1 m ,
AllowCustomPaymentAmounts = true ,
Currency = "BTC" ,
Title = "Payment test title"
2022-01-14 09:50:29 +01:00
} ) ;
2022-11-02 10:41:19 +01:00
await AssertValidationError ( new [ ] { "Amount" } , ( ) = > client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { Amount = - 0.04 m } ) ) ;
invoiceData = await client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { Amount = 0.04 m } ) ;
Assert . Equal ( 0.04 m , invoiceData . Amount ) ;
var firstPaymentId = invoiceData . Id ;
await AssertAPIError ( "archived" , ( ) = > client . PayPaymentRequest ( user . StoreId , archivedPrId , new PayPaymentRequestRequest ( ) ) ) ;
await client . UpdatePaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new UpdatePaymentRequestRequest ( )
{
Amount = 0.1 m ,
AllowCustomPaymentAmounts = true ,
Currency = "BTC" ,
Title = "Payment test title" ,
ExpiryDate = DateTimeOffset . UtcNow - TimeSpan . FromDays ( 1.0 )
} ) ;
await AssertAPIError ( "expired" , ( ) = > client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) ) ) ;
await AssertAPIError ( "already-paid" , ( ) = > client . PayPaymentRequest ( user . StoreId , paidPrId , new PayPaymentRequestRequest ( ) ) ) ;
await client . UpdatePaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new UpdatePaymentRequestRequest ( )
{
Amount = 0.1 m ,
AllowCustomPaymentAmounts = true ,
Currency = "BTC" ,
Title = "Payment test title" ,
ExpiryDate = null
} ) ;
await Pay ( firstPaymentId , true ) ;
invoiceData = await client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) ) ;
Assert . Equal ( 0.06 m , invoiceData . Amount ) ;
Assert . Equal ( "BTC" , invoiceData . Currency ) ;
var expectedInvoiceId = invoiceData . Id ;
invoiceData = await client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { AllowPendingInvoiceReuse = true } ) ;
Assert . Equal ( expectedInvoiceId , invoiceData . Id ) ;
var notExpectedInvoiceId = invoiceData . Id ;
invoiceData = await client . PayPaymentRequest ( user . StoreId , paymentTestPaymentRequest . Id , new PayPaymentRequestRequest ( ) { AllowPendingInvoiceReuse = false } ) ;
Assert . NotEqual ( notExpectedInvoiceId , invoiceData . Id ) ;
2020-05-19 19:59:23 +02:00
}
2020-08-25 07:33:00 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceLegacyTests ( )
{
2021-11-22 09:16:08 +01:00
using ( var tester = CreateServerTester ( ) )
2020-08-25 07:33:00 +02:00
{
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
user . RegisterDerivationScheme ( "BTC" ) ;
var client = await user . CreateClient ( Policies . Unrestricted ) ;
var oldBitpay = user . BitPay ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Let's create an invoice with bitpay API" ) ;
2020-08-25 07:33:00 +02:00
var oldInvoice = await oldBitpay . CreateInvoiceAsync ( new Invoice ( )
{
Currency = "BTC" ,
Price = 1000.19392922 m ,
BuyerAddress1 = "blah" ,
Buyer = new Buyer ( )
{
Address2 = "blah2"
} ,
ItemCode = "code" ,
ItemDesc = "desc" ,
OrderId = "orderId" ,
PosData = "posData"
} ) ;
async Task < Client . Models . InvoiceData > AssertInvoiceMetadata ( )
{
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Let's check if we can get invoice in the new format with the metadata" ) ;
2020-08-25 07:33:00 +02:00
var newInvoice = await client . GetInvoice ( user . StoreId , oldInvoice . Id ) ;
Assert . Equal ( "posData" , newInvoice . Metadata [ "posData" ] . Value < string > ( ) ) ;
Assert . Equal ( "code" , newInvoice . Metadata [ "itemCode" ] . Value < string > ( ) ) ;
Assert . Equal ( "desc" , newInvoice . Metadata [ "itemDesc" ] . Value < string > ( ) ) ;
Assert . Equal ( "orderId" , newInvoice . Metadata [ "orderId" ] . Value < string > ( ) ) ;
Assert . False ( newInvoice . Metadata [ "physical" ] . Value < bool > ( ) ) ;
Assert . Null ( newInvoice . Metadata [ "buyerCountry" ] ) ;
Assert . Equal ( 1000.19392922 m , newInvoice . Amount ) ;
Assert . Equal ( "BTC" , newInvoice . Currency ) ;
return newInvoice ;
}
await AssertInvoiceMetadata ( ) ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Let's hack the Bitpay created invoice to be just like before this update. (Invoice V1)" ) ;
2020-08-25 07:33:00 +02:00
var invoiceV1 = "{\r\n \"version\": 1,\r\n \"id\": \"" + oldInvoice . Id + "\",\r\n \"storeId\": \"" + user . StoreId + "\",\r\n \"orderId\": \"orderId\",\r\n \"speedPolicy\": 1,\r\n \"rate\": 1.0,\r\n \"invoiceTime\": 1598329634,\r\n \"expirationTime\": 1598330534,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\",\r\n \"productInformation\": {\r\n \"itemDesc\": \"desc\",\r\n \"itemCode\": \"code\",\r\n \"physical\": false,\r\n \"price\": 1000.19392922,\r\n \"currency\": \"BTC\"\r\n },\r\n \"buyerInformation\": {\r\n \"buyerName\": null,\r\n \"buyerEmail\": null,\r\n \"buyerCountry\": null,\r\n \"buyerZip\": null,\r\n \"buyerState\": null,\r\n \"buyerCity\": null,\r\n \"buyerAddress2\": \"blah2\",\r\n \"buyerAddress1\": \"blah\",\r\n \"buyerPhone\": null\r\n },\r\n \"posData\": \"posData\",\r\n \"internalTags\": [],\r\n \"derivationStrategy\": null,\r\n \"derivationStrategies\": \"{\\\"BTC\\\":{\\\"signingKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\",\\\"source\\\":\\\"NBXplorer\\\",\\\"accountDerivation\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf-[legacy]\\\",\\\"accountOriginal\\\":null,\\\"accountKeySettings\\\":[{\\\"rootFingerprint\\\":\\\"54d5044d\\\",\\\"accountKeyPath\\\":\\\"44'/1'/0'\\\",\\\"accountKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\"}],\\\"label\\\":null}}\",\r\n \"status\": \"new\",\r\n \"exceptionStatus\": \"\",\r\n \"payments\": [],\r\n \"refundable\": false,\r\n \"refundMail\": null,\r\n \"redirectURL\": null,\r\n \"redirectAutomatically\": false,\r\n \"txFee\": 0,\r\n \"fullNotifications\": false,\r\n \"notificationEmail\": null,\r\n \"notificationURL\": null,\r\n \"serverUrl\": \"http://127.0.0.1:8001\",\r\n \"cryptoData\": {\r\n \"BTC\": {\r\n \"rate\": 1.0,\r\n \"paymentMethod\": {\r\n \"networkFeeMode\": 0,\r\n \"networkFeeRate\": 100.0,\r\n \"payjoinEnabled\": false\r\n },\r\n \"feeRate\": 100.0,\r\n \"txFee\": 0,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\"\r\n }\r\n },\r\n \"monitoringExpiration\": 1598416934,\r\n \"historicalAddresses\": null,\r\n \"availableAddressHashes\": null,\r\n \"extendedNotifications\": false,\r\n \"events\": null,\r\n \"paymentTolerance\": 0.0,\r\n \"archived\": false\r\n}" ;
var db = tester . PayTester . GetService < Data . ApplicationDbContextFactory > ( ) ;
using var ctx = db . CreateContext ( ) ;
var dbInvoice = await ctx . Invoices . FindAsync ( oldInvoice . Id ) ;
2023-02-21 07:06:34 +01:00
#pragma warning disable CS0618 // Type or member is obsolete
2020-08-25 07:33:00 +02:00
dbInvoice . Blob = ZipUtils . Zip ( invoiceV1 ) ;
2023-02-21 07:06:34 +01:00
#pragma warning restore CS0618 // Type or member is obsolete
2020-08-25 07:33:00 +02:00
await ctx . SaveChangesAsync ( ) ;
var newInvoice = await AssertInvoiceMetadata ( ) ;
2021-11-22 09:16:08 +01:00
TestLogs . LogInformation ( "Now, let's create an invoice with the new API but with the same metadata as Bitpay" ) ;
2020-08-25 07:33:00 +02:00
newInvoice . Metadata . Add ( "lol" , "lol" ) ;
newInvoice = await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest ( )
{
Metadata = newInvoice . Metadata ,
Amount = 1000.19392922 m ,
Currency = "BTC"
} ) ;
oldInvoice = await oldBitpay . GetInvoiceAsync ( newInvoice . Id ) ;
await AssertInvoiceMetadata ( ) ;
Assert . Equal ( "lol" , newInvoice . Metadata [ "lol" ] . Value < string > ( ) ) ;
}
}
2020-07-24 12:46:46 +02:00
2021-10-15 07:23:34 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanOverpayInvoice ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var client = await user . CreateClient ( ) ;
var invoice = await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest ( ) { Amount = 5000.0 m , Currency = "USD" } ) ;
var methods = await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ;
var method = methods . First ( ) ;
var amount = method . Amount ;
Assert . Equal ( amount , method . Due ) ;
2021-10-15 07:23:34 +02:00
#pragma warning disable CS0618 // Type or member is obsolete
2022-01-14 09:50:29 +01:00
var btc = tester . NetworkProvider . BTC . NBitcoinNetwork ;
2021-10-15 07:23:34 +02:00
#pragma warning restore CS0618 // Type or member is obsolete
2022-01-14 09:50:29 +01:00
await tester . ExplorerNode . SendToAddressAsync ( BitcoinAddress . Create ( method . Destination , btc ) , Money . Coins ( method . Due ) + Money . Coins ( 1.0 m ) ) ;
await TestUtils . EventuallyAsync ( async ( ) = >
{
invoice = await client . GetInvoice ( user . StoreId , invoice . Id ) ;
Assert . True ( invoice . Status = = InvoiceStatus . Processing ) ;
methods = await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ;
method = methods . First ( ) ;
Assert . Equal ( amount , method . Amount ) ;
Assert . Equal ( - 1.0 m , method . Due ) ;
Assert . Equal ( amount + 1.0 m , method . TotalPaid ) ;
} ) ;
2021-10-15 07:23:34 +02:00
}
2020-07-24 12:46:46 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2022-11-28 09:53:08 +01:00
public async Task CanRefundInvoice ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
var client = await user . CreateClient ( ) ;
var invoice = await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest ( ) { Amount = 5000.0 m , Currency = "USD" } ) ;
var methods = await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ;
var method = methods . First ( ) ;
var amount = method . Amount ;
Assert . Equal ( amount , method . Due ) ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
await tester . ExplorerNode . SendToAddressAsync (
BitcoinAddress . Create ( method . Destination , tester . NetworkProvider . BTC . NBitcoinNetwork ) ,
Money . Coins ( method . Due )
) ;
} ) ;
2023-01-06 14:18:07 +01:00
2022-11-28 09:53:08 +01:00
// test validation that the invoice exists
await AssertHttpError ( 404 , async ( ) = >
{
2023-01-06 14:18:07 +01:00
await client . RefundInvoice ( user . StoreId , "lol fake invoice id" , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . RateThen
} ) ;
} ) ;
// test validation error for when invoice is not yet in the state in which it can be refunded
2023-01-06 14:18:07 +01:00
var apiError = await AssertAPIError ( "non-refundable" , ( ) = > client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . RateThen
} ) ) ;
Assert . Equal ( "Cannot refund this invoice" , apiError . Message ) ;
2023-01-06 14:18:07 +01:00
2022-11-28 09:53:08 +01:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
invoice = await client . GetInvoice ( user . StoreId , invoice . Id ) ;
Assert . True ( invoice . Status = = InvoiceStatus . Processing ) ;
} ) ;
// need to set the status to the one in which we can actually refund the invoice
2023-01-06 14:18:07 +01:00
await client . MarkInvoiceStatus ( user . StoreId , invoice . Id , new MarkInvoiceStatusRequest ( )
{
2022-11-28 09:53:08 +01:00
Status = InvoiceStatus . Settled
} ) ;
// test validation for the payment method
2022-11-28 12:58:18 +01:00
var validationError = await AssertValidationError ( new [ ] { "PaymentMethod" } , async ( ) = >
2022-11-28 09:53:08 +01:00
{
2023-01-06 14:18:07 +01:00
await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = "fake payment method" ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . RateThen
} ) ;
} ) ;
2022-11-28 12:58:18 +01:00
Assert . Contains ( "PaymentMethod: Please select one of the payment methods which were available for the original invoice" , validationError . Message ) ;
2022-11-28 09:53:08 +01:00
// test RefundVariant.RateThen
2023-01-06 14:18:07 +01:00
var pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . RateThen
} ) ;
Assert . Equal ( "BTC" , pp . Currency ) ;
2022-11-28 12:58:18 +01:00
Assert . True ( pp . AutoApproveClaims ) ;
2022-11-28 09:53:08 +01:00
Assert . Equal ( 1 , pp . Amount ) ;
Assert . Equal ( pp . Name , $"Refund {invoice.Id}" ) ;
// test RefundVariant.CurrentRate
2023-01-06 14:18:07 +01:00
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . CurrentRate
} ) ;
Assert . Equal ( "BTC" , pp . Currency ) ;
2022-11-28 12:58:18 +01:00
Assert . True ( pp . AutoApproveClaims ) ;
2022-11-28 09:53:08 +01:00
Assert . Equal ( 1 , pp . Amount ) ;
// test RefundVariant.Fiat
2023-01-06 14:18:07 +01:00
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . Fiat ,
Name = "my test name"
} ) ;
Assert . Equal ( "USD" , pp . Currency ) ;
2022-11-28 12:58:18 +01:00
Assert . False ( pp . AutoApproveClaims ) ;
2022-11-28 09:53:08 +01:00
Assert . Equal ( 5000 , pp . Amount ) ;
Assert . Equal ( "my test name" , pp . Name ) ;
// test RefundVariant.Custom
validationError = await AssertValidationError ( new [ ] { "CustomAmount" , "CustomCurrency" } , async ( ) = >
{
2023-01-06 14:18:07 +01:00
await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . Custom ,
} ) ;
} ) ;
Assert . Contains ( "CustomAmount: Amount must be greater than 0" , validationError . Message ) ;
Assert . Contains ( "CustomCurrency: Invalid currency" , validationError . Message ) ;
2023-01-06 14:18:07 +01:00
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . Custom ,
CustomAmount = 69420 ,
CustomCurrency = "JPY"
} ) ;
Assert . Equal ( "JPY" , pp . Currency ) ;
2022-11-28 12:58:18 +01:00
Assert . False ( pp . AutoApproveClaims ) ;
2022-11-28 09:53:08 +01:00
Assert . Equal ( 69420 , pp . Amount ) ;
// should auto-approve if currencies match
2023-01-06 14:18:07 +01:00
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest ( )
{
2022-11-28 12:58:18 +01:00
PaymentMethod = method . PaymentMethod ,
2022-11-28 09:53:08 +01:00
RefundVariant = RefundVariant . Custom ,
CustomAmount = 0.00069420 m ,
CustomCurrency = "BTC"
} ) ;
2022-11-28 12:58:18 +01:00
Assert . True ( pp . AutoApproveClaims ) ;
2023-05-11 10:33:33 +02:00
// test subtract percentage
validationError = await AssertValidationError ( new [ ] { "SubtractPercentage" } , async ( ) = >
{
await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest
{
PaymentMethod = method . PaymentMethod ,
RefundVariant = RefundVariant . RateThen ,
SubtractPercentage = 101
} ) ;
} ) ;
Assert . Contains ( "SubtractPercentage: Percentage must be a numeric value between 0 and 100" , validationError . Message ) ;
// should auto-approve
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest
{
PaymentMethod = method . PaymentMethod ,
RefundVariant = RefundVariant . RateThen ,
SubtractPercentage = 6.15 m
} ) ;
Assert . Equal ( "BTC" , pp . Currency ) ;
Assert . True ( pp . AutoApproveClaims ) ;
Assert . Equal ( 0.9385 m , pp . Amount ) ;
// test RefundVariant.OverpaidAmount
validationError = await AssertValidationError ( new [ ] { "RefundVariant" } , async ( ) = >
{
await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest
{
PaymentMethod = method . PaymentMethod ,
RefundVariant = RefundVariant . OverpaidAmount
} ) ;
} ) ;
Assert . Contains ( "Invoice is not overpaid" , validationError . Message ) ;
// should auto-approve
invoice = await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest { Amount = 5000.0 m , Currency = "USD" } ) ;
methods = await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ;
method = methods . First ( ) ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
await tester . ExplorerNode . SendToAddressAsync (
BitcoinAddress . Create ( method . Destination , tester . NetworkProvider . BTC . NBitcoinNetwork ) ,
Money . Coins ( method . Due * 2 )
) ;
} ) ;
await tester . ExplorerNode . GenerateAsync ( 5 ) ;
await TestUtils . EventuallyAsync ( async ( ) = >
{
invoice = await client . GetInvoice ( user . StoreId , invoice . Id ) ;
Assert . True ( invoice . Status = = InvoiceStatus . Settled ) ;
Assert . True ( invoice . AdditionalStatus = = InvoiceExceptionStatus . PaidOver ) ;
} ) ;
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest
{
PaymentMethod = method . PaymentMethod ,
RefundVariant = RefundVariant . OverpaidAmount
} ) ;
Assert . Equal ( "BTC" , pp . Currency ) ;
Assert . True ( pp . AutoApproveClaims ) ;
Assert . Equal ( method . Due , pp . Amount ) ;
// once more with subtract percentage
pp = await client . RefundInvoice ( user . StoreId , invoice . Id , new RefundInvoiceRequest
{
PaymentMethod = method . PaymentMethod ,
RefundVariant = RefundVariant . OverpaidAmount ,
SubtractPercentage = 21 m
} ) ;
Assert . Equal ( "BTC" , pp . Currency ) ;
Assert . True ( pp . AutoApproveClaims ) ;
Assert . Equal ( 0.79 m , pp . Amount ) ;
2022-11-28 09:53:08 +01:00
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
2020-07-24 12:46:46 +02:00
public async Task InvoiceTests ( )
{
2022-01-15 06:15:03 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
2023-01-13 09:29:41 +01:00
await user . GrantAccessAsync ( true ) ;
2022-01-15 06:15:03 +01:00
await user . MakeAdmin ( ) ;
await user . SetupWebhook ( ) ;
var client = await user . CreateClient ( Policies . Unrestricted ) ;
var viewOnly = await user . CreateClient ( Policies . CanViewInvoices ) ;
2020-07-24 12:46:46 +02:00
2022-01-15 06:15:03 +01:00
//create
2020-07-24 12:46:46 +02:00
2022-01-15 06:15:03 +01:00
//validation errors
await AssertValidationError ( new [ ] { nameof ( CreateInvoiceRequest . Amount ) , $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}" , $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" } , async ( ) = >
{
await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest ( ) { Amount = - 1 , Checkout = new CreateInvoiceRequest . CheckoutOptions ( ) { PaymentTolerance = - 2 , PaymentMethods = new [ ] { "jasaas_sdsad" } } } ) ;
} ) ;
2020-08-25 07:33:00 +02:00
2022-01-15 06:15:03 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( ) { Currency = "helloinvalid" , Amount = 1 } ) ;
} ) ;
await user . RegisterDerivationSchemeAsync ( "BTC" ) ;
2022-03-03 15:15:10 +01:00
string origOrderId = "testOrder" ;
2022-01-15 06:15:03 +01:00
var newInvoice = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( )
2020-07-24 12:46:46 +02:00
{
2022-01-15 06:15:03 +01:00
Currency = "USD" ,
Amount = 1 ,
2022-03-03 15:15:10 +01:00
Metadata = JObject . Parse ( $"{{\" itemCode \ ": \"testitem\", \"orderId\": \"{origOrderId}\"}}" ) ,
2022-01-15 06:15:03 +01:00
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
2021-04-26 05:37:56 +02:00
{
2022-01-15 06:15:03 +01:00
RedirectAutomatically = true ,
2022-11-02 10:21:33 +01:00
RequiresRefundEmail = true ,
2022-01-15 06:15:03 +01:00
} ,
AdditionalSearchTerms = new string [ ] { "Banana" }
2020-07-24 12:46:46 +02:00
} ) ;
2022-01-15 06:15:03 +01:00
Assert . True ( newInvoice . Checkout . RedirectAutomatically ) ;
Assert . True ( newInvoice . Checkout . RequiresRefundEmail ) ;
Assert . Equal ( user . StoreId , newInvoice . StoreId ) ;
//list
var invoices = await viewOnly . GetInvoices ( user . StoreId ) ;
Assert . NotNull ( invoices ) ;
Assert . Single ( invoices ) ;
Assert . Equal ( newInvoice . Id , invoices . First ( ) . Id ) ;
invoices = await viewOnly . GetInvoices ( user . StoreId , textSearch : "Banana" ) ;
Assert . NotNull ( invoices ) ;
Assert . Single ( invoices ) ;
Assert . Equal ( newInvoice . Id , invoices . First ( ) . Id ) ;
invoices = await viewOnly . GetInvoices ( user . StoreId , textSearch : "apples" ) ;
Assert . NotNull ( invoices ) ;
Assert . Empty ( invoices ) ;
//list Filtered
var invoicesFiltered = await viewOnly . GetInvoices ( user . StoreId ,
orderId : null , status : null , startDate : DateTimeOffset . Now . AddHours ( - 1 ) ,
endDate : DateTimeOffset . Now . AddHours ( 1 ) ) ;
Assert . NotNull ( invoicesFiltered ) ;
Assert . Single ( invoicesFiltered ) ;
Assert . Equal ( newInvoice . Id , invoicesFiltered . First ( ) . Id ) ;
Assert . NotNull ( invoicesFiltered ) ;
Assert . Single ( invoicesFiltered ) ;
Assert . Equal ( newInvoice . Id , invoicesFiltered . First ( ) . Id ) ;
//list Yesterday
var invoicesYesterday = await viewOnly . GetInvoices ( user . StoreId ,
orderId : null , status : null , startDate : DateTimeOffset . Now . AddDays ( - 2 ) ,
endDate : DateTimeOffset . Now . AddDays ( - 1 ) ) ;
Assert . NotNull ( invoicesYesterday ) ;
Assert . Empty ( invoicesYesterday ) ;
// Error, startDate and endDate inverted
await AssertValidationError ( new [ ] { "startDate" , "endDate" } ,
( ) = > viewOnly . GetInvoices ( user . StoreId ,
orderId : null , status : null , startDate : DateTimeOffset . Now . AddDays ( - 1 ) ,
endDate : DateTimeOffset . Now . AddDays ( - 2 ) ) ) ;
await AssertValidationError ( new [ ] { "startDate" } ,
( ) = > viewOnly . SendHttpRequest < Client . Models . InvoiceData [ ] > ( $"api/v1/stores/{user.StoreId}/invoices" , new Dictionary < string , object > ( )
2021-02-23 13:18:16 +01:00
{
2022-01-15 06:15:03 +01:00
{ "startDate" , "blah" }
} ) ) ;
2021-07-14 16:32:20 +02:00
2021-12-31 08:59:02 +01:00
2022-01-15 06:15:03 +01:00
//list Existing OrderId
var invoicesExistingOrderId =
await viewOnly . GetInvoices ( user . StoreId , orderId : new [ ] { newInvoice . Metadata [ "orderId" ] . ToString ( ) } ) ;
Assert . NotNull ( invoicesExistingOrderId ) ;
Assert . Single ( invoicesFiltered ) ;
Assert . Equal ( newInvoice . Id , invoicesFiltered . First ( ) . Id ) ;
//list NonExisting OrderId
var invoicesNonExistingOrderId =
await viewOnly . GetInvoices ( user . StoreId , orderId : new [ ] { "NonExistingOrderId" } ) ;
Assert . NotNull ( invoicesNonExistingOrderId ) ;
Assert . Empty ( invoicesNonExistingOrderId ) ;
//list Existing Status
var invoicesExistingStatus =
await viewOnly . GetInvoices ( user . StoreId , status : new [ ] { newInvoice . Status } ) ;
Assert . NotNull ( invoicesExistingStatus ) ;
Assert . Single ( invoicesExistingStatus ) ;
Assert . Equal ( newInvoice . Id , invoicesExistingStatus . First ( ) . Id ) ;
//list NonExisting Status
var invoicesNonExistingStatus = await viewOnly . GetInvoices ( user . StoreId ,
status : new [ ] { BTCPayServer . Client . Models . InvoiceStatus . Invalid } ) ;
Assert . NotNull ( invoicesNonExistingStatus ) ;
Assert . Empty ( invoicesNonExistingStatus ) ;
//get
var invoice = await viewOnly . GetInvoice ( user . StoreId , newInvoice . Id ) ;
2023-02-21 07:06:34 +01:00
Assert . True ( JObject . DeepEquals ( newInvoice . Metadata , invoice . Metadata ) ) ;
2022-01-15 06:15:03 +01:00
var paymentMethods = await viewOnly . GetInvoicePaymentMethods ( user . StoreId , newInvoice . Id ) ;
Assert . Single ( paymentMethods ) ;
var paymentMethod = paymentMethods . First ( ) ;
Assert . Equal ( "BTC" , paymentMethod . PaymentMethod ) ;
Assert . Equal ( "BTC" , paymentMethod . CryptoCode ) ;
Assert . Empty ( paymentMethod . Payments ) ;
//update
newInvoice = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( ) { Currency = "USD" , Amount = 1 } ) ;
Assert . Contains ( InvoiceStatus . Settled , newInvoice . AvailableStatusesForManualMarking ) ;
Assert . Contains ( InvoiceStatus . Invalid , newInvoice . AvailableStatusesForManualMarking ) ;
await client . MarkInvoiceStatus ( user . StoreId , newInvoice . Id , new MarkInvoiceStatusRequest ( )
{
Status = InvoiceStatus . Settled
} ) ;
newInvoice = await client . GetInvoice ( user . StoreId , newInvoice . Id ) ;
Assert . DoesNotContain ( InvoiceStatus . Settled , newInvoice . AvailableStatusesForManualMarking ) ;
Assert . Contains ( InvoiceStatus . Invalid , newInvoice . AvailableStatusesForManualMarking ) ;
newInvoice = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( ) { Currency = "USD" , Amount = 1 } ) ;
await client . MarkInvoiceStatus ( user . StoreId , newInvoice . Id , new MarkInvoiceStatusRequest ( )
{
Status = InvoiceStatus . Invalid
} ) ;
newInvoice = await client . GetInvoice ( user . StoreId , newInvoice . Id ) ;
2022-03-03 15:15:10 +01:00
const string newOrderId = "UPDATED-ORDER-ID" ;
JObject metadataForUpdate = JObject . Parse ( $"{{\" orderId \ ": \"{newOrderId}\", \"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}}" ) ;
2022-01-15 06:15:03 +01:00
Assert . Contains ( InvoiceStatus . Settled , newInvoice . AvailableStatusesForManualMarking ) ;
Assert . DoesNotContain ( InvoiceStatus . Invalid , newInvoice . AvailableStatusesForManualMarking ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . UpdateInvoice ( user . StoreId , invoice . Id ,
2020-12-12 07:15:34 +01:00
new UpdateInvoiceRequest ( )
{
2022-03-03 15:15:10 +01:00
Metadata = metadataForUpdate
2020-12-12 07:15:34 +01:00
} ) ;
2022-01-15 06:15:03 +01:00
} ) ;
invoice = await client . UpdateInvoice ( user . StoreId , invoice . Id ,
new UpdateInvoiceRequest ( )
2020-07-24 12:46:46 +02:00
{
2022-03-03 15:15:10 +01:00
Metadata = metadataForUpdate
2020-07-24 12:46:46 +02:00
} ) ;
2022-03-03 15:15:10 +01:00
Assert . Equal ( newOrderId , invoice . Metadata [ "orderId" ] . Value < string > ( ) ) ;
2022-01-15 06:15:03 +01:00
Assert . Equal ( "updated" , invoice . Metadata [ "itemCode" ] . Value < string > ( ) ) ;
Assert . Equal ( 15 , ( ( JArray ) invoice . Metadata [ "newstuff" ] ) . Values < int > ( ) . Sum ( ) ) ;
2020-08-25 07:33:00 +02:00
2022-01-15 06:15:03 +01:00
//also test the the metadata actually got saved
invoice = await client . GetInvoice ( user . StoreId , invoice . Id ) ;
2022-03-03 15:15:10 +01:00
Assert . Equal ( newOrderId , invoice . Metadata [ "orderId" ] . Value < string > ( ) ) ;
2022-01-15 06:15:03 +01:00
Assert . Equal ( "updated" , invoice . Metadata [ "itemCode" ] . Value < string > ( ) ) ;
Assert . Equal ( 15 , ( ( JArray ) invoice . Metadata [ "newstuff" ] ) . Values < int > ( ) . Sum ( ) ) ;
2020-08-25 07:33:00 +02:00
2022-03-03 15:15:10 +01:00
// test if we can find the updated invoice using the new orderId
var invoicesWithOrderId = await client . GetInvoices ( user . StoreId , new [ ] { newOrderId } ) ;
Assert . NotNull ( invoicesWithOrderId ) ;
Assert . Single ( invoicesWithOrderId ) ;
Assert . Equal ( invoice . Id , invoicesWithOrderId . First ( ) . Id ) ;
2023-01-06 14:18:07 +01:00
2022-03-03 15:15:10 +01:00
// test if the old orderId does not yield any results anymore
var invoicesWithOldOrderId = await client . GetInvoices ( user . StoreId , new [ ] { origOrderId } ) ;
Assert . NotNull ( invoicesWithOldOrderId ) ;
Assert . Empty ( invoicesWithOldOrderId ) ;
2022-01-15 06:15:03 +01:00
//archive
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnly . ArchiveInvoice ( user . StoreId , invoice . Id ) ;
} ) ;
2020-11-13 08:28:15 +01:00
2022-01-15 06:15:03 +01:00
await client . ArchiveInvoice ( user . StoreId , invoice . Id ) ;
Assert . DoesNotContain ( invoice . Id ,
( await client . GetInvoices ( user . StoreId ) ) . Select ( data = > data . Id ) ) ;
2020-12-10 15:34:50 +01:00
2022-01-15 06:15:03 +01:00
//unarchive
await client . UnarchiveInvoice ( user . StoreId , invoice . Id ) ;
Assert . NotNull ( await client . GetInvoice ( user . StoreId , invoice . Id ) ) ;
2020-12-10 15:34:50 +01:00
2022-01-15 06:15:03 +01:00
foreach ( var marked in new [ ] { InvoiceStatus . Settled , InvoiceStatus . Invalid } )
{
var inv = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( ) { Currency = "USD" , Amount = 100 } ) ;
await user . PayInvoice ( inv . Id ) ;
await client . MarkInvoiceStatus ( user . StoreId , inv . Id , new MarkInvoiceStatusRequest ( )
2020-12-10 15:34:50 +01:00
{
2022-01-15 06:15:03 +01:00
Status = marked
} ) ;
var result = await client . GetInvoice ( user . StoreId , inv . Id ) ;
if ( marked = = InvoiceStatus . Settled )
2020-12-10 15:34:50 +01:00
{
2022-01-15 06:15:03 +01:00
Assert . Equal ( InvoiceStatus . Settled , result . Status ) ;
user . AssertHasWebhookEvent < WebhookInvoiceSettledEvent > ( WebhookEventType . InvoiceSettled ,
o = >
{
Assert . Equal ( inv . Id , o . InvoiceId ) ;
Assert . True ( o . ManuallyMarked ) ;
} ) ;
2020-12-10 15:34:50 +01:00
}
2022-01-15 06:15:03 +01:00
if ( marked = = InvoiceStatus . Invalid )
2020-12-10 15:34:50 +01:00
{
2022-01-15 06:15:03 +01:00
Assert . Equal ( InvoiceStatus . Invalid , result . Status ) ;
var evt = user . AssertHasWebhookEvent < WebhookInvoiceInvalidEvent > ( WebhookEventType . InvoiceInvalid ,
o = >
{
Assert . Equal ( inv . Id , o . InvoiceId ) ;
Assert . True ( o . ManuallyMarked ) ;
} ) ;
Assert . NotNull ( await client . GetWebhookDelivery ( evt . StoreId , evt . WebhookId , evt . DeliveryId ) ) ;
2020-12-10 15:34:50 +01:00
}
2022-01-15 06:15:03 +01:00
}
2021-04-07 06:08:42 +02:00
2021-09-01 05:21:44 +02:00
2022-01-15 06:15:03 +01:00
newInvoice = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( )
{
Currency = "USD" ,
Amount = 1 ,
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
2021-09-01 05:21:44 +02:00
{
2022-01-15 06:15:03 +01:00
DefaultLanguage = "it-it " ,
RedirectURL = "http://toto.com/lol"
}
} ) ;
2022-12-08 05:16:18 +01:00
var invoiceObject = await client . GetOnChainWalletObject ( user . StoreId , "BTC" , new OnChainWalletObjectId ( "invoice" , newInvoice . Id ) , false ) ;
2022-12-13 01:09:25 +01:00
Assert . Contains ( invoiceObject . Links . Select ( l = > l . Type ) , t = > t = = "address" ) ;
2022-12-08 05:16:18 +01:00
2022-01-15 06:15:03 +01:00
Assert . EndsWith ( $"/i/{newInvoice.Id}" , newInvoice . CheckoutLink ) ;
var controller = tester . PayTester . GetController < UIInvoiceController > ( user . UserId , user . StoreId ) ;
var model = ( PaymentModel ) ( ( ViewResult ) await controller . Checkout ( newInvoice . Id ) ) . Model ;
Assert . Equal ( "it-IT" , model . DefaultLang ) ;
Assert . Equal ( "http://toto.com/lol" , model . MerchantRefLink ) ;
var langs = tester . PayTester . GetService < LanguageService > ( ) ;
foreach ( var match in new [ ] { "it" , "it-IT" , "it-LOL" } )
{
Assert . Equal ( "it-IT" , langs . FindLanguage ( match ) . Code ) ;
}
foreach ( var match in new [ ] { "pt-BR" } )
{
Assert . Equal ( "pt-BR" , langs . FindLanguage ( match ) . Code ) ;
}
foreach ( var match in new [ ] { "en" , "en-US" } )
{
Assert . Equal ( "en" , langs . FindLanguage ( match ) . Code ) ;
}
foreach ( var match in new [ ] { "pt" , "pt-pt" , "pt-PT" } )
{
Assert . Equal ( "pt-PT" , langs . FindLanguage ( match ) . Code ) ;
}
2021-12-31 08:59:02 +01:00
2022-01-15 06:15:03 +01:00
//payment method activation tests
var store = await client . GetStore ( user . StoreId ) ;
Assert . False ( store . LazyPaymentMethods ) ;
store . LazyPaymentMethods = true ;
store = await client . UpdateStore ( store . Id ,
JObject . FromObject ( store ) . ToObject < UpdateStoreRequest > ( ) ) ;
Assert . True ( store . LazyPaymentMethods ) ;
invoice = await client . CreateInvoice ( user . StoreId , new CreateInvoiceRequest ( ) { Amount = 1 , Currency = "USD" } ) ;
2022-12-08 05:16:18 +01:00
invoiceObject = await client . GetOnChainWalletObject ( user . StoreId , "BTC" , new OnChainWalletObjectId ( "invoice" , invoice . Id ) , false ) ;
2022-12-13 01:09:25 +01:00
Assert . DoesNotContain ( invoiceObject . Links . Select ( l = > l . Type ) , t = > t = = "address" ) ;
2022-12-08 05:16:18 +01:00
2022-01-15 06:15:03 +01:00
paymentMethods = await client . GetInvoicePaymentMethods ( store . Id , invoice . Id ) ;
Assert . Single ( paymentMethods ) ;
Assert . False ( paymentMethods . First ( ) . Activated ) ;
await client . ActivateInvoicePaymentMethod ( user . StoreId , invoice . Id ,
paymentMethods . First ( ) . PaymentMethod ) ;
2022-12-08 05:16:18 +01:00
invoiceObject = await client . GetOnChainWalletObject ( user . StoreId , "BTC" , new OnChainWalletObjectId ( "invoice" , invoice . Id ) , false ) ;
2022-12-13 01:09:25 +01:00
Assert . Contains ( invoiceObject . Links . Select ( l = > l . Type ) , t = > t = = "address" ) ;
2022-12-08 05:16:18 +01:00
2022-01-15 06:15:03 +01:00
paymentMethods = await client . GetInvoicePaymentMethods ( store . Id , invoice . Id ) ;
Assert . Single ( paymentMethods ) ;
Assert . True ( paymentMethods . First ( ) . Activated ) ;
2023-05-04 15:14:15 +02:00
var invoiceWithDefaultPaymentMethodLN = await client . CreateInvoice ( user . StoreId ,
2022-01-15 06:15:03 +01:00
new CreateInvoiceRequest ( )
{
Currency = "USD" ,
Amount = 100 ,
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
2021-10-15 07:23:34 +02:00
{
2022-01-15 06:15:03 +01:00
PaymentMethods = new [ ] { "BTC" , "BTC-LightningNetwork" } ,
DefaultPaymentMethod = "BTC_LightningLike"
}
} ) ;
2023-05-04 15:14:15 +02:00
Assert . Equal ( "BTC_LightningLike" , invoiceWithDefaultPaymentMethodLN . Checkout . DefaultPaymentMethod ) ;
2021-12-31 08:59:02 +01:00
2023-05-04 15:14:15 +02:00
var invoiceWithDefaultPaymentMethodOnChain = await client . CreateInvoice ( user . StoreId ,
2022-01-15 06:15:03 +01:00
new CreateInvoiceRequest ( )
2021-12-31 08:59:02 +01:00
{
2022-01-15 06:15:03 +01:00
Currency = "USD" ,
Amount = 100 ,
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
{
PaymentMethods = new [ ] { "BTC" , "BTC-LightningNetwork" } ,
DefaultPaymentMethod = "BTC"
}
2021-12-31 08:59:02 +01:00
} ) ;
2023-05-04 15:14:15 +02:00
Assert . Equal ( "BTC" , invoiceWithDefaultPaymentMethodOnChain . Checkout . DefaultPaymentMethod ) ;
// reset lazy payment methods
2022-01-15 06:15:03 +01:00
store = await client . GetStore ( user . StoreId ) ;
store . LazyPaymentMethods = false ;
store = await client . UpdateStore ( store . Id ,
JObject . FromObject ( store ) . ToObject < UpdateStoreRequest > ( ) ) ;
2023-05-04 15:14:15 +02:00
Assert . False ( store . LazyPaymentMethods ) ;
// use store default payment method
store = await client . GetStore ( user . StoreId ) ;
Assert . Null ( store . DefaultPaymentMethod ) ;
var storeDefaultPaymentMethod = "BTC-LightningNetwork" ;
store . DefaultPaymentMethod = storeDefaultPaymentMethod ;
store = await client . UpdateStore ( store . Id ,
JObject . FromObject ( store ) . ToObject < UpdateStoreRequest > ( ) ) ;
Assert . Equal ( storeDefaultPaymentMethod , store . DefaultPaymentMethod ) ;
2022-01-15 06:15:03 +01:00
2023-05-04 15:14:15 +02:00
var invoiceWithStoreDefaultPaymentMethod = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( )
{
Currency = "USD" ,
Amount = 100 ,
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
{
PaymentMethods = new [ ] { "BTC" , "BTC-LightningNetwork" , "BTC_LightningLike" }
}
} ) ;
Assert . Equal ( storeDefaultPaymentMethod , invoiceWithStoreDefaultPaymentMethod . Checkout . DefaultPaymentMethod ) ;
2022-01-15 06:15:03 +01:00
//let's see the overdue amount
invoice = await client . CreateInvoice ( user . StoreId ,
new CreateInvoiceRequest ( )
2021-12-31 08:59:02 +01:00
{
2022-01-15 06:15:03 +01:00
Currency = "BTC" ,
Amount = 0.0001 m ,
Checkout = new CreateInvoiceRequest . CheckoutOptions ( )
{
PaymentMethods = new [ ] { "BTC" } ,
DefaultPaymentMethod = "BTC"
}
2021-12-31 08:59:02 +01:00
} ) ;
2022-01-15 06:15:03 +01:00
var pm = Assert . Single ( await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ) ;
Assert . Equal ( 0.0001 m , pm . Due ) ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
await tester . ExplorerNode . SendToAddressAsync (
BitcoinAddress . Create ( pm . Destination , tester . ExplorerClient . Network . NBitcoinNetwork ) ,
new Money ( 0.0002 m , MoneyUnit . BTC ) ) ;
} ) ;
2022-12-08 05:16:18 +01:00
2022-01-15 06:15:03 +01:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
var pm = Assert . Single ( await client . GetInvoicePaymentMethods ( user . StoreId , invoice . Id ) ) ;
Assert . Single ( pm . Payments ) ;
Assert . Equal ( - 0.0001 m , pm . Due ) ;
2022-12-08 05:16:18 +01:00
2022-12-13 01:09:25 +01:00
invoiceObject = await client . GetOnChainWalletObject ( user . StoreId , "BTC" , new OnChainWalletObjectId ( "invoice" , invoice . Id ) , false ) ;
Assert . Contains ( invoiceObject . Links . Select ( l = > l . Type ) , t = > t = = "tx" ) ;
} ) ;
2020-07-24 12:46:46 +02:00
}
2020-11-13 06:01:51 +01:00
2021-01-27 06:39:38 +01:00
[Fact(Timeout = 60 * 20 * 1000)]
2020-08-19 14:46:45 +02:00
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningAPI ( )
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester ( ) ;
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
var user = tester . NewAccount ( ) ;
2022-09-28 02:34:34 +02:00
await user . GrantAccessAsync ( true ) ;
2022-01-14 09:50:29 +01:00
user . RegisterLightningNode ( "BTC" , LightningConnectionType . CLightning , false ) ;
var merchant = tester . NewAccount ( ) ;
2022-09-28 02:34:34 +02:00
await merchant . GrantAccessAsync ( true ) ;
2022-01-14 09:50:29 +01:00
merchant . RegisterLightningNode ( "BTC" , LightningConnectionType . LndREST ) ;
var merchantClient = await merchant . CreateClient ( $"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}" ) ;
var merchantInvoice = await merchantClient . CreateLightningInvoice ( merchant . StoreId , "BTC" , new CreateLightningInvoiceRequest ( LightMoney . Satoshis ( 1_000 ) , "hey" , TimeSpan . FromSeconds ( 60 ) ) ) ;
2023-01-13 09:29:41 +01:00
Assert . NotNull ( merchantInvoice . Id ) ;
Assert . NotNull ( merchantInvoice . PaymentHash ) ;
Assert . Equal ( merchantInvoice . Id , merchantInvoice . PaymentHash ) ;
2023-04-10 04:07:03 +02:00
2023-05-16 02:17:21 +02:00
var client = await user . CreateClient ( Policies . CanUseInternalLightningNode ) ;
2022-01-14 09:50:29 +01:00
// Not permission for the store!
2023-05-16 02:17:21 +02:00
await AssertAPIError ( "missing-permission" , ( ) = > client . GetLightningNodeChannels ( user . StoreId , "BTC" ) ) ;
var invoiceData = await client . CreateLightningInvoice ( "BTC" , new CreateLightningInvoiceRequest ( )
2020-08-19 14:46:45 +02:00
{
2022-01-14 09:50:29 +01:00
Amount = LightMoney . Satoshis ( 1000 ) ,
Description = "lol" ,
Expiry = TimeSpan . FromSeconds ( 400 ) ,
PrivateRouteHints = false
} ) ;
var chargeInvoice = invoiceData ;
2023-05-16 02:17:21 +02:00
Assert . NotNull ( await client . GetLightningInvoice ( "BTC" , invoiceData . Id ) ) ;
2020-08-19 14:46:45 +02:00
2022-10-17 09:51:15 +02:00
// check list for internal node
2023-05-16 02:17:21 +02:00
var invoices = await client . GetLightningInvoices ( "BTC" ) ;
var pendingInvoices = await client . GetLightningInvoices ( "BTC" , true ) ;
2022-10-17 09:51:15 +02:00
Assert . NotEmpty ( invoices ) ;
Assert . Contains ( invoices , i = > i . Id = = invoiceData . Id ) ;
Assert . NotEmpty ( pendingInvoices ) ;
Assert . Contains ( pendingInvoices , i = > i . Id = = invoiceData . Id ) ;
2023-05-16 02:17:21 +02:00
client = await user . CreateClient ( $"{Policies.CanUseLightningNodeInStore}:{user.StoreId}" ) ;
2022-01-14 09:50:29 +01:00
// Not permission for the server
await AssertAPIError ( "missing-permission" , ( ) = > client . GetLightningNodeChannels ( "BTC" ) ) ;
2020-08-19 14:46:45 +02:00
2022-01-14 09:50:29 +01:00
var data = await client . GetLightningNodeChannels ( user . StoreId , "BTC" ) ;
Assert . Equal ( 2 , data . Count ( ) ) ;
BitcoinAddress . Create ( await client . GetLightningDepositAddress ( user . StoreId , "BTC" ) , Network . RegTest ) ;
2020-08-19 14:46:45 +02:00
2022-01-14 09:50:29 +01:00
invoiceData = await client . CreateLightningInvoice ( user . StoreId , "BTC" , new CreateLightningInvoiceRequest ( )
{
Amount = LightMoney . Satoshis ( 1000 ) ,
Description = "lol" ,
Expiry = TimeSpan . FromSeconds ( 400 ) ,
PrivateRouteHints = false
} ) ;
2020-08-19 14:46:45 +02:00
2022-01-14 09:50:29 +01:00
Assert . NotNull ( await client . GetLightningInvoice ( user . StoreId , "BTC" , invoiceData . Id ) ) ;
2020-08-19 14:46:45 +02:00
2022-10-17 09:51:15 +02:00
// check pending list
var merchantPendingInvoices = await merchantClient . GetLightningInvoices ( merchant . StoreId , "BTC" , true ) ;
Assert . NotEmpty ( merchantPendingInvoices ) ;
Assert . Contains ( merchantPendingInvoices , i = > i . Id = = merchantInvoice . Id ) ;
2023-01-06 14:18:07 +01:00
2022-10-27 01:56:24 +02:00
var payResponse = await client . PayLightningInvoice ( user . StoreId , "BTC" , new PayLightningInvoiceRequest
2022-01-14 09:50:29 +01:00
{
BOLT11 = merchantInvoice . BOLT11
} ) ;
2022-10-27 01:56:24 +02:00
Assert . Equal ( merchantInvoice . BOLT11 , payResponse . BOLT11 ) ;
Assert . Equal ( LightningPaymentStatus . Complete , payResponse . Status ) ;
Assert . NotNull ( payResponse . Preimage ) ;
Assert . NotNull ( payResponse . FeeAmount ) ;
Assert . NotNull ( payResponse . TotalAmount ) ;
Assert . NotNull ( payResponse . PaymentHash ) ;
2023-04-10 04:07:03 +02:00
2023-01-13 09:29:41 +01:00
// check the get invoice response
var merchInvoice = await merchantClient . GetLightningInvoice ( merchant . StoreId , "BTC" , merchantInvoice . Id ) ;
Assert . NotNull ( merchInvoice ) ;
Assert . NotNull ( merchInvoice . Preimage ) ;
Assert . NotNull ( merchInvoice . PaymentHash ) ;
Assert . Equal ( payResponse . Preimage , merchInvoice . Preimage ) ;
Assert . Equal ( payResponse . PaymentHash , merchInvoice . PaymentHash ) ;
2023-01-06 14:18:07 +01:00
2022-01-14 09:50:29 +01:00
await Assert . ThrowsAsync < GreenfieldValidationException > ( async ( ) = > await client . PayLightningInvoice ( user . StoreId , "BTC" , new PayLightningInvoiceRequest ( )
{
BOLT11 = "lol"
} ) ) ;
2020-08-19 14:46:45 +02:00
2022-01-14 09:50:29 +01:00
var validationErr = await Assert . ThrowsAsync < GreenfieldValidationException > ( async ( ) = > await client . CreateLightningInvoice ( user . StoreId , "BTC" , new CreateLightningInvoiceRequest ( )
{
Amount = - 1 ,
Expiry = TimeSpan . FromSeconds ( - 1 ) ,
Description = null
} ) ) ;
Assert . Equal ( 2 , validationErr . ValidationErrors . Length ) ;
var invoice = await merchantClient . GetLightningInvoice ( merchant . StoreId , "BTC" , merchantInvoice . Id ) ;
Assert . NotNull ( invoice . PaidAt ) ;
2023-01-13 09:29:41 +01:00
Assert . NotNull ( invoice . PaymentHash ) ;
Assert . NotNull ( invoice . Preimage ) ;
2022-01-14 09:50:29 +01:00
Assert . Equal ( LightMoney . Satoshis ( 1000 ) , invoice . Amount ) ;
2023-01-06 14:18:07 +01:00
2022-10-17 09:51:15 +02:00
// check list for store with paid invoice
var merchantInvoices = await merchantClient . GetLightningInvoices ( merchant . StoreId , "BTC" ) ;
merchantPendingInvoices = await merchantClient . GetLightningInvoices ( merchant . StoreId , "BTC" , true ) ;
Assert . NotEmpty ( merchantInvoices ) ;
Assert . Empty ( merchantPendingInvoices ) ;
// if the test ran too many times the invoice might be on a later page
2023-01-06 14:18:07 +01:00
if ( merchantInvoices . Length < 100 )
Assert . Contains ( merchantInvoices , i = > i . Id = = merchantInvoice . Id ) ;
2022-01-14 09:50:29 +01:00
// Amount received might be bigger because of internal implementation shit from lightning
Assert . True ( LightMoney . Satoshis ( 1000 ) < = invoice . AmountReceived ) ;
2023-04-10 04:07:03 +02:00
2023-01-26 05:22:49 +01:00
// check payments list for store node
var payments = await client . GetLightningPayments ( user . StoreId , "BTC" ) ;
Assert . NotEmpty ( payments ) ;
Assert . Contains ( payments , i = > i . BOLT11 = = merchantInvoice . BOLT11 ) ;
2022-01-14 09:50:29 +01:00
2023-01-26 05:22:49 +01:00
// Node info
2023-05-16 02:17:21 +02:00
var info = await client . GetLightningNodeInfo ( user . StoreId , "BTC" ) ;
2022-01-14 09:50:29 +01:00
Assert . Single ( info . NodeURIs ) ;
Assert . NotEqual ( 0 , info . BlockHeight ) ;
// As admin, can use the internal node through our store.
await user . MakeAdmin ( true ) ;
await user . RegisterInternalLightningNodeAsync ( "BTC" ) ;
await client . GetLightningNodeInfo ( user . StoreId , "BTC" ) ;
// But if not admin anymore, nope
await user . MakeAdmin ( false ) ;
await AssertPermissionError ( "btcpay.server.canuseinternallightningnode" , ( ) = > client . GetLightningNodeInfo ( user . StoreId , "BTC" ) ) ;
// However, even as a guest, you should be able to create an invoice
var guest = tester . NewAccount ( ) ;
2022-10-17 09:51:15 +02:00
await guest . GrantAccessAsync ( ) ;
2022-01-14 09:50:29 +01:00
await user . AddGuest ( guest . UserId ) ;
client = await guest . CreateClient ( Policies . CanCreateLightningInvoiceInStore ) ;
await client . CreateLightningInvoice ( user . StoreId , "BTC" , new CreateLightningInvoiceRequest ( )
{
Amount = LightMoney . Satoshis ( 1000 ) ,
Description = "lol" ,
Expiry = TimeSpan . FromSeconds ( 600 ) ,
} ) ;
client = await guest . CreateClient ( Policies . CanUseLightningNodeInStore ) ;
// Can use lightning node is only granted to store's owner
await AssertPermissionError ( "btcpay.store.canuselightningnode" , ( ) = > client . GetLightningNodeInfo ( user . StoreId , "BTC" ) ) ;
2020-08-19 14:46:45 +02:00
}
2021-07-14 16:32:20 +02:00
2023-01-13 09:29:41 +01:00
[Fact(Timeout = 60 * 20 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanAccessInvoiceLightningPaymentMethodDetails ( )
{
using var tester = CreateServerTester ( ) ;
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( true ) ;
user . RegisterLightningNode ( "BTC" , LightningConnectionType . CLightning ) ;
2023-04-10 04:07:03 +02:00
2023-01-13 09:29:41 +01:00
var client = await user . CreateClient ( Policies . Unrestricted ) ;
2023-05-25 11:40:35 +02:00
var invoices = new Task < Client . Models . InvoiceData > [ 5 ] ;
// Create invoices
for ( int i = 0 ; i < invoices . Length ; i + + )
{
invoices [ i ] = client . CreateInvoice ( user . StoreId ,
2023-01-13 09:29:41 +01:00
new CreateInvoiceRequest
{
Currency = "USD" ,
Amount = 100 ,
Checkout = new CreateInvoiceRequest . CheckoutOptions
{
PaymentMethods = new [ ] { "BTC-LightningNetwork" } ,
DefaultPaymentMethod = "BTC_LightningLike"
}
} ) ;
2023-05-25 11:40:35 +02:00
}
var pm = new InvoicePaymentMethodDataModel [ invoices . Length ] ;
for ( int i = 0 ; i < invoices . Length ; i + + )
{
pm [ i ] = Assert . Single ( await client . GetInvoicePaymentMethods ( user . StoreId , ( await invoices [ i ] ) . Id ) ) ;
2023-06-23 12:12:11 +02:00
Assert . True ( pm [ i ] . AdditionalData . HasValues ) ;
2023-05-25 11:40:35 +02:00
}
// Pay them all at once
Task < PayResponse > [ ] payResponses = new Task < PayResponse > [ invoices . Length ] ;
for ( int i = 0 ; i < invoices . Length ; i + + )
{
payResponses [ i ] = tester . CustomerLightningD . Pay ( pm [ i ] . Destination ) ;
}
2023-04-10 04:07:03 +02:00
2023-05-25 11:40:35 +02:00
// Checking the results
for ( int i = 0 ; i < invoices . Length ; i + + )
{
var resp = await payResponses [ i ] ;
Assert . Equal ( PayResult . Ok , resp . Result ) ;
Assert . NotNull ( resp . Details . PaymentHash ) ;
Assert . NotNull ( resp . Details . Preimage ) ;
2023-04-10 04:07:03 +02:00
2023-05-25 11:40:35 +02:00
pm [ i ] = Assert . Single ( await client . GetInvoicePaymentMethods ( user . StoreId , ( await invoices [ i ] ) . Id ) ) ;
Assert . True ( pm [ i ] . AdditionalData . HasValues ) ;
Assert . Equal ( resp . Details . PaymentHash . ToString ( ) , pm [ i ] . AdditionalData . GetValue ( "paymentHash" ) ) ;
Assert . Equal ( resp . Details . Preimage . ToString ( ) , pm [ i ] . AdditionalData . GetValue ( "preimage" ) ) ;
}
2023-01-13 09:29:41 +01:00
}
2022-12-13 10:56:33 +01:00
[Fact(Timeout = 60 * 20 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningAPI2 ( )
{
using var tester = CreateServerTester ( ) ;
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( true ) ;
var types = new [ ] { LightningConnectionType . LndREST , LightningConnectionType . CLightning } ;
foreach ( var type in types )
{
user . RegisterLightningNode ( "BTC" , type ) ;
var client = await user . CreateClient ( "btcpay.store.cancreatelightninginvoice" ) ;
var amount = LightMoney . Satoshis ( 1000 ) ;
var expiry = TimeSpan . FromSeconds ( 600 ) ;
2023-01-06 14:18:07 +01:00
2022-12-13 10:56:33 +01:00
var invoice = await client . CreateLightningInvoice ( user . StoreId , "BTC" , new CreateLightningInvoiceRequest
{
Amount = amount ,
Expiry = expiry ,
Description = "Hashed description" ,
DescriptionHashOnly = true
} ) ;
var bolt11 = BOLT11PaymentRequest . Parse ( invoice . BOLT11 , Network . RegTest ) ;
Assert . NotNull ( bolt11 . DescriptionHash ) ;
Assert . Null ( bolt11 . ShortDescription ) ;
2023-01-06 14:18:07 +01:00
2022-12-13 10:56:33 +01:00
invoice = await client . CreateLightningInvoice ( user . StoreId , "BTC" , new CreateLightningInvoiceRequest
{
Amount = amount ,
Expiry = expiry ,
Description = "Standard description" ,
} ) ;
bolt11 = BOLT11PaymentRequest . Parse ( invoice . BOLT11 , Network . RegTest ) ;
Assert . Null ( bolt11 . DescriptionHash ) ;
Assert . NotNull ( bolt11 . ShortDescription ) ;
}
}
2020-12-11 15:11:08 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task NotificationAPITests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2020-12-11 15:11:08 +01:00
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( true ) ;
var client = await user . CreateClient ( Policies . CanManageNotificationsForUser ) ;
var viewOnlyClient = await user . CreateClient ( Policies . CanViewNotificationsForUser ) ;
await tester . PayTester . GetService < NotificationSender > ( )
. SendNotification ( new UserScope ( user . UserId ) , new NewVersionNotification ( ) ) ;
Assert . Single ( await viewOnlyClient . GetNotifications ( ) ) ;
Assert . Single ( await viewOnlyClient . GetNotifications ( false ) ) ;
Assert . Empty ( await viewOnlyClient . GetNotifications ( true ) ) ;
Assert . Single ( await client . GetNotifications ( ) ) ;
Assert . Single ( await client . GetNotifications ( false ) ) ;
Assert . Empty ( await client . GetNotifications ( true ) ) ;
var notification = ( await client . GetNotifications ( ) ) . First ( ) ;
notification = await client . GetNotification ( notification . Id ) ;
Assert . False ( notification . Seen ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . UpdateNotification ( notification . Id , true ) ;
} ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . RemoveNotification ( notification . Id ) ;
} ) ;
Assert . True ( ( await client . UpdateNotification ( notification . Id , true ) ) . Seen ) ;
Assert . Single ( await viewOnlyClient . GetNotifications ( true ) ) ;
Assert . Empty ( await viewOnlyClient . GetNotifications ( false ) ) ;
await client . RemoveNotification ( notification . Id ) ;
Assert . Empty ( await viewOnlyClient . GetNotifications ( true ) ) ;
Assert . Empty ( await viewOnlyClient . GetNotifications ( false ) ) ;
}
2021-07-14 16:32:20 +02:00
2020-12-23 06:00:38 +01:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task OnChainPaymentMethodAPITests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2020-12-23 06:00:38 +01:00
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
2021-07-27 16:53:44 +02:00
var user2 = tester . NewAccount ( ) ;
2020-12-23 06:00:38 +01:00
await user . GrantAccessAsync ( true ) ;
2021-07-27 16:53:44 +02:00
await user2 . GrantAccessAsync ( false ) ;
2021-12-31 08:59:02 +01:00
2020-12-23 06:00:38 +01:00
var client = await user . CreateClient ( Policies . CanModifyStoreSettings ) ;
2021-07-27 16:53:44 +02:00
var client2 = await user2 . CreateClient ( Policies . CanModifyStoreSettings ) ;
2020-12-23 06:00:38 +01:00
var viewOnlyClient = await user . CreateClient ( Policies . CanViewStoreSettings ) ;
2020-07-24 12:46:46 +02:00
2021-07-14 16:32:20 +02:00
var store = await client . CreateStore ( new CreateStoreRequest ( ) { Name = "test store" } ) ;
2020-08-25 07:33:00 +02:00
2020-12-23 06:00:38 +01:00
Assert . Empty ( await client . GetStoreOnChainPaymentMethods ( store . Id ) ) ;
await AssertHttpError ( 403 , async ( ) = >
{
2021-09-25 07:04:34 +02:00
await viewOnlyClient . UpdateStoreOnChainPaymentMethod ( store . Id , "BTC" , new UpdateOnChainPaymentMethodRequest ( ) { } ) ;
2021-12-31 08:59:02 +01:00
} ) ;
2020-12-23 06:00:38 +01:00
var xpriv = new Mnemonic ( "all all all all all all all all all all all all" ) . DeriveExtKey ( )
2021-07-27 16:53:44 +02:00
. Derive ( KeyPath . Parse ( "m/84'/1'/0'" ) ) ;
2020-12-23 06:00:38 +01:00
var xpub = xpriv . Neuter ( ) . ToString ( Network . RegTest ) ;
var firstAddress = xpriv . Derive ( KeyPath . Parse ( "0/0" ) ) . Neuter ( ) . GetPublicKey ( ) . GetAddress ( ScriptPubKeyType . Segwit , Network . RegTest ) . ToString ( ) ;
await AssertHttpError ( 404 , async ( ) = >
{
await client . PreviewStoreOnChainPaymentMethodAddresses ( store . Id , "BTC" ) ;
} ) ;
2021-07-14 16:32:20 +02:00
2020-12-23 06:00:38 +01:00
Assert . Equal ( firstAddress , ( await viewOnlyClient . PreviewProposedStoreOnChainPaymentMethodAddresses ( store . Id , "BTC" ,
2021-09-25 07:04:34 +02:00
new UpdateOnChainPaymentMethodRequest ( ) { Enabled = true , DerivationScheme = xpub } ) ) . Addresses . First ( ) . Address ) ;
2021-07-14 16:32:20 +02:00
2022-04-14 06:17:22 +02:00
await AssertValidationError ( new [ ] { "accountKeyPath" } , ( ) = > viewOnlyClient . SendHttpRequest < GreenfieldValidationError [ ] > ( path : $"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview" , method : HttpMethod . Post ,
2022-01-10 14:10:04 +01:00
bodyPayload : JObject . Parse ( "{\"accountKeyPath\": \"0/1\"}" ) ) ) ;
2020-12-23 06:00:38 +01:00
var method = await client . UpdateStoreOnChainPaymentMethod ( store . Id , "BTC" ,
2021-09-25 07:04:34 +02:00
new UpdateOnChainPaymentMethodRequest ( ) { Enabled = true , DerivationScheme = xpub } ) ;
2021-07-14 16:32:20 +02:00
Assert . Equal ( xpub , method . DerivationScheme ) ;
2020-12-28 13:59:01 +01:00
method = await client . UpdateStoreOnChainPaymentMethod ( store . Id , "BTC" ,
2021-09-25 07:04:34 +02:00
new UpdateOnChainPaymentMethodRequest ( ) { Enabled = true , DerivationScheme = xpub , Label = "lol" , AccountKeyPath = RootedKeyPath . Parse ( "01020304/1/2/3" ) } ) ;
2020-12-28 13:59:01 +01:00
2020-12-23 06:00:38 +01:00
method = await client . GetStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
2020-12-28 13:59:01 +01:00
Assert . Equal ( "lol" , method . Label ) ;
Assert . Equal ( RootedKeyPath . Parse ( "01020304/1/2/3" ) , method . AccountKeyPath ) ;
2021-07-14 16:32:20 +02:00
Assert . Equal ( xpub , method . DerivationScheme ) ;
2020-12-23 06:00:38 +01:00
Assert . Equal ( firstAddress , ( await viewOnlyClient . PreviewStoreOnChainPaymentMethodAddresses ( store . Id , "BTC" ) ) . Addresses . First ( ) . Address ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
} ) ;
2021-07-14 16:32:20 +02:00
await client . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
await AssertHttpError ( 404 , async ( ) = >
{
await client . GetStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
} ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . GenerateOnChainWallet ( store . Id , "BTC" , new GenerateOnChainWalletRequest ( ) { } ) ;
} ) ;
2021-12-31 08:59:02 +01:00
await AssertValidationError ( new [ ] { "SavePrivateKeys" , "ImportKeysToRPC" } , async ( ) = >
{
await client2 . GenerateOnChainWallet ( user2 . StoreId , "BTC" , new GenerateOnChainWalletRequest ( )
{
SavePrivateKeys = true ,
ImportKeysToRPC = true
} ) ;
} ) ;
2021-07-27 16:53:44 +02:00
var allMnemonic = new Mnemonic ( "all all all all all all all all all all all all" ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
await client . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
var generateResponse = await client . GenerateOnChainWallet ( store . Id , "BTC" ,
2021-12-31 08:59:02 +01:00
new GenerateOnChainWalletRequest ( ) { ExistingMnemonic = allMnemonic , } ) ;
2021-07-27 16:53:44 +02:00
Assert . Equal ( generateResponse . Mnemonic . ToString ( ) , allMnemonic . ToString ( ) ) ;
Assert . Equal ( generateResponse . DerivationScheme , xpub ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
await AssertAPIError ( "already-configured" , async ( ) = >
{
await client . GenerateOnChainWallet ( store . Id , "BTC" ,
2021-12-31 08:59:02 +01:00
new GenerateOnChainWalletRequest ( ) { ExistingMnemonic = allMnemonic , } ) ;
2021-07-27 16:53:44 +02:00
} ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
await client . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
generateResponse = await client . GenerateOnChainWallet ( store . Id , "BTC" ,
2021-12-31 08:59:02 +01:00
new GenerateOnChainWalletRequest ( ) { } ) ;
2021-07-27 16:53:44 +02:00
Assert . NotEqual ( generateResponse . Mnemonic . ToString ( ) , allMnemonic . ToString ( ) ) ;
Assert . Equal ( generateResponse . Mnemonic . DeriveExtKey ( ) . Derive ( KeyPath . Parse ( "m/84'/1'/0'" ) ) . Neuter ( ) . ToString ( Network . RegTest ) , generateResponse . DerivationScheme ) ;
await client . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
generateResponse = await client . GenerateOnChainWallet ( store . Id , "BTC" ,
2021-12-31 08:59:02 +01:00
new GenerateOnChainWalletRequest ( ) { ExistingMnemonic = allMnemonic , AccountNumber = 1 } ) ;
2021-07-27 16:53:44 +02:00
Assert . Equal ( generateResponse . Mnemonic . ToString ( ) , allMnemonic . ToString ( ) ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
Assert . Equal ( new Mnemonic ( "all all all all all all all all all all all all" ) . DeriveExtKey ( )
. Derive ( KeyPath . Parse ( "m/84'/1'/1'" ) ) . Neuter ( ) . ToString ( Network . RegTest ) , generateResponse . DerivationScheme ) ;
2021-12-31 08:59:02 +01:00
2021-07-27 16:53:44 +02:00
await client . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
generateResponse = await client . GenerateOnChainWallet ( store . Id , "BTC" ,
2021-12-31 08:59:02 +01:00
new GenerateOnChainWalletRequest ( ) { WordList = Wordlist . Japanese , WordCount = WordCount . TwentyFour } ) ;
2021-07-27 16:53:44 +02:00
2021-12-31 08:59:02 +01:00
Assert . Equal ( 24 , generateResponse . Mnemonic . Words . Length ) ;
Assert . Equal ( Wordlist . Japanese , generateResponse . Mnemonic . WordList ) ;
2021-07-27 16:53:44 +02:00
2020-12-23 06:00:38 +01:00
}
2021-07-14 16:32:20 +02:00
2021-02-26 03:58:51 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task LightningNetworkPaymentMethodAPITests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2021-02-26 03:58:51 +01:00
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
2021-03-02 03:11:58 +01:00
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var admin2 = tester . NewAccount ( ) ;
await admin2 . GrantAccessAsync ( true ) ;
var adminClient = await admin . CreateClient ( Policies . CanModifyStoreSettings ) ;
var admin2Client = await admin2 . CreateClient ( Policies . CanModifyStoreSettings , Policies . CanModifyServerSettings ) ;
var viewOnlyClient = await admin . CreateClient ( Policies . CanViewStoreSettings ) ;
var store = await adminClient . GetStore ( admin . StoreId ) ;
Assert . Empty ( await adminClient . GetStoreLightningNetworkPaymentMethods ( store . Id ) ) ;
2021-02-26 03:58:51 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
2021-09-25 07:04:34 +02:00
await viewOnlyClient . UpdateStoreLightningNetworkPaymentMethod ( store . Id , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( ) { } ) ;
2021-02-26 03:58:51 +01:00
} ) ;
await AssertHttpError ( 404 , async ( ) = >
{
2021-03-02 03:11:58 +01:00
await adminClient . GetStoreLightningNetworkPaymentMethod ( store . Id , "BTC" ) ;
2021-02-26 03:58:51 +01:00
} ) ;
2021-03-02 03:11:58 +01:00
await admin . RegisterLightningNodeAsync ( "BTC" , false ) ;
2021-07-14 16:32:20 +02:00
2021-03-02 03:11:58 +01:00
var method = await adminClient . GetStoreLightningNetworkPaymentMethod ( store . Id , "BTC" ) ;
2021-02-26 03:58:51 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
} ) ;
2021-07-14 16:32:20 +02:00
await adminClient . RemoveStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
2021-02-26 03:58:51 +01:00
await AssertHttpError ( 404 , async ( ) = >
{
2021-03-02 03:11:58 +01:00
await adminClient . GetStoreOnChainPaymentMethod ( store . Id , "BTC" ) ;
} ) ;
// Let's verify that the admin client can't change LN to unsafe connection strings without modify server settings rights
foreach ( var forbidden in new string [ ]
{
"type=clightning;server=tcp://127.0.0.1" ,
"type=clightning;server=tcp://test" ,
"type=clightning;server=tcp://test.lan" ,
"type=clightning;server=tcp://test.local" ,
"type=clightning;server=tcp://192.168.1.2" ,
"type=clightning;server=unix://8.8.8.8" ,
"type=clightning;server=unix://[::1]" ,
"type=clightning;server=unix://[0:0:0:0:0:0:0:1]" ,
} )
{
var ex = await AssertValidationError ( new [ ] { "ConnectionString" } , async ( ) = >
{
2021-09-25 07:04:34 +02:00
await adminClient . UpdateStoreLightningNetworkPaymentMethod ( store . Id , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2021-03-02 03:11:58 +01:00
{
ConnectionString = forbidden ,
Enabled = true
} ) ;
} ) ;
Assert . Contains ( "btcpay.server.canmodifyserversettings" , ex . Message ) ;
// However, the other client should work because he has `btcpay.server.canmodifyserversettings`
2021-09-25 07:04:34 +02:00
await admin2Client . UpdateStoreLightningNetworkPaymentMethod ( admin2 . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2021-03-02 03:11:58 +01:00
{
ConnectionString = forbidden ,
Enabled = true
} ) ;
}
// Allowed ip should be ok
2021-09-25 07:04:34 +02:00
await adminClient . UpdateStoreLightningNetworkPaymentMethod ( store . Id , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2021-03-02 03:11:58 +01:00
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8" ,
Enabled = true
} ) ;
// If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid
await admin2 . MakeAdmin ( false ) ;
await AssertValidationError ( new [ ] { "ConnectionString" } , async ( ) = >
{
2021-09-25 07:04:34 +02:00
await admin2Client . UpdateStoreLightningNetworkPaymentMethod ( admin2 . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2021-03-02 03:11:58 +01:00
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1" ,
Enabled = true
} ) ;
2021-02-26 03:58:51 +01:00
} ) ;
2020-12-23 06:00:38 +01:00
2021-07-14 16:32:20 +02:00
var settings = ( await tester . PayTester . GetService < SettingsRepository > ( ) . GetSettingAsync < PoliciesSettings > ( ) ) ? ? new PoliciesSettings ( ) ;
2021-02-26 03:58:51 +01:00
settings . AllowLightningInternalNodeForAll = false ;
await tester . PayTester . GetService < SettingsRepository > ( ) . UpdateSetting ( settings ) ;
var nonAdminUser = tester . NewAccount ( ) ;
await nonAdminUser . GrantAccessAsync ( false ) ;
2021-07-14 16:32:20 +02:00
var nonAdminUserClient = await nonAdminUser . CreateClient ( Policies . CanModifyStoreSettings ) ;
2021-02-26 03:58:51 +01:00
await AssertHttpError ( 404 , async ( ) = >
{
2021-07-14 16:32:20 +02:00
await nonAdminUserClient . GetStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" ) ;
2021-02-26 03:58:51 +01:00
} ) ;
2022-01-11 15:38:05 +01:00
await AssertPermissionError ( "btcpay.server.canuseinternallightningnode" , ( ) = > nonAdminUserClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2023-01-06 14:18:07 +01:00
{
Enabled = method . Enabled ,
ConnectionString = method . ConnectionString
} ) ) ;
2021-07-14 16:32:20 +02:00
2021-02-26 03:58:51 +01:00
settings = await tester . PayTester . GetService < SettingsRepository > ( ) . GetSettingAsync < PoliciesSettings > ( ) ;
settings . AllowLightningInternalNodeForAll = true ;
await tester . PayTester . GetService < SettingsRepository > ( ) . UpdateSetting ( settings ) ;
2020-05-19 19:59:23 +02:00
2021-12-31 08:59:02 +01:00
await nonAdminUserClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
2021-09-25 07:04:34 +02:00
{
Enabled = method . Enabled ,
ConnectionString = method . ConnectionString
} ) ;
2022-01-11 15:38:05 +01:00
// NonAdmin can't set to internal node in AllowLightningInternalNodeForAll is false, but can do other connection string
settings = ( await tester . PayTester . GetService < SettingsRepository > ( ) . GetSettingAsync < PoliciesSettings > ( ) ) ? ? new PoliciesSettings ( ) ;
settings . AllowLightningInternalNodeForAll = false ;
await tester . PayTester . GetService < SettingsRepository > ( ) . UpdateSetting ( settings ) ;
await nonAdminUserClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
{
Enabled = true ,
ConnectionString = "type=clightning;server=tcp://8.8.8.8"
} ) ;
await AssertPermissionError ( "btcpay.server.canuseinternallightningnode" , ( ) = > nonAdminUserClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
{
Enabled = true ,
ConnectionString = "Internal Node"
} ) ) ;
// NonAdmin add admin as owner of the store
await nonAdminUser . AddOwner ( admin . UserId ) ;
// Admin turn on Internal node
adminClient = await admin . CreateClient ( Policies . CanModifyStoreSettings , Policies . CanUseInternalLightningNode ) ;
var data = await adminClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
{
Enabled = method . Enabled ,
ConnectionString = "Internal Node"
} ) ;
// Make sure that the nonAdmin can toggle enabled, ConnectionString unchanged.
await nonAdminUserClient . UpdateStoreLightningNetworkPaymentMethod ( nonAdminUser . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( )
{
Enabled = ! data . Enabled ,
ConnectionString = "Internal Node"
} ) ;
2021-02-26 03:58:51 +01:00
}
2021-03-11 13:34:52 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task WalletAPITests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2021-03-11 13:34:52 +01:00
await tester . StartAsync ( ) ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( true ) ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
var client = await user . CreateClient ( Policies . CanModifyStoreSettings , Policies . CanModifyServerSettings ) ;
var viewOnlyClient = await user . CreateClient ( Policies . CanViewStoreSettings ) ;
var walletId = await user . RegisterDerivationSchemeAsync ( "BTC" , ScriptPubKeyType . Segwit , true ) ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
//view only clients can't do jack shit with this API
await AssertHttpError ( 403 , async ( ) = >
{
2021-07-14 16:32:20 +02:00
await viewOnlyClient . ShowOnChainWalletOverview ( walletId . StoreId , walletId . CryptoCode ) ;
2021-03-11 13:34:52 +01:00
} ) ;
2021-07-14 16:32:20 +02:00
var overview = await client . ShowOnChainWalletOverview ( walletId . StoreId , walletId . CryptoCode ) ;
2021-03-11 13:34:52 +01:00
Assert . Equal ( 0 m , overview . Balance ) ;
2021-07-14 16:32:20 +02:00
var fee = await client . GetOnChainFeeRate ( walletId . StoreId , walletId . CryptoCode ) ;
Assert . NotNull ( fee . FeeRate ) ;
2021-04-07 08:16:17 +02:00
2021-03-11 13:34:52 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
2021-07-14 16:32:20 +02:00
await viewOnlyClient . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode ) ;
2021-03-11 13:34:52 +01:00
} ) ;
2021-07-14 16:32:20 +02:00
var address = await client . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode ) ;
var address2 = await client . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode ) ;
var address3 = await client . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode , true ) ;
2021-03-11 13:34:52 +01:00
Assert . Equal ( address . Address , address2 . Address ) ;
Assert . NotEqual ( address . Address , address3 . Address ) ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . GetOnChainWalletUTXOs ( walletId . StoreId , walletId . CryptoCode ) ;
} ) ;
Assert . Empty ( await client . GetOnChainWalletUTXOs ( walletId . StoreId , walletId . CryptoCode ) ) ;
uint256 txhash = null ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
2021-07-14 16:32:20 +02:00
txhash = await tester . ExplorerNode . SendToAddressAsync (
2021-03-11 13:34:52 +01:00
BitcoinAddress . Create ( address3 . Address , tester . ExplorerClient . Network . NBitcoinNetwork ) ,
new Money ( 0.01 m , MoneyUnit . BTC ) ) ;
} ) ;
await tester . ExplorerNode . GenerateAsync ( 1 ) ;
2021-07-14 16:32:20 +02:00
var address4 = await client . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode , false ) ;
2021-03-11 13:34:52 +01:00
Assert . NotEqual ( address3 . Address , address4 . Address ) ;
await client . UnReserveOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode ) ;
2021-07-14 16:32:20 +02:00
var address5 = await client . GetOnChainWalletReceiveAddress ( walletId . StoreId , walletId . CryptoCode , true ) ;
2021-03-11 13:34:52 +01:00
Assert . Equal ( address5 . Address , address4 . Address ) ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
var utxo = Assert . Single ( await client . GetOnChainWalletUTXOs ( walletId . StoreId , walletId . CryptoCode ) ) ;
2021-07-14 16:32:20 +02:00
Assert . Equal ( 0.01 m , utxo . Amount ) ;
Assert . Equal ( txhash , utxo . Outpoint . Hash ) ;
overview = await client . ShowOnChainWalletOverview ( walletId . StoreId , walletId . CryptoCode ) ;
Assert . Equal ( 0.01 m , overview . Balance ) ;
2021-03-11 13:34:52 +01:00
//the simplest request:
var nodeAddress = await tester . ExplorerNode . GetNewAddressAsync ( ) ;
var createTxRequest = new CreateOnChainTransactionRequest ( )
{
Destinations =
new List < CreateOnChainTransactionRequest . CreateOnChainTransactionRequestDestination > ( )
{
new CreateOnChainTransactionRequest . CreateOnChainTransactionRequestDestination ( )
{
Destination = nodeAddress . ToString ( ) , Amount = 0.001 m
}
} ,
FeeRate = new FeeRate ( 5 m ) //only because regtest may fail but not required
} ;
await AssertHttpError ( 403 , async ( ) = >
{
2021-07-14 16:32:20 +02:00
await viewOnlyClient . CreateOnChainTransaction ( walletId . StoreId , walletId . CryptoCode , createTxRequest ) ;
2021-03-11 13:34:52 +01:00
} ) ;
await Assert . ThrowsAsync < ArgumentOutOfRangeException > ( async ( ) = >
{
await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
await Assert . ThrowsAsync < ArgumentOutOfRangeException > ( async ( ) = >
{
createTxRequest . ProceedWithBroadcast = false ;
await client . CreateOnChainTransaction ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest ) ;
} ) ;
Transaction tx ;
2021-07-14 16:32:20 +02:00
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
2021-03-11 13:34:52 +01:00
Assert . NotNull ( tx ) ;
Assert . Contains ( tx . Outputs , txout = > txout . IsTo ( nodeAddress ) & & txout . Value . ToDecimal ( MoneyUnit . BTC ) = = 0.001 m ) ;
Assert . True ( ( await tester . ExplorerNode . TestMempoolAcceptAsync ( tx ) ) . IsAllowed ) ;
// no change test
createTxRequest . NoChange = true ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
Assert . NotNull ( tx ) ;
2021-07-14 16:32:20 +02:00
Assert . True ( Assert . Single ( tx . Outputs ) . IsTo ( nodeAddress ) ) ;
2021-03-11 13:34:52 +01:00
Assert . True ( ( await tester . ExplorerNode . TestMempoolAcceptAsync ( tx ) ) . IsAllowed ) ;
createTxRequest . NoChange = false ;
2022-06-10 05:58:51 +02:00
// Validation for excluding unconfirmed UTXOs and manually selecting inputs at the same time
await AssertValidationError ( new [ ] { "ExcludeUnconfirmed" } , async ( ) = >
{
2023-01-06 14:18:07 +01:00
createTxRequest . SelectedInputs = new List < OutPoint > ( ) ;
createTxRequest . ExcludeUnconfirmed = true ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
2022-06-10 05:58:51 +02:00
} ) ;
createTxRequest . SelectedInputs = null ;
createTxRequest . ExcludeUnconfirmed = false ;
2021-03-11 13:34:52 +01:00
//coin selection
2021-07-14 16:32:20 +02:00
await AssertValidationError ( new [ ] { nameof ( createTxRequest . SelectedInputs ) } , async ( ) = >
{
createTxRequest . SelectedInputs = new List < OutPoint > ( ) ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
2021-03-11 13:34:52 +01:00
createTxRequest . SelectedInputs = new List < OutPoint > ( )
{
utxo . Outpoint
} ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
createTxRequest . SelectedInputs = null ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
//destination testing
2021-07-14 16:32:20 +02:00
await AssertValidationError ( new [ ] { "Destinations" } , async ( ) = >
{
createTxRequest . Destinations [ 0 ] . Amount = utxo . Amount ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
2021-03-11 13:34:52 +01:00
createTxRequest . Destinations [ 0 ] . SubtractFromAmount = true ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
2021-07-14 16:32:20 +02:00
await AssertValidationError ( new [ ] { "Destinations[0]" } , async ( ) = >
{
createTxRequest . Destinations [ 0 ] . Amount = 0 m ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
2021-03-11 13:34:52 +01:00
//dest can be a bip21
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
//cant use bip with subtractfromamount
createTxRequest . Destinations [ 0 ] . Amount = null ;
createTxRequest . Destinations [ 0 ] . Destination = $"bitcoin:{nodeAddress}?amount=0.001" ;
2021-07-14 16:32:20 +02:00
await AssertValidationError ( new [ ] { "Destinations[0]" } , async ( ) = >
{
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
2021-03-11 13:34:52 +01:00
//if amt specified, it overrides bip21 amount
createTxRequest . Destinations [ 0 ] . Amount = 0.0001 m ;
createTxRequest . Destinations [ 0 ] . SubtractFromAmount = false ;
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
2021-07-14 16:32:20 +02:00
Assert . Contains ( tx . Outputs , txout = > txout . Value . GetValue ( tester . NetworkProvider . GetNetwork < BTCPayNetwork > ( "BTC" ) ) = = 0.0001 m ) ;
2021-03-11 13:34:52 +01:00
//fee rate test
createTxRequest . FeeRate = FeeRate . Zero ;
2021-07-14 16:32:20 +02:00
await AssertValidationError ( new [ ] { "FeeRate" } , async ( ) = >
{
tx = await client . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
2021-03-11 14:18:02 +01:00
2021-07-14 16:32:20 +02:00
createTxRequest . FeeRate = new FeeRate ( 5.0 m ) ;
2021-03-11 14:18:02 +01:00
2021-03-11 13:34:52 +01:00
createTxRequest . Destinations [ 0 ] . Amount = 0.001 m ;
createTxRequest . Destinations [ 0 ] . Destination = nodeAddress . ToString ( ) ;
createTxRequest . Destinations [ 0 ] . SubtractFromAmount = false ;
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . CreateOnChainTransactionButDoNotBroadcast ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest , tester . ExplorerClient . Network . NBitcoinNetwork ) ;
} ) ;
createTxRequest . ProceedWithBroadcast = true ;
2021-07-14 16:32:20 +02:00
var txdata =
2021-03-11 13:34:52 +01:00
await client . CreateOnChainTransaction ( walletId . StoreId , walletId . CryptoCode ,
createTxRequest ) ;
Assert . Equal ( TransactionStatus . Unconfirmed , txdata . Status ) ;
Assert . Null ( txdata . BlockHeight ) ;
Assert . Null ( txdata . BlockHash ) ;
Assert . NotNull ( await tester . ExplorerClient . GetTransactionAsync ( txdata . TransactionHash ) ) ;
2021-07-14 16:32:20 +02:00
2021-03-11 13:34:52 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . GetOnChainWalletTransaction ( walletId . StoreId , walletId . CryptoCode , txdata . TransactionHash . ToString ( ) ) ;
} ) ;
2022-04-14 06:17:22 +02:00
var transaction = await client . GetOnChainWalletTransaction ( walletId . StoreId , walletId . CryptoCode , txdata . TransactionHash . ToString ( ) ) ;
2023-08-23 09:11:25 +02:00
// Check skip doesn't crash
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode , skip : 1 ) ;
2022-04-14 06:17:22 +02:00
Assert . Equal ( transaction . TransactionHash , txdata . TransactionHash ) ;
Assert . Equal ( String . Empty , transaction . Comment ) ;
2022-10-11 10:34:29 +02:00
#pragma warning disable CS0612 // Type or member is obsolete
2022-04-14 06:17:22 +02:00
Assert . Equal ( new Dictionary < string , LabelData > ( ) , transaction . Labels ) ;
// transaction patch tests
var patchedTransaction = await client . PatchOnChainWalletTransaction (
walletId . StoreId , walletId . CryptoCode , txdata . TransactionHash . ToString ( ) ,
2023-01-06 14:18:07 +01:00
new PatchOnChainTransactionRequest ( )
{
2022-04-14 06:17:22 +02:00
Comment = "test comment" ,
Labels = new List < string >
{
"test label"
}
} ) ;
Assert . Equal ( "test comment" , patchedTransaction . Comment ) ;
Assert . Equal (
new Dictionary < string , LabelData > ( )
{
{ "test label" , new LabelData ( ) { Type = "raw" , Text = "test label" } }
} . ToJson ( ) ,
patchedTransaction . Labels . ToJson ( )
) ;
2022-10-11 10:34:29 +02:00
#pragma warning restore CS0612 // Type or member is obsolete
2021-03-11 13:34:52 +01:00
await AssertHttpError ( 403 , async ( ) = >
{
await viewOnlyClient . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode ) ;
} ) ;
Assert . True ( Assert . Single (
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode ,
2021-07-14 16:32:20 +02:00
new [ ] { TransactionStatus . Confirmed } ) ) . TransactionHash = = utxo . Outpoint . Hash ) ;
2021-03-11 13:34:52 +01:00
Assert . Contains (
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode ,
2021-07-14 16:32:20 +02:00
new [ ] { TransactionStatus . Unconfirmed } ) , data = > data . TransactionHash = = txdata . TransactionHash ) ;
2021-03-11 13:34:52 +01:00
Assert . Contains (
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode ) , data = > data . TransactionHash = = txdata . TransactionHash ) ;
2022-04-18 04:20:15 +02:00
Assert . Contains (
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode , null , "test label" ) , data = > data . TransactionHash = = txdata . TransactionHash ) ;
2021-03-11 13:34:52 +01:00
await tester . WaitForEvent < NewBlockEvent > ( async ( ) = >
{
await tester . ExplorerNode . GenerateAsync ( 1 ) ;
} , bevent = > bevent . CryptoCode . Equals ( "BTC" , StringComparison . Ordinal ) ) ;
Assert . Contains (
await client . ShowOnChainWalletTransactions ( walletId . StoreId , walletId . CryptoCode ,
2021-07-14 16:32:20 +02:00
new [ ] { TransactionStatus . Confirmed } ) , data = > data . TransactionHash = = txdata . TransactionHash ) ;
2021-03-11 13:34:52 +01:00
}
2021-12-31 08:59:02 +01:00
2021-07-23 10:05:15 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task StorePaymentMethodsAPITests ( )
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester ( ) ;
2021-07-23 10:05:15 +02:00
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
2021-10-01 12:30:00 +02:00
var viewerOnlyClient = await admin . CreateClient ( Policies . CanViewStoreSettings ) ;
2021-07-23 10:05:15 +02:00
var store = await adminClient . GetStore ( admin . StoreId ) ;
Assert . Empty ( await adminClient . GetStorePaymentMethods ( store . Id ) ) ;
2021-09-25 07:04:34 +02:00
await adminClient . UpdateStoreLightningNetworkPaymentMethod ( admin . StoreId , "BTC" , new UpdateLightningNetworkPaymentMethodRequest ( "Internal Node" , true ) ) ;
2021-07-23 10:05:15 +02:00
void VerifyLightning ( Dictionary < string , GenericPaymentMethodData > dictionary )
{
Assert . True ( dictionary . TryGetValue ( new PaymentMethodId ( "BTC" , PaymentTypes . LightningLike ) . ToStringNormalized ( ) , out var item ) ) ;
2021-10-25 08:18:02 +02:00
var lightningNetworkPaymentMethodBaseData = Assert . IsType < JObject > ( item . Data ) . ToObject < LightningNetworkPaymentMethodBaseData > ( ) ;
2021-07-23 10:05:15 +02:00
Assert . Equal ( "Internal Node" , lightningNetworkPaymentMethodBaseData . ConnectionString ) ;
}
var methods = await adminClient . GetStorePaymentMethods ( store . Id ) ;
2021-07-29 13:29:34 +02:00
Assert . Single ( methods ) ;
2021-07-23 10:05:15 +02:00
VerifyLightning ( methods ) ;
2021-12-31 08:59:02 +01:00
2021-07-23 10:05:15 +02:00
var randK = new Mnemonic ( Wordlist . English , WordCount . Twelve ) . DeriveExtKey ( ) . Neuter ( ) . ToString ( Network . RegTest ) ;
await adminClient . UpdateStoreOnChainPaymentMethod ( admin . StoreId , "BTC" ,
2021-09-25 07:04:34 +02:00
new UpdateOnChainPaymentMethodRequest ( true , randK , "testing" , null ) ) ;
2021-07-23 10:05:15 +02:00
void VerifyOnChain ( Dictionary < string , GenericPaymentMethodData > dictionary )
{
Assert . True ( dictionary . TryGetValue ( new PaymentMethodId ( "BTC" , PaymentTypes . BTCLike ) . ToStringNormalized ( ) , out var item ) ) ;
2021-10-25 08:18:02 +02:00
var paymentMethodBaseData = Assert . IsType < JObject > ( item . Data ) . ToObject < OnChainPaymentMethodBaseData > ( ) ;
2021-07-23 10:05:15 +02:00
Assert . Equal ( randK , paymentMethodBaseData . DerivationScheme ) ;
}
2021-12-31 08:59:02 +01:00
2021-07-23 10:05:15 +02:00
methods = await adminClient . GetStorePaymentMethods ( store . Id ) ;
Assert . Equal ( 2 , methods . Count ) ;
VerifyLightning ( methods ) ;
VerifyOnChain ( methods ) ;
2021-12-31 08:59:02 +01:00
2021-10-01 12:30:00 +02:00
methods = await viewerOnlyClient . GetStorePaymentMethods ( store . Id ) ;
2021-12-31 08:59:02 +01:00
2021-10-01 12:30:00 +02:00
VerifyLightning ( methods ) ;
2021-12-31 08:59:02 +01:00
await adminClient . UpdateStoreLightningNetworkPaymentMethod ( store . Id , "BTC" ,
new UpdateLightningNetworkPaymentMethodRequest (
tester . GetLightningConnectionString ( LightningConnectionType . CLightning , true ) , true ) ) ;
2021-10-01 12:30:00 +02:00
methods = await viewerOnlyClient . GetStorePaymentMethods ( store . Id ) ;
2021-12-31 08:59:02 +01:00
2021-10-01 12:30:00 +02:00
Assert . True ( methods . TryGetValue ( new PaymentMethodId ( "BTC" , PaymentTypes . LightningLike ) . ToStringNormalized ( ) , out var item ) ) ;
2021-12-31 08:59:02 +01:00
var lightningNetworkPaymentMethodBaseData = Assert . IsType < JObject > ( item . Data ) . ToObject < LightningNetworkPaymentMethodBaseData > ( ) ;
2021-10-04 10:44:09 +02:00
Assert . Equal ( "*NEED CanModifyStoreSettings PERMISSION TO VIEW*" , lightningNetworkPaymentMethodBaseData . ConnectionString ) ;
2021-12-31 08:59:02 +01:00
2021-10-01 12:30:00 +02:00
methods = await adminClient . GetStorePaymentMethods ( store . Id ) ;
2021-12-31 08:59:02 +01:00
2021-10-01 12:30:00 +02:00
Assert . True ( methods . TryGetValue ( new PaymentMethodId ( "BTC" , PaymentTypes . LightningLike ) . ToStringNormalized ( ) , out item ) ) ;
2021-12-31 08:59:02 +01:00
lightningNetworkPaymentMethodBaseData = Assert . IsType < JObject > ( item . Data ) . ToObject < LightningNetworkPaymentMethodBaseData > ( ) ;
2021-10-04 10:44:09 +02:00
Assert . NotEqual ( "*NEED CanModifyStoreSettings PERMISSION TO VIEW*" , lightningNetworkPaymentMethodBaseData . ConnectionString ) ;
2021-12-31 08:59:02 +01:00
2021-07-23 10:05:15 +02:00
}
2022-02-10 06:51:10 +01:00
2023-04-10 04:07:03 +02:00
[Fact(Timeout = TestTimeout)]
2023-01-23 10:11:34 +01:00
[Trait("Integration", "Integration")]
public async Task StoreLightningAddressesAPITests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
var store = await adminClient . GetStore ( admin . StoreId ) ;
Assert . Empty ( await adminClient . GetStorePaymentMethods ( store . Id ) ) ;
2023-04-10 04:07:03 +02:00
var store2 = ( await adminClient . CreateStore ( new CreateStoreRequest ( ) { Name = "test2" } ) ) . Id ;
2023-01-23 10:11:34 +01:00
var address1 = Guid . NewGuid ( ) . ToString ( "n" ) . Substring ( 0 , 8 ) ;
var address2 = Guid . NewGuid ( ) . ToString ( "n" ) . Substring ( 0 , 8 ) ;
2023-04-10 04:07:03 +02:00
Assert . Empty ( await adminClient . GetStoreLightningAddresses ( store . Id ) ) ;
Assert . Empty ( await adminClient . GetStoreLightningAddresses ( store2 ) ) ;
2023-01-23 10:11:34 +01:00
await adminClient . AddOrUpdateStoreLightningAddress ( store . Id , address1 , new LightningAddressData ( ) ) ;
2023-04-10 04:07:03 +02:00
2023-01-23 10:11:34 +01:00
await adminClient . AddOrUpdateStoreLightningAddress ( store . Id , address1 , new LightningAddressData ( )
{
Max = 1
} ) ;
await AssertAPIError ( "username-already-used" , async ( ) = >
{
await adminClient . AddOrUpdateStoreLightningAddress ( store2 , address1 , new LightningAddressData ( ) ) ;
} ) ;
2023-04-10 04:07:03 +02:00
Assert . Equal ( 1 , Assert . Single ( await adminClient . GetStoreLightningAddresses ( store . Id ) ) . Max ) ;
Assert . Empty ( await adminClient . GetStoreLightningAddresses ( store2 ) ) ;
2023-01-23 10:11:34 +01:00
await adminClient . AddOrUpdateStoreLightningAddress ( store2 , address2 , new LightningAddressData ( ) ) ;
Assert . Single ( await adminClient . GetStoreLightningAddresses ( store . Id ) ) ;
Assert . Single ( await adminClient . GetStoreLightningAddresses ( store2 ) ) ;
await AssertHttpError ( 404 , async ( ) = >
{
await adminClient . RemoveStoreLightningAddress ( store2 , address1 ) ;
} ) ;
await adminClient . RemoveStoreLightningAddress ( store2 , address2 ) ;
2023-04-10 04:07:03 +02:00
Assert . Empty ( await adminClient . GetStoreLightningAddresses ( store2 ) ) ;
2023-01-23 10:11:34 +01:00
}
2022-02-10 06:51:10 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreUsersAPITest ( )
{
2023-01-06 14:18:07 +01:00
2022-02-10 06:51:10 +01:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( true ) ;
var client = await user . CreateClient ( Policies . CanModifyStoreSettings , Policies . CanModifyServerSettings ) ;
2023-05-26 16:49:32 +02:00
var roles = await client . GetServerRoles ( ) ;
Assert . Equal ( 2 , roles . Count ) ;
#pragma warning disable CS0618
var ownerRole = roles . Single ( data = > data . Role = = StoreRoles . Owner ) ;
var guestRole = roles . Single ( data = > data . Role = = StoreRoles . Guest ) ;
#pragma warning restore CS0618
2022-02-10 06:51:10 +01:00
var users = await client . GetStoreUsers ( user . StoreId ) ;
var storeuser = Assert . Single ( users ) ;
2023-01-06 14:18:07 +01:00
Assert . Equal ( user . UserId , storeuser . UserId ) ;
2023-05-26 16:49:32 +02:00
Assert . Equal ( ownerRole . Id , storeuser . Role ) ;
2023-01-06 14:18:07 +01:00
var user2 = tester . NewAccount ( ) ;
2022-02-10 06:51:10 +01:00
await user2 . GrantAccessAsync ( false ) ;
2023-01-06 14:18:07 +01:00
var user2Client = await user2 . CreateClient ( Policies . CanModifyStoreSettings ) ;
2022-02-10 06:51:10 +01:00
//test no access to api when unrelated to store at all
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . GetStoreUsers ( user . StoreId ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . AddStoreUser ( user . StoreId , new StoreUserData ( ) ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . RemoveStoreUser ( user . StoreId , user . UserId ) ) ;
2023-05-26 16:49:32 +02:00
await client . AddStoreUser ( user . StoreId , new StoreUserData ( ) { Role = guestRole . Id , UserId = user2 . UserId } ) ;
2023-01-06 14:18:07 +01:00
2022-02-10 06:51:10 +01:00
//test no access to api when only a guest
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . GetStoreUsers ( user . StoreId ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . AddStoreUser ( user . StoreId , new StoreUserData ( ) ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await user2Client . RemoveStoreUser ( user . StoreId , user . UserId ) ) ;
await user2Client . GetStore ( user . StoreId ) ;
2023-01-06 14:18:07 +01:00
2022-02-10 06:51:10 +01:00
await client . RemoveStoreUser ( user . StoreId , user2 . UserId ) ;
await AssertHttpError ( 403 , async ( ) = >
await user2Client . GetStore ( user . StoreId ) ) ;
2023-01-06 14:18:07 +01:00
2023-05-26 16:49:32 +02:00
await client . AddStoreUser ( user . StoreId , new StoreUserData ( ) { Role = ownerRole . Id , UserId = user2 . UserId } ) ;
2023-01-06 14:18:07 +01:00
await AssertAPIError ( "duplicate-store-user-role" , async ( ) = >
await client . AddStoreUser ( user . StoreId ,
2023-05-26 16:49:32 +02:00
new StoreUserData ( ) { Role = ownerRole . Id , UserId = user2 . UserId } ) ) ;
2022-02-10 06:51:10 +01:00
await user2Client . RemoveStoreUser ( user . StoreId , user . UserId ) ;
2023-01-06 14:18:07 +01:00
2022-02-10 06:51:10 +01:00
//test no access to api when unrelated to store at all
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await client . GetStoreUsers ( user . StoreId ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await client . AddStoreUser ( user . StoreId , new StoreUserData ( ) ) ) ;
await AssertPermissionError ( Policies . CanModifyStoreSettings , async ( ) = > await client . RemoveStoreUser ( user . StoreId , user . UserId ) ) ;
await AssertAPIError ( "store-user-role-orphaned" , async ( ) = > await user2Client . RemoveStoreUser ( user . StoreId , user2 . UserId ) ) ;
}
2022-03-11 10:17:40 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreEmailTests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
2022-03-11 10:17:50 +01:00
await adminClient . UpdateStoreEmailSettings ( admin . StoreId ,
new EmailSettingsData ( ) ) ;
2022-03-11 10:17:40 +01:00
2022-06-22 05:05:32 +02:00
var data = new EmailSettingsData
2022-03-11 10:17:40 +01:00
{
2022-03-11 10:17:50 +01:00
From = "admin@admin.com" ,
Login = "admin@admin.com" ,
Password = "admin@admin.com" ,
Port = 1234 ,
Server = "admin.com" ,
} ;
await adminClient . UpdateStoreEmailSettings ( admin . StoreId , data ) ;
var s = await adminClient . GetStoreEmailSettings ( admin . StoreId ) ;
Assert . Equal ( JsonConvert . SerializeObject ( s ) , JsonConvert . SerializeObject ( data ) ) ;
await AssertValidationError ( new [ ] { nameof ( EmailSettingsData . From ) } ,
async ( ) = > await adminClient . UpdateStoreEmailSettings ( admin . StoreId ,
2022-06-22 05:05:32 +02:00
new EmailSettingsData { From = "invalid" } ) ) ;
2022-03-11 10:17:50 +01:00
await adminClient . SendEmail ( admin . StoreId ,
2022-06-22 05:05:32 +02:00
new SendEmailRequest { Body = "lol" , Subject = "subj" , Email = "to@example.org" } ) ;
2022-03-11 10:17:40 +01:00
}
2022-04-24 05:19:34 +02:00
2022-04-26 14:27:35 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task DisabledEnabledUserTests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
var newUser = tester . NewAccount ( ) ;
await newUser . GrantAccessAsync ( ) ;
var newUserClient = await newUser . CreateClient ( Policies . Unrestricted ) ;
Assert . False ( ( await newUserClient . GetCurrentUser ( ) ) . Disabled ) ;
2022-12-07 19:01:50 +01:00
Assert . True ( await adminClient . LockUser ( newUser . UserId , true , CancellationToken . None ) ) ;
2022-04-26 14:27:35 +02:00
Assert . True ( ( await adminClient . GetUserByIdOrEmail ( newUser . UserId ) ) . Disabled ) ;
2023-01-06 14:18:07 +01:00
await AssertAPIError ( "unauthenticated" , async ( ) = >
{
await newUserClient . GetCurrentUser ( ) ;
} ) ;
2022-04-26 14:27:35 +02:00
var newUserBasicClient = new BTCPayServerClient ( newUserClient . Host , newUser . RegisterDetails . Email ,
newUser . RegisterDetails . Password ) ;
2023-01-06 14:18:07 +01:00
await AssertAPIError ( "unauthenticated" , async ( ) = >
{
await newUserBasicClient . GetCurrentUser ( ) ;
} ) ;
2022-04-26 14:27:35 +02:00
2022-12-07 19:01:50 +01:00
Assert . True ( await adminClient . LockUser ( newUser . UserId , false , CancellationToken . None ) ) ;
2022-04-26 14:27:35 +02:00
Assert . False ( ( await adminClient . GetUserByIdOrEmail ( newUser . UserId ) ) . Disabled ) ;
await newUserClient . GetCurrentUser ( ) ;
await newUserBasicClient . GetCurrentUser ( ) ;
// Twice for good measure
2022-12-07 19:01:50 +01:00
Assert . True ( await adminClient . LockUser ( newUser . UserId , false , CancellationToken . None ) ) ;
2022-04-26 14:27:35 +02:00
Assert . False ( ( await adminClient . GetUserByIdOrEmail ( newUser . UserId ) ) . Disabled ) ;
await newUserClient . GetCurrentUser ( ) ;
await newUserBasicClient . GetCurrentUser ( ) ;
}
2022-04-24 05:19:34 +02:00
2022-08-17 09:45:51 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNPayoutProcessor ( )
{
LightningPendingPayoutListener . SecondsDelay = 0 ;
using var tester = CreateServerTester ( ) ;
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
tester . ActivateLightning ( ) ;
await tester . StartAsync ( ) ;
await tester . EnsureChannelsSetup ( ) ;
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
var admin = tester . NewAccount ( ) ;
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
await admin . GrantAccessAsync ( true ) ;
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
admin . RegisterLightningNode ( "BTC" , LightningConnectionType . LndREST ) ;
var payoutAmount = LightMoney . Satoshis ( 1000 ) ;
var inv = await tester . MerchantLnd . Client . CreateInvoice ( payoutAmount , "Donation to merchant" , TimeSpan . FromHours ( 1 ) , default ) ;
var resp = await tester . CustomerLightningD . Pay ( inv . BOLT11 ) ;
Assert . Equal ( PayResult . Ok , resp . Result ) ;
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
var customerInvoice = await tester . CustomerLightningD . CreateInvoice ( LightMoney . FromUnit ( 10 , LightMoneyUnit . Satoshi ) ,
Guid . NewGuid ( ) . ToString ( ) , TimeSpan . FromDays ( 40 ) ) ;
var payout = await adminClient . CreatePayout ( admin . StoreId ,
new CreatePayoutThroughStoreRequest ( )
{
2023-01-06 14:18:07 +01:00
Approved = true ,
PaymentMethod = "BTC_LightningNetwork" ,
Destination = customerInvoice . BOLT11
2022-08-17 09:45:51 +02:00
} ) ;
2023-07-24 11:37:18 +02:00
Assert . Equal ( payout . Metadata . ToString ( ) , new JObject ( ) . ToString ( ) ) ; //empty
2022-08-17 09:45:51 +02:00
Assert . Empty ( await adminClient . GetStoreLightningAutomatedPayoutProcessors ( admin . StoreId , "BTC_LightningNetwork" ) ) ;
await adminClient . UpdateStoreLightningAutomatedPayoutProcessors ( admin . StoreId , "BTC_LightningNetwork" ,
2023-04-27 05:48:47 +02:00
new LightningAutomatedPayoutSettings ( ) { IntervalSeconds = TimeSpan . FromSeconds ( 600 ) } ) ;
Assert . Equal ( 600 , Assert . Single ( await adminClient . GetStoreLightningAutomatedPayoutProcessors ( admin . StoreId , "BTC_LightningNetwork" ) ) . IntervalSeconds . TotalSeconds ) ;
2022-08-17 09:45:51 +02:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
var payoutC =
( await adminClient . GetStorePayouts ( admin . StoreId , false ) ) . Single ( data = > data . Id = = payout . Id ) ;
2023-01-06 14:18:07 +01:00
Assert . Equal ( PayoutState . Completed , payoutC . State ) ;
2022-08-17 09:45:51 +02:00
} ) ;
2023-07-24 13:40:26 +02:00
2023-07-24 11:37:18 +02:00
payout = await adminClient . CreatePayout ( admin . StoreId ,
new CreatePayoutThroughStoreRequest ( )
{
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await tester . ExplorerNode . GetNewAddressAsync ( ) ) . ToString ( ) ,
Amount = 0.0001 m ,
Metadata = JObject . FromObject ( new
{
source = "apitest" ,
sourceLink = "https://chocolate.com"
} )
} ) ;
Assert . Equal ( payout . Metadata . ToString ( ) , JObject . FromObject ( new
{
source = "apitest" ,
sourceLink = "https://chocolate.com"
} ) . ToString ( ) ) ;
payout =
( await adminClient . GetStorePayouts ( admin . StoreId , false ) ) . Single ( data = > data . Id = = payout . Id ) ;
Assert . Equal ( payout . Metadata . ToString ( ) , JObject . FromObject ( new
{
source = "apitest" ,
sourceLink = "https://chocolate.com"
} ) . ToString ( ) ) ;
2023-07-24 13:40:26 +02:00
customerInvoice = await tester . CustomerLightningD . CreateInvoice ( LightMoney . FromUnit ( 10 , LightMoneyUnit . Satoshi ) ,
Guid . NewGuid ( ) . ToString ( ) , TimeSpan . FromDays ( 40 ) ) ;
var payout2 = await adminClient . CreatePayout ( admin . StoreId ,
new CreatePayoutThroughStoreRequest ( )
{
Approved = true ,
Amount = new Money ( 100 , MoneyUnit . Satoshi ) . ToDecimal ( MoneyUnit . BTC ) ,
PaymentMethod = "BTC_LightningNetwork" ,
Destination = customerInvoice . BOLT11
} ) ;
Assert . Equal ( payout2 . Amount , new Money ( 100 , MoneyUnit . Satoshi ) . ToDecimal ( MoneyUnit . BTC ) ) ;
2022-08-17 09:45:51 +02:00
}
2022-04-24 05:19:34 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanUsePayoutProcessorsThroughAPI ( )
{
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
var registeredProcessors = await adminClient . GetPayoutProcessors ( ) ;
2023-01-06 14:18:07 +01:00
Assert . Equal ( 2 , registeredProcessors . Count ( ) ) ;
2022-04-24 05:19:34 +02:00
await adminClient . GenerateOnChainWallet ( admin . StoreId , "BTC" , new GenerateOnChainWalletRequest ( )
{
SavePrivateKeys = true
} ) ;
var preApprovedPayoutWithoutPullPayment = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 0.0001 m ,
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
var notApprovedPayoutWithoutPullPayment = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 0.00001 m ,
Approved = false ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
var pullPayment = await adminClient . CreatePullPayment ( admin . StoreId , new CreatePullPaymentRequest ( )
{
Amount = 100 ,
Currency = "USD" ,
Name = "pull payment" ,
2023-01-06 14:18:07 +01:00
PaymentMethods = new [ ] { "BTC" }
2022-04-24 05:19:34 +02:00
} ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
var notapprovedPayoutWithPullPayment = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
PullPaymentId = pullPayment . Id ,
Amount = 10 ,
Approved = false ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
await adminClient . ApprovePayout ( admin . StoreId , notapprovedPayoutWithPullPayment . Id ,
new ApprovePayoutRequest ( ) { } ) ;
var payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
Assert . Equal ( 3 , payouts . Length ) ;
Assert . Single ( payouts , data = > data . State = = PayoutState . AwaitingApproval ) ;
await adminClient . ApprovePayout ( admin . StoreId , notApprovedPayoutWithoutPullPayment . Id ,
new ApprovePayoutRequest ( ) { } ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
Assert . Equal ( 3 , payouts . Length ) ;
Assert . Empty ( payouts . Where ( data = > data . State = = PayoutState . AwaitingApproval ) ) ;
Assert . Empty ( payouts . Where ( data = > data . PaymentMethodAmount is null ) ) ;
2023-01-06 14:18:07 +01:00
Assert . Empty ( await adminClient . ShowOnChainWalletTransactions ( admin . StoreId , "BTC" ) ) ;
Assert . Empty ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) ;
Assert . Empty ( await adminClient . GetPayoutProcessors ( admin . StoreId ) ) ;
await adminClient . UpdateStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ,
2023-04-27 03:59:19 +02:00
new OnChainAutomatedPayoutSettings ( ) { IntervalSeconds = TimeSpan . FromSeconds ( 3600 ) } ) ;
Assert . Equal ( 3600 , Assert . Single ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) . IntervalSeconds . TotalSeconds ) ;
2023-01-06 14:18:07 +01:00
var tpGen = Assert . Single ( await adminClient . GetPayoutProcessors ( admin . StoreId ) ) ;
Assert . Equal ( "BTC" , Assert . Single ( tpGen . PaymentMethods ) ) ;
//still too poor to process any payouts
Assert . Empty ( await adminClient . ShowOnChainWalletTransactions ( admin . StoreId , "BTC" ) ) ;
await adminClient . RemovePayoutProcessor ( admin . StoreId , tpGen . Name , tpGen . PaymentMethods . First ( ) ) ;
Assert . Empty ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) ;
Assert . Empty ( await adminClient . GetPayoutProcessors ( admin . StoreId ) ) ;
2023-04-27 03:59:19 +02:00
// Send just enough money to cover the smallest of the payouts.
2023-04-27 05:48:47 +02:00
var fee = ( await tester . PayTester . GetService < IFeeProviderFactory > ( ) . CreateFeeProvider ( tester . DefaultNetwork ) . GetFeeRateAsync ( 100 ) ) . GetFee ( 150 ) ;
2023-01-06 14:18:07 +01:00
await tester . ExplorerNode . SendToAddressAsync ( BitcoinAddress . Create ( ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
2023-04-27 03:59:19 +02:00
tester . ExplorerClient . Network . NBitcoinNetwork ) , Money . Coins ( 0.00001 m ) + fee ) ;
2023-01-06 14:18:07 +01:00
await tester . ExplorerNode . GenerateAsync ( 1 ) ;
await TestUtils . EventuallyAsync ( async ( ) = >
{
Assert . Single ( await adminClient . ShowOnChainWalletTransactions ( admin . StoreId , "BTC" ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Equal ( 3 , payouts . Length ) ;
} ) ;
await adminClient . UpdateStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ,
2023-04-27 03:59:19 +02:00
new OnChainAutomatedPayoutSettings ( ) { IntervalSeconds = TimeSpan . FromSeconds ( 600 ) , FeeBlockTarget = 1000 } ) ;
Assert . Equal ( 600 , Assert . Single ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) . IntervalSeconds . TotalSeconds ) ;
2023-01-06 14:18:07 +01:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
Assert . Equal ( 2 , ( await adminClient . ShowOnChainWalletTransactions ( admin . StoreId , "BTC" ) ) . Count ( ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Single ( payouts . Where ( data = > data . State = = PayoutState . InProgress ) ) ;
} ) ;
2023-07-20 15:05:14 +02:00
uint256 txid = null ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
txid = await tester . ExplorerNode . SendToAddressAsync ( BitcoinAddress . Create ( ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
tester . ExplorerClient . Network . NBitcoinNetwork ) , Money . Coins ( 0.01 m ) + fee ) ;
} , correctEvent : ev = > ev . NewTransactionEvent . TransactionData . TransactionHash = = txid ) ;
2023-04-27 03:59:19 +02:00
await tester . PayTester . GetService < PayoutProcessorService > ( ) . Restart ( new PayoutProcessorService . PayoutProcessorQuery ( admin . StoreId , "BTC" ) ) ;
2023-01-06 14:18:07 +01:00
await TestUtils . EventuallyAsync ( async ( ) = >
{
Assert . Equal ( 4 , ( await adminClient . ShowOnChainWalletTransactions ( admin . StoreId , "BTC" ) ) . Count ( ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Empty ( payouts . Where ( data = > data . State ! = PayoutState . InProgress ) ) ;
} ) ;
2023-07-20 15:05:14 +02:00
// settings that were added later
var settings =
Assert . Single ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) ;
Assert . False ( settings . ProcessNewPayoutsInstantly ) ;
Assert . Equal ( 0 m , settings . Threshold ) ;
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
settings . IntervalSeconds = TimeSpan . FromDays ( 1 ) ;
settings . ProcessNewPayoutsInstantly = true ;
await tester . WaitForEvent < NewOnChainTransactionEvent > ( async ( ) = >
{
txid = await tester . ExplorerNode . SendToAddressAsync ( BitcoinAddress . Create ( ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
tester . ExplorerClient . Network . NBitcoinNetwork ) , Money . Coins ( 1 m ) + fee ) ;
} , correctEvent : ev = > ev . NewTransactionEvent . TransactionData . TransactionHash = = txid ) ;
await adminClient . UpdateStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" , settings ) ;
settings =
Assert . Single ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) ;
Assert . True ( settings . ProcessNewPayoutsInstantly ) ;
var pluginHookService = tester . PayTester . GetService < IPluginHookService > ( ) ;
var beforeHookTcs = new TaskCompletionSource ( ) ;
var afterHookTcs = new TaskCompletionSource ( ) ;
pluginHookService . ActionInvoked + = ( sender , tuple ) = >
{
switch ( tuple . hook )
{
case "before-automated-payout-processing" :
beforeHookTcs . TrySetResult ( ) ;
break ;
case "after-automated-payout-processing" :
afterHookTcs . TrySetResult ( ) ;
break ;
}
} ;
var payoutThatShouldBeProcessedStraightAway = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
PullPaymentId = pullPayment . Id ,
Amount = 0.5 m ,
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
await beforeHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
await afterHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Single ( payouts . Where ( data = > data . State = = PayoutState . InProgress & & data . Id = = payoutThatShouldBeProcessedStraightAway . Id ) ) ;
beforeHookTcs = new TaskCompletionSource ( ) ;
afterHookTcs = new TaskCompletionSource ( ) ;
//let's test the threshold limiter
settings . Threshold = 0.5 m ;
await adminClient . UpdateStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" , settings ) ;
//quick test: when updating processor, it processes instantly
await beforeHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
await afterHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
settings =
Assert . Single ( await adminClient . GetStoreOnChainAutomatedPayoutProcessors ( admin . StoreId , "BTC" ) ) ;
Assert . Equal ( 0.5 m , settings . Threshold ) ;
//create a payout that should not be processed straight away due to threshold
beforeHookTcs = new TaskCompletionSource ( ) ;
afterHookTcs = new TaskCompletionSource ( ) ;
var payoutThatShouldNotBeProcessedStraightAway = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 0.1 m ,
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
await beforeHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
await afterHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Single ( payouts . Where ( data = > data . State = = PayoutState . AwaitingPayment & & data . Id = = payoutThatShouldNotBeProcessedStraightAway . Id ) ) ;
beforeHookTcs = new TaskCompletionSource ( ) ;
afterHookTcs = new TaskCompletionSource ( ) ;
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 0.3 m ,
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
await beforeHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
await afterHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Equal ( 2 , payouts . Count ( data = > data . State = = PayoutState . AwaitingPayment & &
( data . Id = = payoutThatShouldNotBeProcessedStraightAway . Id | | data . Id = = payoutThatShouldNotBeProcessedStraightAway2 . Id ) ) ) ;
beforeHookTcs = new TaskCompletionSource ( ) ;
afterHookTcs = new TaskCompletionSource ( ) ;
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient . CreatePayout ( admin . StoreId , new CreatePayoutThroughStoreRequest ( )
{
Amount = 0.3 m ,
Approved = true ,
PaymentMethod = "BTC" ,
Destination = ( await adminClient . GetOnChainWalletReceiveAddress ( admin . StoreId , "BTC" , true ) ) . Address ,
} ) ;
await beforeHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
await afterHookTcs . Task . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
payouts = await adminClient . GetStorePayouts ( admin . StoreId ) ;
Assert . Empty ( payouts . Where ( data = > data . State ! = PayoutState . InProgress ) ) ;
2022-04-24 05:19:34 +02:00
}
2022-11-16 04:11:17 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanUseWalletObjectsAPI ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
2022-11-18 16:04:46 +01:00
2022-11-16 04:11:17 +01:00
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
2022-11-18 16:04:46 +01:00
2022-11-16 04:11:17 +01:00
var client = await admin . CreateClient ( Policies . Unrestricted ) ;
2022-11-18 16:04:46 +01:00
Assert . Empty ( await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ) ;
var test = new OnChainWalletObjectId ( "test" , "test" ) ;
Assert . NotNull ( await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( test . Type , test . Id ) ) ) ;
2022-11-16 04:11:17 +01:00
2022-11-18 16:04:46 +01:00
Assert . Single ( await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ) ;
Assert . NotNull ( await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , test ) ) ;
Assert . Null ( await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , new OnChainWalletObjectId ( "test-wrong" , "test" ) ) ) ;
Assert . Null ( await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , new OnChainWalletObjectId ( "test" , "test-wrong" ) ) ) ;
await client . RemoveOnChainWalletObject ( admin . StoreId , "BTC" , new OnChainWalletObjectId ( "test" , "test" ) ) ;
Assert . Empty ( await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ) ;
var test1 = new OnChainWalletObjectId ( "test" , "test1" ) ;
var test2 = new OnChainWalletObjectId ( "test" , "test2" ) ;
await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( test . Type , test . Id ) ) ;
// Those links don't exists
await AssertAPIError ( "wallet-object-not-found" , ( ) = > client . AddOrUpdateOnChainWalletLink ( admin . StoreId , "BTC" , test , new AddOnChainWalletObjectLinkRequest ( test1 . Type , test1 . Id ) ) ) ;
await AssertAPIError ( "wallet-object-not-found" , ( ) = > client . AddOrUpdateOnChainWalletLink ( admin . StoreId , "BTC" , test , new AddOnChainWalletObjectLinkRequest ( test2 . Type , test2 . Id ) ) ) ;
Assert . Single ( await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ) ;
await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( test1 . Type , test1 . Id ) ) ;
await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( test2 . Type , test2 . Id ) ) ;
await client . AddOrUpdateOnChainWalletLink ( admin . StoreId , "BTC" , test , new AddOnChainWalletObjectLinkRequest ( test1 . Type , test1 . Id ) ) ;
await client . AddOrUpdateOnChainWalletLink ( admin . StoreId , "BTC" , test , new AddOnChainWalletObjectLinkRequest ( test2 . Type , test2 . Id ) ) ;
var objs = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ;
2022-11-16 04:11:17 +01:00
Assert . Equal ( 3 , objs . Length ) ;
var middleObj = objs . Single ( data = > data . Id = = "test" & & data . Type = = "test" ) ;
2022-11-18 16:04:46 +01:00
Assert . Equal ( 2 , middleObj . Links . Length ) ;
Assert . Contains ( "test1" , middleObj . Links . Select ( l = > l . Id ) ) ;
Assert . Contains ( "test2" , middleObj . Links . Select ( l = > l . Id ) ) ;
var test1Obj = objs . Single ( data = > data . Id = = "test1" & & data . Type = = "test" ) ;
var test2Obj = objs . Single ( data = > data . Id = = "test2" & & data . Type = = "test" ) ;
Assert . Single ( test1Obj . Links . Select ( l = > l . Id ) , l = > l = = "test" ) ;
Assert . Single ( test2Obj . Links . Select ( l = > l . Id ) , l = > l = = "test" ) ;
2022-11-16 04:11:17 +01:00
await client . RemoveOnChainWalletLinks ( admin . StoreId , "BTC" ,
2022-11-18 16:04:46 +01:00
test1 ,
test ) ;
var testObj = await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , test ) ;
Assert . Single ( testObj . Links . Select ( l = > l . Id ) , l = > l = = "test2" ) ;
Assert . Single ( testObj . Links ) ;
test1Obj = await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , test1 ) ;
Assert . Empty ( test1Obj . Links ) ;
await client . AddOrUpdateOnChainWalletLink ( admin . StoreId , "BTC" ,
test1 ,
new AddOnChainWalletObjectLinkRequest ( test . Type , test . Id ) { Data = new JObject ( ) { [ "testData" ] = "lol" } } ) ;
// Add some data to test1
await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( ) { Type = test1 . Type , Id = test1 . Id , Data = new JObject ( ) { [ "testData" ] = "test1" } } ) ;
// Create a new type
await client . AddOrUpdateOnChainWalletObject ( admin . StoreId , "BTC" , new AddOnChainWalletObjectRequest ( ) { Type = "newtype" , Id = test1 . Id } ) ;
testObj = await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , test ) ;
Assert . Single ( testObj . Links . Where ( l = > l . Id = = "test1" & & l . LinkData [ "testData" ] ? . Value < string > ( ) = = "lol" ) ) ;
Assert . Single ( testObj . Links . Where ( l = > l . Id = = "test1" & & l . ObjectData [ "testData" ] ? . Value < string > ( ) = = "test1" ) ) ;
testObj = await client . GetOnChainWalletObject ( admin . StoreId , "BTC" , test , false ) ;
Assert . Single ( testObj . Links . Where ( l = > l . Id = = "test1" & & l . LinkData [ "testData" ] ? . Value < string > ( ) = = "lol" ) ) ;
Assert . Single ( testObj . Links . Where ( l = > l . Id = = "test1" & & l . ObjectData is null ) ) ;
async Task TestWalletRepository ( bool useInefficient )
{
// We should have 4 nodes, two `test` type and one `newtype`
// Only the node `test` `test` is connected to `test1`
var wid = new WalletId ( admin . StoreId , "BTC" ) ;
var repo = tester . PayTester . GetService < WalletRepository > ( ) ;
2022-12-01 01:54:55 +01:00
var allObjects = await repo . GetWalletObjects ( new ( wid ) { UseInefficientPath = useInefficient } ) ;
2022-11-19 15:39:41 +01:00
var allObjectsNoWallet = await repo . GetWalletObjects ( ( new ( ) { UseInefficientPath = useInefficient } ) ) ;
var allObjectsNoWalletAndType = await repo . GetWalletObjects ( ( new ( ) { Type = "test" , UseInefficientPath = useInefficient } ) ) ;
var allTests = await repo . GetWalletObjects ( ( new ( wid , "test" ) { UseInefficientPath = useInefficient } ) ) ;
var twoTests2 = await repo . GetWalletObjects ( ( new ( wid , "test" , new [ ] { "test1" , "test2" , "test-unk" } ) { UseInefficientPath = useInefficient } ) ) ;
var oneTest = await repo . GetWalletObjects ( ( new ( wid , "test" , new [ ] { "test" } ) { UseInefficientPath = useInefficient } ) ) ;
var oneTestWithoutData = await repo . GetWalletObjects ( ( new ( wid , "test" , new [ ] { "test" } ) { UseInefficientPath = useInefficient , IncludeNeighbours = false } ) ) ;
var idsTypes = await repo . GetWalletObjects ( ( new ( wid ) { TypesIds = new [ ] { new ObjectTypeId ( "test" , "test1" ) , new ObjectTypeId ( "test" , "test2" ) } , UseInefficientPath = useInefficient } ) ) ;
2022-11-18 16:04:46 +01:00
Assert . Equal ( 4 , allObjects . Count ) ;
2022-11-19 15:39:41 +01:00
// We are reusing a db in this test, as such we may have other wallets here.
Assert . True ( allObjectsNoWallet . Count > = 4 ) ;
2022-11-20 06:19:48 +01:00
Assert . True ( allObjectsNoWalletAndType . Count > = 3 ) ;
2022-11-18 16:04:46 +01:00
Assert . Equal ( 3 , allTests . Count ) ;
Assert . Equal ( 2 , twoTests2 . Count ) ;
Assert . Single ( oneTest ) ;
Assert . NotNull ( oneTest . First ( ) . Value . GetNeighbours ( ) . Select ( n = > n . Data ) . FirstOrDefault ( ) ) ;
Assert . Single ( oneTestWithoutData ) ;
Assert . Null ( oneTestWithoutData . First ( ) . Value . GetNeighbours ( ) . Select ( n = > n . Data ) . FirstOrDefault ( ) ) ;
2022-11-19 15:39:41 +01:00
Assert . Equal ( 2 , idsTypes . Count ) ;
2022-11-18 16:04:46 +01:00
}
await TestWalletRepository ( false ) ;
await TestWalletRepository ( true ) ;
{
var allObjects = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" ) ;
var allTests = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" , new GetWalletObjectsRequest ( ) { Type = "test" } ) ;
var twoTests2 = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" , new GetWalletObjectsRequest ( ) { Type = "test" , Ids = new [ ] { "test1" , "test2" , "test-unk" } } ) ;
2023-01-06 14:18:07 +01:00
var oneTest = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" , new GetWalletObjectsRequest ( ) { Type = "test" , Ids = new [ ] { "test" } } ) ;
2022-11-18 16:04:46 +01:00
var oneTestWithoutData = await client . GetOnChainWalletObjects ( admin . StoreId , "BTC" , new GetWalletObjectsRequest ( ) { Type = "test" , Ids = new [ ] { "test" } , IncludeNeighbourData = false } ) ;
Assert . Equal ( 4 , allObjects . Length ) ;
Assert . Equal ( 3 , allTests . Length ) ;
Assert . Equal ( 2 , twoTests2 . Length ) ;
Assert . Single ( oneTest ) ;
Assert . NotNull ( oneTest . First ( ) . Links . Select ( n = > n . ObjectData ) . FirstOrDefault ( ) ) ;
Assert . Single ( oneTestWithoutData ) ;
Assert . Null ( oneTestWithoutData . First ( ) . Links . Select ( n = > n . ObjectData ) . FirstOrDefault ( ) ) ;
}
2022-11-16 04:11:17 +01:00
}
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CustodiansControllerTests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
2022-05-24 06:18:16 +02:00
await tester . PayTester . EnableExperimental ( ) ;
2022-05-18 07:59:56 +02:00
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodians ( ) ) ;
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
var clientBasic = await user . CreateClient ( ) ;
var custodians = await clientBasic . GetCustodians ( ) ;
Assert . NotNull ( custodians ) ;
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
Assert . NotEmpty ( custodians ) ;
2022-05-18 07:59:56 +02:00
}
2023-01-06 14:18:07 +01:00
2022-10-12 15:19:33 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoreRateConfigTests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetRateSources ( ) ) ;
2022-05-18 07:59:56 +02:00
2022-10-12 15:19:33 +02:00
var user = tester . NewAccount ( ) ;
await user . GrantAccessAsync ( ) ;
var clientBasic = await user . CreateClient ( ) ;
Assert . NotEmpty ( await clientBasic . GetRateSources ( ) ) ;
var config = await clientBasic . GetStoreRateConfiguration ( user . StoreId ) ;
Assert . NotNull ( config ) ;
Assert . False ( config . IsCustomScript ) ;
Assert . Equal ( "X_X = coingecko(X_X);" , config . EffectiveScript ) ;
Assert . Equal ( "coingecko" , config . PreferredSource ) ;
Assert . Equal ( 0.9 m ,
Assert . Single ( await clientBasic . PreviewUpdateStoreRateConfiguration ( user . StoreId ,
2023-01-06 14:18:07 +01:00
new StoreRateConfiguration ( ) { IsCustomScript = true , EffectiveScript = "BTC_XYZ = 1;" , Spread = 10 m , } ,
new [ ] { "BTC_XYZ" } ) ) . Rate ) ;
2022-10-12 15:19:33 +02:00
Assert . True ( ( await clientBasic . UpdateStoreRateConfiguration ( user . StoreId ,
2023-01-06 14:18:07 +01:00
new StoreRateConfiguration ( ) { IsCustomScript = true , EffectiveScript = "BTC_XYZ = 1" , Spread = 10 m , } ) )
2022-10-12 15:19:33 +02:00
. IsCustomScript ) ;
2023-01-06 14:18:07 +01:00
2023-01-31 06:42:25 +01:00
Assert . Equal ( 0.9 m ,
Assert . Single ( await clientBasic . GetStoreRates ( user . StoreId , new [ ] { "BTC_XYZ" } ) ) . Rate ) ;
2023-04-10 04:07:03 +02:00
2022-10-12 15:19:33 +02:00
config = await clientBasic . GetStoreRateConfiguration ( user . StoreId ) ;
Assert . NotNull ( config ) ;
Assert . NotNull ( config . EffectiveScript ) ;
Assert . Equal ( "BTC_XYZ = 1;" , config . EffectiveScript ) ;
Assert . Equal ( 10 m , config . Spread ) ;
Assert . Null ( config . PreferredSource ) ;
Assert . NotNull ( ( await clientBasic . GetStoreRateConfiguration ( user . StoreId ) ) . EffectiveScript ) ;
Assert . NotNull ( ( await clientBasic . UpdateStoreRateConfiguration ( user . StoreId ,
2023-01-06 14:18:07 +01:00
new StoreRateConfiguration ( ) { IsCustomScript = false , PreferredSource = "coingecko" } ) )
2022-10-12 15:19:33 +02:00
. PreferredSource ) ;
config = await clientBasic . GetStoreRateConfiguration ( user . StoreId ) ;
Assert . Equal ( "X_X = coingecko(X_X);" , config . EffectiveScript ) ;
await AssertValidationError ( new [ ] { "EffectiveScript" , "PreferredSource" } , ( ) = >
clientBasic . UpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = false , EffectiveScript = "BTC_XYZ = 1;" } ) ) ;
await AssertValidationError ( new [ ] { "EffectiveScript" } , ( ) = >
clientBasic . UpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = true , EffectiveScript = "BTC_XYZ rg8w*# 1;" } ) ) ;
await AssertValidationError ( new [ ] { "PreferredSource" } , ( ) = >
clientBasic . UpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = true , EffectiveScript = "" , PreferredSource = "coingecko" } ) ) ;
await AssertValidationError ( new [ ] { "PreferredSource" , "Spread" } , ( ) = >
clientBasic . UpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = false , PreferredSource = "coingeckoOOO" , Spread = - 1 m } ) ) ;
await AssertValidationError ( new [ ] { "currencyPair" } , ( ) = >
clientBasic . PreviewUpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = false , PreferredSource = "coingecko" } , new [ ] { "BTC_USD_USD_BTC" } ) ) ;
await AssertValidationError ( new [ ] { "PreferredSource" , "currencyPair" } , ( ) = >
clientBasic . PreviewUpdateStoreRateConfiguration ( user . StoreId , new StoreRateConfiguration ( ) { IsCustomScript = false , PreferredSource = "coingeckoOOO" } , new [ ] { "BTC_USD_USD_BTC" } ) ) ;
}
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CustodianAccountControllerTests ( )
{
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
2022-05-24 06:18:16 +02:00
await tester . PayTester . EnableExperimental ( ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
var authedButLackingPermissionsClient = await admin . CreateClient ( Policies . CanViewStoreSettings ) ;
var viewerOnlyClient = await admin . CreateClient ( Policies . CanViewCustodianAccounts ) ;
var managerClient = await admin . CreateClient ( Policies . CanManageCustodianAccounts ) ;
var store = await adminClient . GetStore ( admin . StoreId ) ;
var storeId = store . Id ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Load a custodian, we use the first one we find.
2023-01-06 14:18:07 +01:00
var custodians = tester . PayTester . GetService < IEnumerable < ICustodian > > ( ) ;
2022-05-18 07:59:56 +02:00
var custodian = custodians . First ( ) ;
// List custodian accounts
// Unauth
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccounts ( storeId ) ) ;
2023-01-06 14:18:07 +01:00
// Auth, but wrong permission
await AssertHttpError ( 403 , async ( ) = > await authedButLackingPermissionsClient . GetCustodianAccounts ( storeId ) ) ;
// Auth, correct permission, empty result
var emptyCustodianAccounts = await viewerOnlyClient . GetCustodianAccounts ( storeId ) ;
Assert . Empty ( emptyCustodianAccounts ) ;
// Create custodian account
2022-05-18 07:59:56 +02:00
JObject config = JObject . Parse ( @ "{
' WithdrawToAddressNamePerPaymentMethod ' : {
' BTC - OnChain ' : ' My Ledger Nano '
} ,
' ApiKey ' : ' APIKEY ' ,
' PrivateKey ' : ' UFJJVkFURUtFWQ = = '
} ");
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
var createCustodianAccountRequest = new CreateCustodianAccountRequest ( ) ;
createCustodianAccountRequest . Config = config ;
createCustodianAccountRequest . CustodianCode = custodian . Code ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Unauthorized
await AssertHttpError ( 401 , async ( ) = > await unauthClient . CreateCustodianAccount ( storeId , createCustodianAccountRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Auth, but wrong permission
await AssertHttpError ( 403 , async ( ) = > await viewerOnlyClient . CreateCustodianAccount ( storeId , createCustodianAccountRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Auth, correct permission
var custodianAccountData = await managerClient . CreateCustodianAccount ( storeId , createCustodianAccountRequest ) ;
Assert . NotNull ( custodianAccountData ) ;
Assert . NotNull ( custodianAccountData . Id ) ;
var accountId = custodianAccountData . Id ;
Assert . Equal ( custodian . Code , custodianAccountData . CustodianCode ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// We did not provide a name, so the custodian's name should've been picked as a fallback
Assert . Equal ( custodian . Name , custodianAccountData . Name ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
Assert . Equal ( storeId , custodianAccountData . StoreId ) ;
Assert . True ( JToken . DeepEquals ( config , custodianAccountData . Config ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// List all Custodian Accounts, now that we have 1 result
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Admin can see all
var adminCustodianAccounts = await adminClient . GetCustodianAccounts ( storeId ) ;
Assert . Single ( adminCustodianAccounts ) ;
var adminCustodianAccount = adminCustodianAccounts . First ( ) ;
Assert . Equal ( adminCustodianAccount . CustodianCode , custodian . Code ) ;
// Manager can see all, including config
var managerCustodianAccounts = await managerClient . GetCustodianAccounts ( storeId ) ;
Assert . Single ( managerCustodianAccounts ) ;
Assert . Equal ( managerCustodianAccounts . First ( ) . CustodianCode , custodian . Code ) ;
Assert . NotNull ( managerCustodianAccounts . First ( ) . Config ) ;
Assert . True ( JToken . DeepEquals ( config , managerCustodianAccounts . First ( ) . Config ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Viewer can see all, but no config
var viewerCustodianAccounts = await viewerOnlyClient . GetCustodianAccounts ( storeId ) ;
Assert . Single ( viewerCustodianAccounts ) ;
Assert . Equal ( viewerCustodianAccounts . First ( ) . CustodianCode , custodian . Code ) ;
Assert . Null ( viewerCustodianAccounts . First ( ) . Config ) ;
2023-01-06 14:18:07 +01:00
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
// Wrong store ID
await AssertApiError ( 403 , "missing-permission" , async ( ) = > await adminClient . GetCustodianAccounts ( "WRONG-STORE-ID" ) ) ;
2022-05-18 07:59:56 +02:00
2023-01-06 14:18:07 +01:00
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
// Try to fetch 1 custodian account
2022-05-18 07:59:56 +02:00
// Admin
var singleAdminCustodianAccount = await adminClient . GetCustodianAccount ( storeId , accountId ) ;
Assert . NotNull ( singleAdminCustodianAccount ) ;
Assert . Equal ( singleAdminCustodianAccount . CustodianCode , custodian . Code ) ;
2023-01-06 14:18:07 +01:00
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
// Wrong store ID
2023-01-06 14:18:07 +01:00
await AssertApiError ( 403 , "missing-permission" , async ( ) = > await adminClient . GetCustodianAccount ( "WRONG-STORE-ID" , accountId ) ) ;
Custodian Account UI: CRUD (#3923)
* WIP New APIs for dealing with custodians/exchanges
* Simplified things
* More API refinements + index.html file for quick viewing
* Finishing touches on spec
* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning
* Moved draft API docs to "/docs-draft"
* WIP baby steps
* Added DB migration for CustodianAccountData
* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian
* WIP + early Kraken API client
* Moved service registration to proper location
* Working create + list custodian accounts + permissions + WIP Kraken client
* Kraken API Balances call is working
* Added asset balances to response
* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.
* Call to get the details of 1 specific custodian account
* Added permissions to swagger
* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours
* Removed unused file
* WIP + Moved files to better locations
* Updated docs
* Working API endpoint to get info on a trade (same response as creating a new trade)
* Working API endpoints for Deposit + Trade + untested Withdraw
* Delete custodian account
* Trading works, better error handling, cleanup
* Working withdrawals + New endpoint for getting bid/ask prices
* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,
* Better error handling when withdrawing to a wrong destination
* WithdrawalAddressName in config is now a string per currency (dictionary)
* Added TODOs
* Only show the custodian account "config" to users who are allowed
* Added the new permissions to the API Keys UI
* Renamed KrakenClient to KrakenExchange
* WIP Kraken Config Form
* Removed files for UI again, will make separate PR later
* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere
* Updated withdrawal info docs
* First unit test
* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes
* Mock custodian and more exceptions
* Many more tests + cleanup, moved files to better locations
* More tests
* WIP more tests
* Greenfield API tests complete
* Added missing "Name" column
* Cleanup, TODOs and beginning of Kraken Tests
* Added Kraken tests using public endpoints + handling of "SATS" currency
* Added 1st mocked Kraken API call: GetAssetBalancesAsync
* Added assert for bad config
* Mocked more Kraken API responses + added CreationDate to withdrawal response
* pr review club changes
* Make Kraken Custodian a plugin
* Re-added User-Agent header as it is required
* Fixed bug in market trade on Kraken using a percentage as qty
* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.
* Merged the draft swagger into the main swagger since it didn't work anymore
* Fixed API permissions test
* Removed 2 TODOs
* Fixed unit test
* After a utxo rescan, the cached balance should be invalidated
* Fixed Kraken plugin build issues
* Added Kraken plugin to build
* WIP UI + config form
* Create custodian account almost working - only need to add in the config form
* Working form, but lacks refinement
* Viewing balances + Editing custodian account works, but cannot change the withdrawal destination config because that is an object using a name with [] in it
* cleanup
* Minor cleanup, comments
* Working: Delete custodian account
* Moved the MockCustodian used in tests to a new plugin + linked it to the tests
* WIP viewing custodian account balances
* Split the Mock custodian into a Mock + Fake, various UI improvements and minor fixes
* Minor UI fixes
* Removed broken link
* Removed links to anchors as they cannot pass the tests since they use JavaScript
* Removed non-existing link. Even though it was commented out, the test still broke?
* Added TODOs
* Now throwing BadConfigException if API key is invalid
* UI improvements
* Commented out unfinished API endpoints. Can be finished later.
* Show fiat value for fiat assets
* Removed Kraken plugin so I can make a PR
Removed more Kraken files
* Add experimental route on UICustodianAccountsControllre
* Removed unneeded code
* Cleanup code
* Processed Nicolas' feedback
Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-07-07 15:42:50 +02:00
// Wrong account ID
2023-01-06 14:18:07 +01:00
await AssertApiError ( 404 , "custodian-account-not-found" , async ( ) = > await adminClient . GetCustodianAccount ( storeId , "WRONG-ACCOUNT-ID" ) ) ;
2022-05-18 07:59:56 +02:00
// Manager can see, including config
var singleManagerCustodianAccount = await managerClient . GetCustodianAccount ( storeId , accountId ) ;
Assert . NotNull ( singleManagerCustodianAccount ) ;
Assert . Equal ( singleManagerCustodianAccount . CustodianCode , custodian . Code ) ;
Assert . NotNull ( singleManagerCustodianAccount . Config ) ;
Assert . True ( JToken . DeepEquals ( config , singleManagerCustodianAccount . Config ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Viewer can see, but no config
var singleViewerCustodianAccount = await viewerOnlyClient . GetCustodianAccount ( storeId , accountId ) ;
Assert . NotNull ( singleViewerCustodianAccount ) ;
Assert . Equal ( singleViewerCustodianAccount . CustodianCode , custodian . Code ) ;
Assert . Null ( singleViewerCustodianAccount . Config ) ;
// Test updating the custodian account we created
var updateCustodianAccountRequest = createCustodianAccountRequest ;
updateCustodianAccountRequest . Name = "My Custodian" ;
updateCustodianAccountRequest . Config [ "ApiKey" ] = "ZZZ" ;
// Unauth
await AssertHttpError ( 401 , async ( ) = > await unauthClient . UpdateCustodianAccount ( storeId , accountId , updateCustodianAccountRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Auth, but wrong permission
await AssertHttpError ( 403 , async ( ) = > await viewerOnlyClient . UpdateCustodianAccount ( storeId , accountId , updateCustodianAccountRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Correct auth: update permissions
var updatedCustodianAccountData = await managerClient . UpdateCustodianAccount ( storeId , accountId , createCustodianAccountRequest ) ;
Assert . NotNull ( updatedCustodianAccountData ) ;
Assert . Equal ( custodian . Code , updatedCustodianAccountData . CustodianCode ) ;
Assert . Equal ( updateCustodianAccountRequest . Name , updatedCustodianAccountData . Name ) ;
Assert . Equal ( storeId , custodianAccountData . StoreId ) ;
Assert . True ( JToken . DeepEquals ( updateCustodianAccountRequest . Config , createCustodianAccountRequest . Config ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Admin
updateCustodianAccountRequest . Name = "Admin Account" ;
updateCustodianAccountRequest . Config [ "ApiKey" ] = "AAA" ;
updatedCustodianAccountData = await adminClient . UpdateCustodianAccount ( storeId , accountId , createCustodianAccountRequest ) ;
Assert . NotNull ( updatedCustodianAccountData ) ;
Assert . Equal ( custodian . Code , updatedCustodianAccountData . CustodianCode ) ;
Assert . Equal ( updateCustodianAccountRequest . Name , updatedCustodianAccountData . Name ) ;
Assert . Equal ( storeId , custodianAccountData . StoreId ) ;
Assert . True ( JToken . DeepEquals ( updateCustodianAccountRequest . Config , createCustodianAccountRequest . Config ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Admin tries to update a non-existing custodian account
await AssertHttpError ( 404 , async ( ) = > await adminClient . UpdateCustodianAccount ( storeId , "WRONG-ACCOUNT-ID" , updateCustodianAccountRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Get asset balances, but we cannot because of misconfiguration (we did enter dummy data)
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccounts ( storeId , true ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// // Auth, viewer permission => Error 500 because of BadConfigException (dummy data)
// await AssertHttpError(500, async () => await viewerOnlyClient.GetCustodianAccounts(storeId, true));
//
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Delete custodian account
// Unauth
await AssertHttpError ( 401 , async ( ) = > await unauthClient . DeleteCustodianAccount ( storeId , accountId ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Auth, but wrong permission
await AssertHttpError ( 403 , async ( ) = > await viewerOnlyClient . DeleteCustodianAccount ( storeId , accountId ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Auth, correct permission
await managerClient . DeleteCustodianAccount ( storeId , accountId ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Check if the Custodian Account was actually deleted
await AssertHttpError ( 404 , async ( ) = > await managerClient . GetCustodianAccount ( storeId , accountId ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// TODO what if we try to create a custodian account for a custodian code that does not exist?
// TODO what if we try so set config data that is not valid? In phase 2 we will validate the config and only allow you to save a config that makes sense!
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CustodianTests ( )
{
using var tester = CreateServerTester ( ) ;
await tester . StartAsync ( ) ;
2022-05-24 06:18:16 +02:00
await tester . PayTester . EnableExperimental ( ) ;
2022-05-18 07:59:56 +02:00
var admin = tester . NewAccount ( ) ;
await admin . GrantAccessAsync ( true ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
var unauthClient = new BTCPayServerClient ( tester . PayTester . ServerUri ) ;
2023-01-06 14:18:07 +01:00
var authClientNoPermissions = await admin . CreateClient ( Policies . CanViewInvoices ) ;
2022-05-18 07:59:56 +02:00
var adminClient = await admin . CreateClient ( Policies . Unrestricted ) ;
var managerClient = await admin . CreateClient ( Policies . CanManageCustodianAccounts ) ;
var withdrawalClient = await admin . CreateClient ( Policies . CanWithdrawFromCustodianAccounts ) ;
var depositClient = await admin . CreateClient ( Policies . CanDepositToCustodianAccounts ) ;
var tradeClient = await admin . CreateClient ( Policies . CanTradeCustodianAccount ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
var store = await adminClient . GetStore ( admin . StoreId ) ;
var storeId = store . Id ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Load a custodian, we use the first one we find.
2023-01-06 14:18:07 +01:00
var custodians = tester . PayTester . GetService < IEnumerable < ICustodian > > ( ) ;
2022-05-18 07:59:56 +02:00
var mockCustodian = custodians . First ( c = > c . Code = = "mock" ) ;
2023-01-06 14:18:07 +01:00
// Create custodian account
var createCustodianAccountRequest = new CreateCustodianAccountRequest ( ) ;
2022-05-18 07:59:56 +02:00
createCustodianAccountRequest . CustodianCode = mockCustodian . Code ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
var custodianAccountData = await managerClient . CreateCustodianAccount ( storeId , createCustodianAccountRequest ) ;
Assert . NotNull ( custodianAccountData ) ;
Assert . Equal ( mockCustodian . Code , custodianAccountData . CustodianCode ) ;
Assert . NotNull ( custodianAccountData . Id ) ;
var accountId = custodianAccountData . Id ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Get Asset Balances
2023-01-06 14:18:07 +01:00
var custodianAccountWithBalances = await adminClient . GetCustodianAccount ( storeId , accountId , true ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( custodianAccountWithBalances ) ;
Assert . NotNull ( custodianAccountWithBalances . AssetBalances ) ;
Assert . Equal ( 4 , custodianAccountWithBalances . AssetBalances . Count ) ;
Assert . True ( custodianAccountWithBalances . AssetBalances . Keys . Contains ( "BTC" ) ) ;
Assert . True ( custodianAccountWithBalances . AssetBalances . Keys . Contains ( "LTC" ) ) ;
Assert . True ( custodianAccountWithBalances . AssetBalances . Keys . Contains ( "EUR" ) ) ;
Assert . True ( custodianAccountWithBalances . AssetBalances . Keys . Contains ( "USD" ) ) ;
Assert . Equal ( MockCustodian . BalanceBTC , custodianAccountWithBalances . AssetBalances [ "BTC" ] ) ;
Assert . Equal ( MockCustodian . BalanceLTC , custodianAccountWithBalances . AssetBalances [ "LTC" ] ) ;
Assert . Equal ( MockCustodian . BalanceEUR , custodianAccountWithBalances . AssetBalances [ "EUR" ] ) ;
Assert . Equal ( MockCustodian . BalanceUSD , custodianAccountWithBalances . AssetBalances [ "USD" ] ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Get Asset Balances omitted if we choose so
2023-01-06 14:18:07 +01:00
var custodianAccountWithoutBalances = await adminClient . GetCustodianAccount ( storeId , accountId , false ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( custodianAccountWithoutBalances ) ;
Assert . Null ( custodianAccountWithoutBalances . AssetBalances ) ;
// Test: GetDepositAddress, unauth
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccountDepositAddress ( storeId , accountId , MockCustodian . DepositPaymentMethod ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetDepositAddress, auth, but wrong permission
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . GetCustodianAccountDepositAddress ( storeId , accountId , MockCustodian . DepositPaymentMethod ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetDepositAddress, wrong payment method
2023-04-10 04:07:03 +02:00
await AssertApiError ( 400 , "unsupported-payment-method" , async ( ) = > await depositClient . GetCustodianAccountDepositAddress ( storeId , accountId , "WRONG-PaymentMethod" ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetDepositAddress, wrong store ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await depositClient . GetCustodianAccountDepositAddress ( "WRONG-STORE" , accountId , MockCustodian . DepositPaymentMethod ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetDepositAddress, wrong account ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await depositClient . GetCustodianAccountDepositAddress ( storeId , "WRONG-ACCOUNT-ID" , MockCustodian . DepositPaymentMethod ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetDepositAddress, correct payment method
2023-03-20 02:45:32 +01:00
var depositAddress = await depositClient . GetCustodianAccountDepositAddress ( storeId , accountId , MockCustodian . DepositPaymentMethod ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( depositAddress ) ;
Assert . Equal ( MockCustodian . DepositAddress , depositAddress . Address ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Trade, unauth
2023-04-10 04:07:03 +02:00
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian . TradeFromAsset , ToAsset = MockCustodian . TradeToAsset , Qty = new TradeQuantity ( MockCustodian . TradeQtyBought , TradeQuantity . ValueType . Exact ) } ;
2022-08-04 04:38:49 +02:00
await AssertHttpError ( 401 , async ( ) = > await unauthClient . MarketTradeCustodianAccountAsset ( storeId , accountId , tradeRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Trade, auth, but wrong permission
2022-08-04 04:38:49 +02:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . MarketTradeCustodianAccountAsset ( storeId , accountId , tradeRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Trade, correct permission, correct assets, correct amount
2022-08-04 04:38:49 +02:00
var newTradeResult = await tradeClient . MarketTradeCustodianAccountAsset ( storeId , accountId , tradeRequest ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( newTradeResult ) ;
Assert . Equal ( accountId , newTradeResult . AccountId ) ;
Assert . Equal ( mockCustodian . Code , newTradeResult . CustodianCode ) ;
Assert . Equal ( MockCustodian . TradeId , newTradeResult . TradeId ) ;
Assert . Equal ( tradeRequest . FromAsset , newTradeResult . FromAsset ) ;
Assert . Equal ( tradeRequest . ToAsset , newTradeResult . ToAsset ) ;
2023-01-06 14:18:07 +01:00
Assert . NotNull ( newTradeResult . LedgerEntries ) ;
Assert . Equal ( 3 , newTradeResult . LedgerEntries . Count ) ;
Assert . Equal ( MockCustodian . TradeQtyBought , newTradeResult . LedgerEntries [ 0 ] . Qty ) ;
Assert . Equal ( tradeRequest . ToAsset , newTradeResult . LedgerEntries [ 0 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Trade , newTradeResult . LedgerEntries [ 0 ] . Type ) ;
Assert . Equal ( - 1 * MockCustodian . TradeQtyBought * MockCustodian . BtcPriceInEuro , newTradeResult . LedgerEntries [ 1 ] . Qty ) ;
Assert . Equal ( tradeRequest . FromAsset , newTradeResult . LedgerEntries [ 1 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Trade , newTradeResult . LedgerEntries [ 1 ] . Type ) ;
Assert . Equal ( - 1 * MockCustodian . TradeFeeEuro , newTradeResult . LedgerEntries [ 2 ] . Qty ) ;
Assert . Equal ( tradeRequest . FromAsset , newTradeResult . LedgerEntries [ 2 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Fee , newTradeResult . LedgerEntries [ 2 ] . Type ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, SATS
2023-04-04 07:48:29 +02:00
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian . TradeFromAsset , ToAsset = "SATS" , Qty = new TradeQuantity ( MockCustodian . TradeQtyBought , TradeQuantity . ValueType . Exact ) } ;
2022-08-04 04:38:49 +02:00
await AssertApiError ( 400 , "use-asset-synonym" , async ( ) = > await tradeClient . MarketTradeCustodianAccountAsset ( storeId , accountId , satsTradeRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// TODO Test: Trade with percentage qty
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Trade, wrong assets method
2023-04-04 07:48:29 +02:00
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG" , ToAsset = MockCustodian . TradeToAsset , Qty = new TradeQuantity ( MockCustodian . TradeQtyBought , TradeQuantity . ValueType . Exact ) } ;
2022-08-04 04:38:49 +02:00
await AssertHttpError ( WrongTradingPairException . HttpCode , async ( ) = > await tradeClient . MarketTradeCustodianAccountAsset ( storeId , accountId , wrongAssetsTradeRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: wrong account ID
2022-08-04 04:38:49 +02:00
await AssertHttpError ( 404 , async ( ) = > await tradeClient . MarketTradeCustodianAccountAsset ( storeId , "WRONG-ACCOUNT-ID" , tradeRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: wrong store ID
2022-08-04 04:38:49 +02:00
await AssertHttpError ( 403 , async ( ) = > await tradeClient . MarketTradeCustodianAccountAsset ( "WRONG-STORE-ID" , accountId , tradeRequest ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: Trade, correct assets, wrong amount
2023-04-04 07:48:29 +02:00
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian . TradeFromAsset , ToAsset = MockCustodian . TradeToAsset , Qty = new TradeQuantity ( 0.01 m , TradeQuantity . ValueType . Exact ) } ;
2022-08-04 04:38:49 +02:00
await AssertApiError ( 400 , "insufficient-funds" , async ( ) = > await tradeClient . MarketTradeCustodianAccountAsset ( storeId , accountId , insufficientFundsTradeRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, unauth
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccountTradeQuote ( storeId , accountId , MockCustodian . TradeFromAsset , MockCustodian . TradeToAsset ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, auth, but wrong permission
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . GetCustodianAccountTradeQuote ( storeId , accountId , MockCustodian . TradeFromAsset , MockCustodian . TradeToAsset ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, auth, correct permission
2023-03-20 02:45:32 +01:00
var tradeQuote = await tradeClient . GetCustodianAccountTradeQuote ( storeId , accountId , MockCustodian . TradeFromAsset , MockCustodian . TradeToAsset ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( tradeQuote ) ;
Assert . Equal ( MockCustodian . TradeFromAsset , tradeQuote . FromAsset ) ;
Assert . Equal ( MockCustodian . TradeToAsset , tradeQuote . ToAsset ) ;
Assert . Equal ( MockCustodian . BtcPriceInEuro , tradeQuote . Bid ) ;
Assert . Equal ( MockCustodian . BtcPriceInEuro , tradeQuote . Ask ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, SATS
2023-04-10 04:07:03 +02:00
await AssertApiError ( 400 , "use-asset-synonym" , async ( ) = > await tradeClient . GetCustodianAccountTradeQuote ( storeId , accountId , MockCustodian . TradeFromAsset , "SATS" ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeQuote, wrong asset
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = > await tradeClient . GetCustodianAccountTradeQuote ( storeId , accountId , "WRONG-ASSET" , MockCustodian . TradeToAsset ) ) ;
await AssertHttpError ( 404 , async ( ) = > await tradeClient . GetCustodianAccountTradeQuote ( storeId , accountId , MockCustodian . TradeFromAsset , "WRONG-ASSET" ) ) ;
2022-05-18 07:59:56 +02:00
// Test: wrong account ID
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 404 , async ( ) = > await tradeClient . GetCustodianAccountTradeQuote ( storeId , "WRONG-ACCOUNT-ID" , MockCustodian . TradeFromAsset , MockCustodian . TradeToAsset ) ) ;
2022-05-18 07:59:56 +02:00
// Test: wrong store ID
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 403 , async ( ) = > await tradeClient . GetCustodianAccountTradeQuote ( "WRONG-STORE-ID" , accountId , MockCustodian . TradeFromAsset , MockCustodian . TradeToAsset ) ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
// Test: GetTradeInfo, unauth
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccountTradeInfo ( storeId , accountId , MockCustodian . TradeId ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeInfo, auth, but wrong permission
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . GetCustodianAccountTradeInfo ( storeId , accountId , MockCustodian . TradeId ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeInfo, auth, correct permission
2023-03-20 02:45:32 +01:00
var tradeResult = await tradeClient . GetCustodianAccountTradeInfo ( storeId , accountId , MockCustodian . TradeId ) ;
2022-05-18 07:59:56 +02:00
Assert . NotNull ( tradeResult ) ;
Assert . Equal ( accountId , tradeResult . AccountId ) ;
Assert . Equal ( mockCustodian . Code , tradeResult . CustodianCode ) ;
Assert . Equal ( MockCustodian . TradeId , tradeResult . TradeId ) ;
Assert . Equal ( tradeRequest . FromAsset , tradeResult . FromAsset ) ;
Assert . Equal ( tradeRequest . ToAsset , tradeResult . ToAsset ) ;
2023-01-06 14:18:07 +01:00
Assert . NotNull ( tradeResult . LedgerEntries ) ;
Assert . Equal ( 3 , tradeResult . LedgerEntries . Count ) ;
Assert . Equal ( MockCustodian . TradeQtyBought , tradeResult . LedgerEntries [ 0 ] . Qty ) ;
Assert . Equal ( tradeRequest . ToAsset , tradeResult . LedgerEntries [ 0 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Trade , tradeResult . LedgerEntries [ 0 ] . Type ) ;
Assert . Equal ( - 1 * MockCustodian . TradeQtyBought * MockCustodian . BtcPriceInEuro , tradeResult . LedgerEntries [ 1 ] . Qty ) ;
Assert . Equal ( tradeRequest . FromAsset , tradeResult . LedgerEntries [ 1 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Trade , tradeResult . LedgerEntries [ 1 ] . Type ) ;
Assert . Equal ( - 1 * MockCustodian . TradeFeeEuro , tradeResult . LedgerEntries [ 2 ] . Qty ) ;
Assert . Equal ( tradeRequest . FromAsset , tradeResult . LedgerEntries [ 2 ] . Asset ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Fee , tradeResult . LedgerEntries [ 2 ] . Type ) ;
2022-05-18 07:59:56 +02:00
// Test: GetTradeInfo, wrong trade ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await tradeClient . GetCustodianAccountTradeInfo ( storeId , accountId , "WRONG-TRADE-ID" ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: wrong account ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await tradeClient . GetCustodianAccountTradeInfo ( storeId , "WRONG-ACCOUNT-ID" , MockCustodian . TradeId ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: wrong store ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await tradeClient . GetCustodianAccountTradeInfo ( "WRONG-STORE-ID" , accountId , MockCustodian . TradeId ) ) ;
2022-05-18 07:59:56 +02:00
2023-03-20 02:45:32 +01:00
var qty = new TradeQuantity ( MockCustodian . WithdrawalAmount , TradeQuantity . ValueType . Exact ) ;
2023-04-10 04:07:03 +02:00
// Test: SimulateWithdrawal, unauth
2023-03-20 02:45:32 +01:00
var simulateWithdrawalRequest = new WithdrawRequestData ( MockCustodian . WithdrawalPaymentMethod , qty ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . SimulateCustodianAccountWithdrawal ( storeId , accountId , simulateWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, auth, but wrong permission
await AssertHttpError ( 403 , async ( ) = > await managerClient . SimulateCustodianAccountWithdrawal ( storeId , accountId , simulateWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, correct payment method, correct amount
var simulateWithdrawResponse = await withdrawalClient . SimulateCustodianAccountWithdrawal ( storeId , accountId , simulateWithdrawalRequest ) ;
AssertMockWithdrawal ( simulateWithdrawResponse , custodianAccountData ) ;
2023-04-10 04:07:03 +02:00
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, wrong payment method
var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData ( "WRONG-PAYMENT-METHOD" , qty ) ;
2023-04-10 04:07:03 +02:00
await AssertApiError ( 400 , "unsupported-payment-method" , async ( ) = > await withdrawalClient . SimulateCustodianAccountWithdrawal ( storeId , accountId , wrongPaymentMethodSimulateWithdrawalRequest ) ) ;
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, wrong account ID
await AssertHttpError ( 404 , async ( ) = > await withdrawalClient . SimulateCustodianAccountWithdrawal ( storeId , "WRONG-ACCOUNT-ID" , simulateWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, wrong store ID
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 403 , async ( ) = > await withdrawalClient . SimulateCustodianAccountWithdrawal ( "WRONG-STORE-ID" , accountId , simulateWithdrawalRequest ) ) ;
2023-03-20 02:45:32 +01:00
// Test: SimulateWithdrawal, correct payment method, wrong amount
var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData ( MockCustodian . WithdrawalPaymentMethod , TradeQuantity . Parse ( "0.666" ) ) ;
await AssertHttpError ( 400 , async ( ) = > await withdrawalClient . SimulateCustodianAccountWithdrawal ( storeId , accountId , wrongAmountSimulateWithdrawalRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, unauth
2023-03-20 02:45:32 +01:00
var createWithdrawalRequest = new WithdrawRequestData ( MockCustodian . WithdrawalPaymentMethod , qty ) ;
var createWithdrawalRequestPercentage = new WithdrawRequestData ( MockCustodian . WithdrawalPaymentMethod , qty ) ;
await AssertHttpError ( 401 , async ( ) = > await unauthClient . CreateCustodianAccountWithdrawal ( storeId , accountId , createWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, auth, but wrong permission
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . CreateCustodianAccountWithdrawal ( storeId , accountId , createWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, correct payment method, correct amount
2023-03-20 02:45:32 +01:00
var withdrawResponse = await withdrawalClient . CreateCustodianAccountWithdrawal ( storeId , accountId , createWithdrawalRequest ) ;
2022-05-18 07:59:56 +02:00
AssertMockWithdrawal ( withdrawResponse , custodianAccountData ) ;
2023-04-10 04:07:03 +02:00
2023-03-20 02:45:32 +01:00
// Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage
var withdrawWithPercentageResponse = await withdrawalClient . CreateCustodianAccountWithdrawal ( storeId , accountId , createWithdrawalRequestPercentage ) ;
AssertMockWithdrawal ( withdrawWithPercentageResponse , custodianAccountData ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, wrong payment method
2023-03-20 02:45:32 +01:00
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData ( "WRONG-PAYMENT-METHOD" , qty ) ;
2023-04-10 04:07:03 +02:00
await AssertApiError ( 400 , "unsupported-payment-method" , async ( ) = > await withdrawalClient . CreateCustodianAccountWithdrawal ( storeId , accountId , wrongPaymentMethodCreateWithdrawalRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, wrong account ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await withdrawalClient . CreateCustodianAccountWithdrawal ( storeId , "WRONG-ACCOUNT-ID" , createWithdrawalRequest ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, wrong store ID
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
2023-04-10 04:07:03 +02:00
await AssertHttpError ( 403 , async ( ) = > await withdrawalClient . CreateCustodianAccountWithdrawal ( "WRONG-STORE-ID" , accountId , createWithdrawalRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: CreateWithdrawal, correct payment method, wrong amount
2023-03-20 02:45:32 +01:00
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData ( MockCustodian . WithdrawalPaymentMethod , TradeQuantity . Parse ( "0.666" ) ) ;
await AssertHttpError ( 400 , async ( ) = > await withdrawalClient . CreateCustodianAccountWithdrawal ( storeId , accountId , wrongAmountCreateWithdrawalRequest ) ) ;
2022-05-18 07:59:56 +02:00
// Test: GetWithdrawalInfo, unauth
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 401 , async ( ) = > await unauthClient . GetCustodianAccountWithdrawalInfo ( storeId , accountId , MockCustodian . WithdrawalPaymentMethod , MockCustodian . WithdrawalId ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetWithdrawalInfo, auth, but wrong permission
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await managerClient . GetCustodianAccountWithdrawalInfo ( storeId , accountId , MockCustodian . WithdrawalPaymentMethod , MockCustodian . WithdrawalId ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: GetWithdrawalInfo, auth, correct permission
2023-03-20 02:45:32 +01:00
var withdrawalInfo = await withdrawalClient . GetCustodianAccountWithdrawalInfo ( storeId , accountId , MockCustodian . WithdrawalPaymentMethod , MockCustodian . WithdrawalId ) ;
2022-05-18 07:59:56 +02:00
AssertMockWithdrawal ( withdrawalInfo , custodianAccountData ) ;
// Test: GetWithdrawalInfo, wrong withdrawal ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await withdrawalClient . GetCustodianAccountWithdrawalInfo ( storeId , accountId , MockCustodian . WithdrawalPaymentMethod , "WRONG-WITHDRAWAL-ID" ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: wrong account ID
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 404 , async ( ) = > await withdrawalClient . GetCustodianAccountWithdrawalInfo ( storeId , "WRONG-ACCOUNT-ID" , MockCustodian . WithdrawalPaymentMethod , MockCustodian . WithdrawalId ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// Test: wrong store ID
// TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered
2023-03-20 02:45:32 +01:00
await AssertHttpError ( 403 , async ( ) = > await withdrawalClient . GetCustodianAccountWithdrawalInfo ( "WRONG-STORE-ID" , accountId , MockCustodian . WithdrawalPaymentMethod , MockCustodian . WithdrawalId ) ) ;
2023-04-10 04:07:03 +02:00
2022-05-18 07:59:56 +02:00
// TODO assert API error codes, not just status codes by using AssertCustodianApiError()
// TODO also test withdrawals for the various "Status" (Queued, Complete, Failed)
// TODO create a mock custodian with only ICustodian
// TODO create a mock custodian with only ICustodian + ICanWithdraw
// TODO create a mock custodian with only ICustodian + ICanTrade
// TODO create a mock custodian with only ICustodian + ICanDeposit
}
2023-03-20 02:45:32 +01:00
private void AssertMockWithdrawal ( WithdrawalBaseResponseData withdrawResponse , CustodianAccountData account )
2022-05-18 07:59:56 +02:00
{
Assert . NotNull ( withdrawResponse ) ;
Assert . Equal ( MockCustodian . WithdrawalAsset , withdrawResponse . Asset ) ;
Assert . Equal ( MockCustodian . WithdrawalPaymentMethod , withdrawResponse . PaymentMethod ) ;
Assert . Equal ( account . Id , withdrawResponse . AccountId ) ;
Assert . Equal ( account . CustodianCode , withdrawResponse . CustodianCode ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
Assert . Equal ( 2 , withdrawResponse . LedgerEntries . Count ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
Assert . Equal ( MockCustodian . WithdrawalAsset , withdrawResponse . LedgerEntries [ 0 ] . Asset ) ;
Assert . Equal ( MockCustodian . WithdrawalAmount - MockCustodian . WithdrawalFee , withdrawResponse . LedgerEntries [ 0 ] . Qty ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Withdrawal , withdrawResponse . LedgerEntries [ 0 ] . Type ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 07:59:56 +02:00
Assert . Equal ( MockCustodian . WithdrawalAsset , withdrawResponse . LedgerEntries [ 1 ] . Asset ) ;
Assert . Equal ( MockCustodian . WithdrawalFee , withdrawResponse . LedgerEntries [ 1 ] . Qty ) ;
Assert . Equal ( LedgerEntryData . LedgerEntryType . Fee , withdrawResponse . LedgerEntries [ 1 ] . Type ) ;
2023-03-20 02:45:32 +01:00
if ( withdrawResponse is WithdrawalResponseData withdrawalResponseData )
{
Assert . Equal ( MockCustodian . WithdrawalStatus , withdrawalResponseData . Status ) ;
Assert . Equal ( MockCustodian . WithdrawalTargetAddress , withdrawalResponseData . TargetAddress ) ;
Assert . Equal ( MockCustodian . WithdrawalTransactionId , withdrawalResponseData . TransactionId ) ;
Assert . Equal ( MockCustodian . WithdrawalId , withdrawalResponseData . WithdrawalId ) ;
Assert . NotEqual ( default , withdrawalResponseData . CreatedTime ) ;
}
if ( withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData )
{
Assert . Equal ( MockCustodian . WithdrawalMinAmount , withdrawalSimulationResponseData . MinQty ) ;
Assert . Equal ( MockCustodian . WithdrawalMaxAmount , withdrawalSimulationResponseData . MaxQty ) ;
}
2022-05-18 07:59:56 +02:00
}
2020-02-24 18:43:28 +01:00
}
}