Compare commits

..

75 Commits

Author SHA1 Message Date
c03dc48fe9 Do not crash if can't load rate cache 2020-02-16 22:07:56 +09:00
143c909812 bump 2020-02-16 19:58:53 +09:00
821b904163 Added SendGrid, Mailgun to Quick-fill email settings () 2020-02-15 14:37:29 +09:00
6015eb337a Fix broken link 2020-02-15 14:36:36 +09:00
5d817a0483 Revert fix mysql 2020-02-14 00:23:00 +09:00
ee9905e85a Fix mysql 2020-02-14 00:07:19 +09:00
ff4c7c364e Fix mysql 2020-02-14 00:02:47 +09:00
a2d657f5cb Fix mysql migration 2020-02-13 23:58:48 +09:00
db6a4687d2 Wallet prep work for BPU ()
* Wallet prep work for BPU

This PR prepares the wallet for . It makes transfers from the vault and ledger to go to their own post actions for processing (not particularly useful in this PR but is needed in BPU to propose a new tx)  It also makes the Sign with seed consistent with redirect to /psbt/ready after signing which it did not do (it stayed on the seed route)

* fix test

* add assert
2020-02-13 22:06:00 +09:00
07f0d95f56 BIP21 Support for Wallet spending ()
* BIP21 Support for Wallet spending

* extract bip21 loading to method

* add bip21 parsing test
2020-02-13 17:18:43 +09:00
1a409a441d Update POLIS related entries ()
* Update Polis related info and services

* Fix Polis Rate Fetcher

* Fix Polis ratefetcher - Cryptopia is obsolete

* POLIS rate provider changes to comply with internal testing

* URL / pair alignment

* Add small doc to re-trigger testing
2020-02-13 14:44:31 +09:00
445e184154 Merge pull request from pavlenex/readme
Minor readme cleanup + license clarification
2020-02-13 14:42:18 +09:00
9a10f55a85 Merge remote-tracking branch 'upstream/master' into readme 2020-02-12 19:11:53 +01:00
ae33b1d0a8 Fix PSBT Redirect No-access issues 2020-02-12 16:35:24 +09:00
4ed2db83a5 fix a broken link 2020-02-09 19:01:52 +01:00
500aa85142 Fix broken links 2020-02-09 17:35:00 +01:00
3b6cc84a93 Minor readme cleanup + license clarification 2020-02-09 17:33:44 +01:00
5ce29d2bb8 Merge pull request from Kukks/fix-revoke-access
Fixes  store token revoke redirect error
2020-02-08 21:54:43 +00:00
3184d2b2df Merge pull request from Kukks/coldcard-fix
Fix  Coldcard import dialog
2020-02-08 21:54:19 +00:00
f5e65ec2a6 Fix Coldcard import dialog 2020-02-08 10:54:34 +01:00
66488d813b Fixes store token revoke redirect error 2020-02-07 08:23:00 +01:00
4853cfe41a bump 2020-02-03 19:13:51 +09:00
dc7733abcd Merge pull request from Kukks/satscurrency
Add sats as a native currency
2020-02-03 08:42:35 +00:00
771c8e2758 Merge pull request from btcpayserver/feature/errorpages
Adding error pages to handle HTTP errors
2020-02-03 08:39:58 +00:00
24664b60af Adding test ensuring that api errors are properly returned 2020-02-03 02:21:03 -06:00
82393eb8bb Fixing api exception handling in the pipeline 2020-02-03 02:18:36 -06:00
b432d8903f Grammar fix by Kukks 2020-02-01 11:16:40 -06:00
ea9169f607 Updating 404 page not found assert 2020-02-01 02:29:08 -06:00
496a6f0f55 Special page to handle 429 errors 2020-02-01 01:59:56 -06:00
fb2a0fb7fb Special page to handle 500 errors 2020-02-01 01:58:17 -06:00
ef503fa907 Special page to handle 404 errors 2020-02-01 01:41:27 -06:00
fe2eca4fda Adding prettier error handling page in the pipeline 2020-02-01 01:40:50 -06:00
88835b5b55 Moving _LayoutWelcome to shared folder 2020-02-01 00:32:31 -06:00
876c940032 Reverting delegate reference to previous state until Nicolas confirms change 2020-02-01 00:26:01 -06:00
a08d5be35c Expanding tests to check implicit conversion of Sats to BTC 2020-01-29 22:31:43 -06:00
0074790684 Remove "#nullable enable" directive and unnecessary operators 2020-01-29 01:53:47 -06:00
23aaf794ef Add nullable enable directive to HttpClientRequestMaker.MakeRequestAsync 2020-01-29 01:53:47 -06:00
bb12d37416 Displaying sats in a more user-friendly way (space as group separator) ()
Fix: 
2020-01-27 19:57:46 +09:00
e058903450 Do not show assets in sync modal () 2020-01-26 19:45:52 +09:00
06f1c17a5f Make unused assets in store settings collapsed () 2020-01-26 19:45:24 +09:00
e00136de93 Fix spurious DefaultAntiforgery errors 2020-01-26 15:02:40 +09:00
56d8c033d7 Update display text on the view model. 2020-01-24 15:45:35 -06:00
666682677c Merge pull request from btcpayserver/feat/viewnewwindow
Providing open in new window split buttons
2020-01-24 15:34:25 -06:00
652b958d4f Removing viewapp command now that we directly redirect in cshtml 2020-01-24 15:11:34 -06:00
c7c0db612a Restoring IDs Selenium depends on for tests 2020-01-23 20:40:20 -06:00
a83edce4dc Updating idents, code formatting 2020-01-23 20:19:24 -06:00
f99058a9fa Adding code comment for review 2020-01-23 20:18:33 -06:00
a907143d81 Providing open in new window split button when updating crowdfund
Unifying styles on POS and Crowdfund settings

co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 20:17:29 -06:00
4ae173bb69 Providing open in new window split button when updating POS app
co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 20:04:34 -06:00
1436420a93 Providing link to view app in new window
co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 19:51:57 -06:00
086cbaa231 Add clightning rest services page ()
* Add clightning rest services page

* fix rebase
2020-01-23 22:20:37 +09:00
5dd3112e0d Ensure "import from....a new/existing seed" modal text is readable in Casa theme ()
fix 
2020-01-23 22:20:00 +09:00
b42e4f240a Fix ()
* Fix seed signing validation

* fix ident
2020-01-23 22:02:37 +09:00
7076692069 fix configurator password loader () 2020-01-22 15:16:32 +09:00
dcb3601791 Fix ETB asset id 2020-01-21 18:22:42 +01:00
54c7c0d696 Add currency precision based on network () 2020-01-21 22:28:13 +09:00
f324185d82 bump nbx 2020-01-21 21:47:51 +09:00
a63502873c Add implicit hidden rate rule for sats in parser 2020-01-21 13:34:00 +01:00
f5cbf6672a remove default rate rule for sats 2020-01-21 13:34:00 +01:00
a78dff5931 remove padding 2020-01-21 13:34:00 +01:00
f8139a9156 cleanup (remove sats rate provider and just use rate scripting) 2020-01-21 13:34:00 +01:00
27a61b7afd fix test 2020-01-21 13:34:00 +01:00
71671b9e16 Add sats as a native currency
This will allow you to create an invoice where its primary currency is denominated in sats
2020-01-21 13:33:59 +01:00
c68bf5220e bump 2020-01-21 21:09:49 +09:00
80ee03d897 Remove dead link 2020-01-21 21:06:35 +09:00
d0bfa67495 Fix build 2020-01-21 21:04:35 +09:00
bdb2edba12 Fix U2F signing 2020-01-21 21:00:34 +09:00
78d8f4e011 Fix rescan wallet link 2020-01-21 20:54:45 +09:00
1bfe9dda97 Integrate Configurator External Service () 2020-01-21 18:27:10 +09:00
8e6f43cd3a Sign with NBX Seed () 2020-01-21 17:33:12 +09:00
6848482999 Remove the next address to pay to from Invoice details page (Fix ) () 2020-01-21 16:53:24 +09:00
43967ee86e bump 2020-01-21 13:20:52 +09:00
61b99f6630 Add support for ETB liquid asset () 2020-01-21 13:19:55 +09:00
7e073fb7e1 Add test CanCreateSqlitedb 2020-01-19 22:10:05 +09:00
1ceb5cb576 Fix sqlite madness ()
* Need to reference Microsoft.EntityFrameworkCore.Design else you can't generate migrations
* fix design time migrations issues
* update snapshot
```
Your startup project 'BTCPayServer.Data' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is c
orrect, install the package, and try again.
```
*
2020-01-19 21:57:50 +09:00
85 changed files with 1305 additions and 423 deletions
.gitignore
BTCPayServer.Common
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
Configuration
Controllers
HostedServices
Hosting
Models
Payments
Properties
Services
Views
wwwroot
Build
README.md

1
.gitignore vendored

@ -293,3 +293,4 @@ BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode
BTCPayServer/testpwd

@ -16,13 +16,13 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
"POLIS_BTC = polispay(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

@ -59,7 +59,8 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
InitMonero();
InitPolis();
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
{
@ -77,7 +78,6 @@ namespace BTCPayServer
}
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

@ -31,21 +31,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
},
SupportRBF = true
});
}
}

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
using NBitcoin;
namespace BTCPayServer
{
@ -16,6 +11,7 @@ namespace BTCPayServer
{
CryptoCode = "USDt",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"USDT_UST = 1",
@ -30,21 +26,30 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
}
SupportRBF = true
});
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "ETB",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"ETB_X = ETB_BTC * BTC_X",
"ETB_BTC = bitpay(ETB_BTC)"
},
Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
}
}

@ -6,16 +6,16 @@ using NBXplorer.Models;
namespace BTCPayServer
{
public class ElementsBTCPayNetwork:BTCPayNetwork
public class ElementsBTCPayNetwork : BTCPayNetwork
{
public string NetworkCryptoCode { get; set; }
public uint256 AssetId { get; set; }
public override bool ReadonlyWallet { get; set; } = true;
public int Divisibility { get; set; } = 8;
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs)
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(
NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Where(output =>
return evtOutputs.Outputs.Where(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
@ -23,7 +23,7 @@ namespace BTCPayServer
});
}
public override string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
}

@ -10,6 +10,7 @@ namespace BTCPayServer
{
CryptoCode = "XMR",
DisplayName = "Monero",
Divisibility = 12,
BlockExplorerLink =
NetworkType == NetworkType.Mainnet
? "https://www.exploremonero.com/transaction/{0}"

@ -113,18 +113,19 @@ namespace BTCPayServer
});
}
public virtual string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue}";
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
}
}
public abstract class BTCPayNetworkBase
{
public bool ShowSyncSummary { get; set; } = true;
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]
public bool IsBTC
{

@ -196,10 +196,12 @@ namespace BTCPayServer.Services.Rates
throw new APIException(text);
}
api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse));
Action<IAPIRequestMaker, RequestMakerState, object>? requestStateChanged = RequestStateChanged;
// local reference to handle delegate becoming null, extended discussion here:
// https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103
var requestStateChanged = RequestStateChanged;
if (requestStateChanged != null)
{
requestStateChanged!(this, RequestMakerState.Finished, text);
requestStateChanged(this, RequestMakerState.Finished, text);
return text;
}
return text;

@ -0,0 +1,26 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class PolisRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public PolisRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["data"].Value<decimal>();
return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) };
}
}
}

@ -99,6 +99,8 @@ namespace BTCPayServer.Rating
RuleList ruleList;
decimal _Spread;
private const string ImplicitSatsRule = "SATS_X = SATS_BTC * BTC_X;\nSATS_BTC = 0.00000001;\n";
public decimal Spread
{
get
@ -126,6 +128,7 @@ namespace BTCPayServer.Rating
}
public static bool TryParse(string str, out RateRules rules, out List<RateRulesErrors> errors)
{
str = ImplicitSatsRule + str;
rules = null;
errors = null;
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
@ -195,6 +198,7 @@ namespace BTCPayServer.Rating
{
return root.NormalizeWhitespace("", "\n")
.ToFullString()
.Replace(ImplicitSatsRule, string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase);
}

@ -79,6 +79,8 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
@ -98,6 +100,7 @@ namespace BTCPayServer.Services.Rates
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
// Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking

@ -71,7 +71,7 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmLedger.WebsocketPath);
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"));
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmPSBT.Decoded);
@ -80,14 +80,20 @@ namespace BTCPayServer.Tests
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.ExtKey);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive);
@ -98,7 +104,7 @@ namespace BTCPayServer.Tests
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
combineVM.PSBT = signedPSBT.ToBase64();
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
@ -108,7 +114,7 @@ namespace BTCPayServer.Tests
// Can use uploaded file?
combineVM.PSBT = null;
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
@ -116,17 +122,18 @@ namespace BTCPayServer.Tests
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
}
}
private static string AssertRedirectedPSBT(IActionResult view)
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
{
var postRedirectView = Assert.IsType<ViewResult>(view);
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
Assert.Equal(actionName, postRedirectViewModel.AspAction);
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value;
return redirectedPSBT;
}

@ -56,6 +56,8 @@ namespace BTCPayServer.Tests
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
(Pair: "SATS_CAD", Expected: "0.00000001 * coinbase(BTC_CAD)"),
(Pair: "Sats_USD", Expected: "0.00000001 * kraken(BTC_USD)")
};
foreach (var test in tests)
{
@ -102,6 +104,8 @@ namespace BTCPayServer.Tests
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
(Pair: "SATS_USD", Expected: "0.00000001 * kraken(BTC_USD)", ExpectedExchangeRates: "kraken(BTC_USD)"),
(Pair: "SATS_EUR", Expected: "0.00000001 * coinbase(BTC_EUR)", ExpectedExchangeRates: "coinbase(BTC_EUR)")
};
foreach (var test in tests2)
{
@ -189,6 +193,37 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
// Make sure defining value in sats works
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
builder.AppendLine("BTC_X = coinbase(BTC_X)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("0.00000001 * (6000, 6100)", rule2.ToString(true));
Assert.Equal(0.00006m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_SATS"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (0.00000001 * (6000, 6100))", rule2.ToString(true));
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
// testing rounding
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
Assert.Equal(1m / 0.0000000123m, rule2.BidAsk.Ask);
Assert.Equal(1m / 0.0000000234m, rule2.BidAsk.Bid);
}
}
}

@ -18,6 +18,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Views.Stores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -70,18 +71,18 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
internal void AssertHappyMessage()
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
using var cts = new CancellationTokenSource(20_000);
while (!cts.IsCancellationRequested)
{
var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any();
var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed);
if (success)
return;
Thread.Sleep(100);
}
Logs.Tester.LogInformation(this.Driver.PageSource);
Assert.True(false, "Should have shown happy message");
Assert.True(false, $"Should have shown {severity} message");
}
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
@ -202,7 +203,7 @@ namespace BTCPayServer.Tests
internal void AssertNotFound()
{
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
Assert.Contains("404 - Page not found</h1>", Driver.PageSource);
}
public void GoToHome()

@ -9,6 +9,8 @@ using System.Linq;
using NBitcoin;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using BTCPayServer.Models;
using NBitcoin.Payment;
namespace BTCPayServer.Tests
{
@ -432,6 +434,13 @@ namespace BTCPayServer.Tests
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
//you cant use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("WalletReceive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
@ -472,16 +481,19 @@ namespace BTCPayServer.Tests
var address = invoice.EntityToDTO().Addresses["BTC"];
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId);
var mnemonic = s.GenerateWallet("BTC", "", true, true);
//lets import and save private keys
var root = new Mnemonic(mnemonic).DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
@ -538,7 +550,43 @@ namespace BTCPayServer.Tests
checkboxElement.Click();
}
}
SignWith(mnemonic);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(0, jack, 0.01m);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick();
Assert.EndsWith("psbt", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Assert.Equal(walletTransactionLink, s.Driver.Url);
var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
}
}
}

@ -173,7 +173,7 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
CryptoCode = "BTC",
NetworkFee = 0.00000100m,
@ -315,7 +315,7 @@ namespace BTCPayServer.Tests
entity.Payments.Add(
new PaymentEntity()
{
Output = new TxOut(Money.Coins(0.2m), new Key()),
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true
});
@ -330,15 +330,15 @@ namespace BTCPayServer.Tests
paymentMethods.Add(
new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
CryptoCode = "BTC",
Rate = 1000,
NextNetworkFee = Money.Coins(0.1m)
});
paymentMethods.Add(
new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
CryptoCode = "LTC",
Rate = 500,
NextNetworkFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
@ -521,6 +521,39 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanThrowBitpay404Error()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
try
{
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
}
catch (BitPayException ex)
{
Assert.Equal("Object not found", ex.Errors.First());
}
}
}
[Fact]
[Trait("Fast", "Fast")]
public void RoundupCurrenciesCorrectly()
@ -819,7 +852,7 @@ namespace BTCPayServer.Tests
var walletId = new WalletId(acc.StoreId, "BTC");
acc.IsAdmin = true;
walletController = acc.GetController<WalletsController>();
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.True(rescan.Ok);
Assert.True(rescan.IsFullySync);
@ -1788,7 +1821,7 @@ namespace BTCPayServer.Tests
string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
@ -1970,15 +2003,15 @@ donation:
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
@ -2026,8 +2059,8 @@ donation:
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
}
//test inventory related features
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
@ -2040,13 +2073,13 @@ inventoryitem:
noninventoryitem:
price: 10.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
//inventoryitem has 1 item available
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
//we already bought all available stock so this should fail
await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
//inventoryitem has unlimited items available
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
@ -2056,7 +2089,7 @@ noninventoryitem:
Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem")));
var inventoryItemInvoice = Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
Assert.NotNull(inventoryItemInvoice);
//let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock
var controller = tester.PayTester.GetController<InvoiceController>(user.UserId, user.StoreId);
var appService = tester.PayTester.GetService<AppService>();
@ -2068,7 +2101,7 @@ noninventoryitem:
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal(1, appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
}
}
@ -2251,7 +2284,7 @@ noninventoryitem:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
//
//
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
@ -2689,7 +2722,7 @@ noninventoryitem:
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
result.Fetcher.InvalidateCache();
var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
@ -2702,6 +2735,11 @@ noninventoryitem:
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else if (result.ExpectedName == "polispay")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS
}
else
{
// This check if the currency pair is using right currency pair
@ -2887,6 +2925,18 @@ noninventoryitem:
return name;
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public async Task CanCreateSqlitedb()
{
if (File.Exists("temp.db"))
File.Delete("temp.db");
// This test sqlite can migrate
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlite("Data Source=temp.db");
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
@ -2947,7 +2997,7 @@ noninventoryitem:
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
@ -2968,8 +3018,8 @@ noninventoryitem:
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
@ -2980,13 +3030,13 @@ noninventoryitem:
}}
}
});
Assert.Single(invoice.SupportedTransactionCurrencies);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
@ -3007,7 +3057,7 @@ noninventoryitem:
})).ActionName);
var manageController = user.GetController<ManageController>();
//by default no u2f devices available
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addRequest = Assert.IsType<AddU2FDeviceViewModel>(Assert.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
@ -3036,10 +3086,10 @@ noninventoryitem:
};
await context.U2FDevices.AddAsync(newDevice);
await context.SaveChangesAsync();
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
}
//check if we are showing the u2f login screen now
@ -3053,10 +3103,10 @@ noninventoryitem:
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
//2fa was never enabled for user so this should be empty
Assert.Null(vm.LoginWith2FaViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
}
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();

@ -76,7 +76,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.5
image: nicolasdorier/nbxplorer:2.1.8
restart: unless-stopped
ports:
- "32838:32838"

@ -141,6 +141,7 @@ namespace BTCPayServer.Configuration
ExternalServices.Load(net.CryptoCode, conf);
}
ExternalServices.LoadNonCryptoServices(conf);
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
var services = conf.GetOrDefault<string>("externalservices", null);

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
@ -46,7 +47,7 @@ namespace BTCPayServer.Configuration
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest || serviceType == ExternalServiceTypes.CLightningRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
@ -77,7 +78,7 @@ namespace BTCPayServer.Configuration
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
if (new []{ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator}.Contains(serviceType))
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
@ -94,7 +95,7 @@ namespace BTCPayServer.Configuration
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator)
{
connectionString.AccessKey = cookieFileContent;
}

@ -35,17 +35,30 @@ namespace BTCPayServer.Configuration
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
"Ride the Lightning server");
Load(configuration, cryptoCode, "clightningrest", ExternalServiceTypes.CLightningRest, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/clightning-rest/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning REST");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
public void LoadNonCryptoServices(IConfiguration configuration)
{
Load(configuration, null, "configurator", ExternalServiceTypes.Configurator, "Invalid setting {0}, " + Environment.NewLine +
$"configurator: 'cookiefilepathfile=/etc/configurator/cookie'" + Environment.NewLine +
"Error: {1}",
"Configurator");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var setting = $"{(!string.IsNullOrEmpty(cryptoCode)? $"{cryptoCode}.": string.Empty)}external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
@ -65,8 +78,11 @@ namespace BTCPayServer.Configuration
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
return this.FirstOrDefault(o =>
(cryptoCode == null && o.CryptoCode == null) ||
(o.CryptoCode != null && o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase))
&&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
@ -88,6 +104,8 @@ namespace BTCPayServer.Configuration
RTL,
Charge,
P2P,
RPC
RPC,
Configurator,
CLightningRest
}
}

@ -22,8 +22,8 @@ namespace BTCPayServer.Controllers
return String.Empty;
}
}
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
@ -61,8 +61,8 @@ namespace BTCPayServer.Controllers
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
@ -70,9 +70,9 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
if (!string.IsNullOrEmpty(vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
@ -98,14 +98,14 @@ namespace BTCPayServer.Controllers
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
@ -114,13 +114,13 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
@ -157,24 +157,16 @@ namespace BTCPayServer.Controllers
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
if (command == "save")
{
await _AppService.UpdateOrCreateApp(app);
await _AppService.UpdateOrCreateApp(app);
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
else if (command == "viewapp")
_EventAggregator.Publish(new AppUpdated()
{
return RedirectToAction(nameof(AppsPublicController.ViewCrowdfund), "AppsPublic", new { appId });
}
return NotFound();
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
}
}

@ -79,9 +79,9 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; }
public string Description { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
@ -96,7 +96,7 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
SearchTerm = $"storeid:{app.StoreDataId}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : ""
};
if (HttpContext?.Request != null)
{
@ -197,8 +197,8 @@ namespace BTCPayServer.Controllers
NotificationEmail = vm.NotificationEmail,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
});
await _AppService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";
@ -211,8 +211,8 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
else
}
else
{
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/[action]")]
public class ErrorController : Controller
{
public IActionResult Handle(int? statusCode = null)
{
if (statusCode.HasValue)
{
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
{
var viewName = statusCode.ToString();
return View(viewName);
}
}
return View(statusCode);
}
}
}

@ -104,7 +104,6 @@ namespace BTCPayServer.Controllers
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var paymentMethodDetails = data.GetPaymentMethodDetails();
cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination();
cryptoPayment.Rate = ExchangeRate(data);
model.CryptoPayments.Add(cryptoPayment);
}

@ -528,10 +528,12 @@ namespace BTCPayServer.Controllers
return null;
}
[Route("server/services/{serviceName}/{cryptoCode}")]
[Route("server/services/{serviceName}/{cryptoCode?}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _))
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
@ -542,6 +544,7 @@ namespace BTCPayServer.Controllers
try
{
if (service.Type == ExternalServiceTypes.P2P)
{
return View("P2PService", new LightningWalletServices()
@ -592,9 +595,19 @@ namespace BTCPayServer.Controllers
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.CLightningRest:
return LndServices(service, connectionString, nonce, "CLightningRestServices");
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
case ExternalServiceTypes.Configurator:
return View("ConfiguratorService",
new LightningWalletServices()
{
ShowQR = showQR,
WalletName = service.ServiceName,
ServiceLink = $"{connectionString.Server}?password={connectionString.AccessKey}"
});
default:
throw new NotSupportedException(service.Type.ToString());
}
@ -663,7 +676,7 @@ namespace BTCPayServer.Controllers
return View(nameof(LightningChargeServices), vm);
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce, string view = nameof(LndServices))
{
var model = new LndServicesViewModel();
if (service.Type == ExternalServiceTypes.LNDGRPC)
@ -673,7 +686,7 @@ namespace BTCPayServer.Controllers
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
{
model.Uri = connectionString.Server.AbsoluteUri;
model.ConnectionType = "REST";
@ -702,7 +715,7 @@ namespace BTCPayServer.Controllers
}
}
return View(nameof(LndServices), model);
return View(view, model);
}
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
@ -754,10 +767,10 @@ namespace BTCPayServer.Controllers
grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Type = service.Type == ExternalServiceTypes.LNDRest? "lnd-rest": "clightning-rest";
restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf);
}
@ -778,7 +791,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
[Route("server/services/dynamic-dns")]
public async Task<IActionResult> DynamicDnsServices()
{

@ -499,13 +499,16 @@ namespace BTCPayServer.Controllers
case BitcoinPaymentType _:
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
Value = strategy?.ToPrettyString() ?? string.Empty,
Value = value,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null,
Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
});
break;
case LightningPaymentType _:
@ -670,7 +673,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token";
else
TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
return RedirectToAction(nameof(ListTokens));
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId});
}
[HttpGet]

@ -7,6 +7,7 @@ using BTCPayServer.ModelBinders;
using BTCPayServer.Models.WalletViewModels;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
@ -19,7 +20,6 @@ namespace BTCPayServer.Controllers
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
foreach (var transactionOutput in sendModel.Outputs)
{
var psbtDestination = new CreatePSBTDestination();
@ -56,12 +56,15 @@ namespace BTCPayServer.Controllers
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
vm.CryptoCode = network.CryptoCode;
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.Mnemonic));
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
{
vm.Decoded = psbt.ToString();
vm.PSBT = psbt.ToBase64();
}
return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
[Route("{walletId}/psbt")]
@ -93,7 +96,7 @@ namespace BTCPayServer.Controllers
case "vault":
return ViewVault(walletId, psbt);
case "ledger":
return ViewWalletSendLedger(psbt);
return ViewWalletSendLedger(walletId, psbt);
case "update":
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network);
@ -103,12 +106,25 @@ namespace BTCPayServer.Controllers
return View(vm);
}
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
return RedirectToWalletPSBT(walletId, psbt, vm.FileName);
return RedirectToWalletPSBT(psbt, vm.FileName);
case "seed":
return SignWithSeed(walletId, psbt.ToBase64());
case "nbx-seed":
if (await CanUseHotWallet())
{
var derivationScheme = GetDerivationSchemeSettings(walletId);
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
return await SignWithSeed(walletId,
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()});
}
return View(vm);
case "broadcast":
{
return await WalletPSBTReady(walletId, psbt.ToBase64());
return RedirectToWalletPSBTReady(psbt.ToBase64());
}
case "combine":
ModelState.Remove(nameof(vm.PSBT));
@ -145,7 +161,6 @@ namespace BTCPayServer.Controllers
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
vm.SigningKey = signingKey;
vm.SigningKeyPath = signingKeyPath;
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
return NotFound();
@ -207,7 +222,7 @@ namespace BTCPayServer.Controllers
vm.CanCalculateBalance = true;
vm.Positive = balanceChange >= Money.Zero;
}
vm.Inputs = new List<WalletPSBTReadyViewModel.InputViewModel>();
foreach (var input in psbtObject.Inputs)
{
var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
inputVm.Positive = balanceChange2 >= Money.Zero;
inputVm.Index = (int)input.Index;
}
vm.Destinations = new List<WalletPSBTReadyViewModel.DestinationViewModel>();
foreach (var output in psbtObject.Outputs)
{
var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
@ -280,14 +295,14 @@ namespace BTCPayServer.Controllers
catch
{
vm.GlobalError = "Invalid PSBT";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
vm.SetErrors(errors);
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
var transaction = psbt.ExtractTransaction();
try
@ -296,24 +311,24 @@ namespace BTCPayServer.Controllers
if (!broadcastResult.Success)
{
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
}
catch (Exception ex)
{
vm.GlobalError = "Error while broadcasting: " + ex.Message;
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
return RedirectToWalletTransaction(walletId, transaction);
}
else if (command == "analyze-psbt")
{
return RedirectToWalletPSBT(walletId, psbt);
return RedirectToWalletPSBT(psbt);
}
else
{
vm.GlobalError = "Unknown command";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
}
@ -342,7 +357,7 @@ namespace BTCPayServer.Controllers
}
sourcePSBT = sourcePSBT.Combine(psbt);
TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!";
return RedirectToWalletPSBT(walletId, sourcePSBT);
return RedirectToWalletPSBT(sourcePSBT);
}
}
}

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
@ -8,7 +7,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
@ -19,20 +17,15 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
namespace BTCPayServer.Controllers
{
@ -54,6 +47,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly EventAggregator _EventAggregator;
private readonly SettingsRepository _settingsRepository;
public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable;
@ -70,7 +64,8 @@ namespace BTCPayServer.Controllers
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
EventAggregator eventAggregator)
EventAggregator eventAggregator,
SettingsRepository settingsRepository)
{
_currencyTable = currencyTable;
Repository = repo;
@ -86,6 +81,7 @@ namespace BTCPayServer.Controllers
_walletProvider = walletProvider;
_WalletReceiveStateService = walletReceiveStateService;
_EventAggregator = eventAggregator;
_settingsRepository = settingsRepository;
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
@ -368,6 +364,15 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(WalletReceive), new {walletId});
}
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
@ -405,6 +410,9 @@ namespace BTCPayServer.Controllers
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey));
model.CurrentBalance = await balance;
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
@ -430,12 +438,13 @@ namespace BTCPayServer.Controllers
}
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
{
if (walletId?.StoreId == null)
return NotFound();
@ -446,6 +455,13 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(vm, bip21, network);
return View(vm);
}
decimal transactionAmountSum = 0;
if (command == "add-output")
@ -461,7 +477,6 @@ namespace BTCPayServer.Controllers
vm.Outputs.RemoveAt(index);
return View(vm);
}
if (!vm.Outputs.Any())
{
@ -554,20 +569,70 @@ namespace BTCPayServer.Controllers
{
case "vault":
return ViewVault(walletId, psbt.PSBT);
case "nbx-seed":
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation);
return await SignWithSeed(walletId, new SignWithSeedViewModel()
{
SeedOrKey = extKey,
PSBT = psbt.PSBT.ToBase64()
});
case "ledger":
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
return ViewWalletSendLedger(walletId, psbt.PSBT, psbt.ChangeAddress);
case "seed":
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(walletId, psbt.PSBT, name);
return RedirectToWalletPSBT(psbt.PSBT, name);
default:
return View(vm);
}
}
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
{
try
{
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
{
bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}";
}
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
}
};
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Html =
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
});
}
}
catch (Exception)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
});
}
ModelState.Clear();
}
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
{
return View("WalletSendVault", new WalletSendVaultModel()
@ -578,7 +643,31 @@ namespace BTCPayServer.Controllers
});
}
private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null)
[HttpPost]
[Route("{walletId}/vault")]
public async Task<IActionResult> SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(model.PSBT);
}
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null)
{
var vm = new PostRedirectViewModel()
{
AspController = "Wallets",
AspAction = nameof(WalletPSBTReady),
Parameters =
{
new KeyValuePair<string, string>("psbt", psbt),
new KeyValuePair<string, string>("SigningKey", signingKey),
new KeyValuePair<string, string>("SigningKeyPath", signingKeyPath)
}
};
return View("PostRedirect", vm);
}
private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null)
{
var vm = new PostRedirectViewModel()
{
@ -616,17 +705,25 @@ namespace BTCPayServer.Controllers
return null;
}
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
private ViewResult ViewWalletSendLedger(WalletId walletId, PSBT psbt, BitcoinAddress hintChange = null)
{
SetAmbientPSBT(psbt);
return View("WalletSendLedger", new WalletSendLedgerModel()
{
PSBT = psbt.ToBase64(),
HintChange = hintChange?.ToString(),
WebsocketPath = this.Url.Action(nameof(LedgerConnection))
WebsocketPath = this.Url.Action(nameof(LedgerConnection), new { walletId = walletId.ToString() })
});
}
[HttpPost]
[Route("{walletId}/ledger")]
public async Task<IActionResult> SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendLedgerModel model)
{
return RedirectToWalletPSBTReady(model.PSBT);
}
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,string psbt)
@ -643,7 +740,7 @@ namespace BTCPayServer.Controllers
{
if (!ModelState.IsValid)
{
return View(viewModel);
return View("SignWithSeed", viewModel);
}
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (network == null)
@ -666,7 +763,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
{
return View(viewModel);
return View("SignWithSeed", viewModel);
}
ExtKey signingKey = null;
@ -679,7 +776,7 @@ namespace BTCPayServer.Controllers
if (rootedKeyPath == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
return View(viewModel);
return View("SignWithSeed", viewModel);
}
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
@ -700,7 +797,7 @@ namespace BTCPayServer.Controllers
return View(viewModel);
}
ModelState.Remove(nameof(viewModel.PSBT));
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
}
private bool PSBTChanged(PSBT psbt, Action act)

@ -115,20 +115,24 @@ namespace BTCPayServer.HostedServices
private async Task TryLoadRateCache()
{
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
if (cache != null)
try
{
_LastCacheDate = cache.Created;
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
foreach (var provider in _RateProviderFactory.Providers)
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
if (cache != null)
{
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
provider.Value is BackgroundFetcherRateProvider fetcher)
_LastCacheDate = cache.Created;
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
foreach (var provider in _RateProviderFactory.Providers)
{
fetcher.LoadState(state);
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
provider.Value is BackgroundFetcherRateProvider fetcher)
{
fetcher.LoadState(state);
}
}
}
}
catch { }
}
DateTimeOffset? _LastCacheDate;

@ -62,10 +62,12 @@ namespace BTCPayServer.Hosting
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
return;
}
catch (BitpayHttpException ex)
{
await HandleBitpayHttpException(httpContext, ex);
return;
}
catch (Exception ex)
{
@ -133,13 +135,9 @@ namespace BTCPayServer.Hosting
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;
using (var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true))
{
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
writer.Write(result);
await writer.FlushAsync();
}
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
await httpContext.Response.WriteAsync(result);
}
}
}

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Validation.AspNetCore;
using OpenIddict.Abstractions;
@ -49,6 +50,9 @@ namespace BTCPayServer.Hosting
Logs.Configure(LoggerFactory);
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddDataProtection()
.SetApplicationName("BTCPay Server")
.PersistKeysToFileSystem(GetDataDir());
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
@ -140,6 +144,11 @@ namespace BTCPayServer.Hosting
}
}
private DirectoryInfo GetDataDir()
{
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
}
private void ConfigureOpenIddict(IServiceCollection services)
{
// Register the OpenIddict services.
@ -232,6 +241,10 @@ namespace BTCPayServer.Hosting
forwardingOptions.KnownProxies.Clear();
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
app.UseForwardedHeaders(forwardingOptions);
app.UseStatusCodePagesWithReExecute("/Error/Handle", "?statusCode={0}");
app.UsePayServer();
app.UseRouting();
app.UseCors();
@ -243,7 +256,6 @@ namespace BTCPayServer.Hosting
app.UseSession();
app.UseWebSockets();
app.UseStatusCodePages();
app.UseEndpoints(endpoints =>
{

@ -10,16 +10,20 @@ namespace BTCPayServer.Models.AppViewModels
public class UpdateCrowdfundViewModel
{
public string StoreId { get; set; }
[Required] [MaxLength(30)] public string Title { get; set; }
[Required]
[MaxLength(30)]
public string Title { get; set; }
[MaxLength(50)] public string Tagline { get; set; }
[MaxLength(50)]
public string Tagline { get; set; }
[Required]
public string Description { get; set; }
[Required] public string Description { get; set; }
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
[Display(Name = "Callback Notification Url")]
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice IPN Notification")]
@ -59,7 +63,8 @@ namespace BTCPayServer.Models.AppViewModels
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
[Display(Name = "Reset goal every")]
public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
public int ResetEveryAmount { get; set; } = 1;
@ -78,7 +83,7 @@ namespace BTCPayServer.Models.AppViewModels
public string EmbeddedCSS { get; set; }
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
public bool UseAllStoreInvoices { get; set; }
public bool UseAllStoreInvoices { get; set; }
public string AppId { get; set; }
public string SearchTerm { get; set; }
@ -90,10 +95,18 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
public string Sounds{ get; set; }
public string Sounds { get; set; }
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
public string AnimationColors{ get; set; }
public string AnimationColors { get; set; }
public bool NotificationEmailWarning { get; set; }
// NOTE: Improve validation if needed
public bool ModelWithMinimumData
{
get { return Description != null && Title != null && TargetCurrency != null; }
}
}
}

@ -15,6 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
Currency = "USD";
}
[Required]
public decimal? Amount
{
@ -28,11 +29,13 @@ namespace BTCPayServer.Models.InvoicingModels
}
[Required]
[DisplayName("Store Id")]
public string StoreId
{
get; set;
}
[DisplayName("Order Id")]
public string OrderId
{
get; set;
@ -51,6 +54,7 @@ namespace BTCPayServer.Models.InvoicingModels
}
[EmailAddress]
[DisplayName("Buyer Email")]
public string BuyerEmail
{
get; set;
@ -72,22 +76,19 @@ namespace BTCPayServer.Models.InvoicingModels
public SelectList Stores
{
get;
set;
get; set;
}
[DisplayName("Supported Transaction Currencies")]
public List<string> SupportedTransactionCurrencies
{
get;
set;
get; set;
}
[DisplayName("Available Payment Methods")]
public SelectList AvailablePaymentMethods
{
get;
set;
get; set;
}
}
}

@ -36,7 +36,6 @@ namespace BTCPayServer.Models.InvoicingModels
public string PaymentMethod { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
public string Overpaid { get; set; }

@ -15,26 +15,8 @@ namespace BTCPayServer.Models
public StatusSeverity Severity { get; set; }
public bool AllowDismiss { get; set; } = true;
public string SeverityCSS
{
get
{
switch (Severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
case StatusSeverity.Warning:
return "warning";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public string SeverityCSS => ToString(Severity);
private void ParseNonJsonStatus(string s)
{
Message = s;
@ -43,6 +25,23 @@ namespace BTCPayServer.Models
: StatusSeverity.Success;
}
public static string ToString(StatusSeverity severity)
{
switch (severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
case StatusSeverity.Warning:
return "warning";
default:
throw new ArgumentOutOfRangeException();
}
}
public enum StatusSeverity
{
Info,

@ -14,6 +14,7 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
public class AdditionalPaymentMethod

@ -13,6 +13,8 @@ namespace BTCPayServer.Models.WalletViewModels
public string CryptoCode { get; set; }
public string Decoded { get; set; }
string _FileName;
public bool NBXSeedAvailable { get; set; }
public string FileName
{
get

@ -45,5 +45,7 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SupportRBF { get; set; }
[Display(Name = "Disable RBF")]
public bool DisableRBF { get; set; }
public bool NBXSeedAvailable { get; set; }
}
}

@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, (BTCPayNetwork)network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility);
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), (BTCPayNetwork)network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
@ -180,11 +180,15 @@ namespace BTCPayServer.Payments.Lightning
model.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC" )
{
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name);
satoshiCulture.NumberFormat.NumberGroupSeparator = " ";
model.CryptoCode = "Sats";
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
}
}
public override string GetCryptoImage(PaymentMethodId paymentMethodId)

@ -64,7 +64,6 @@ namespace BTCPayServer.Payments
public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData);
public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str);
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
public abstract string InvoiceViewPaymentPartialName { get; }
}

@ -40,6 +40,7 @@
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
"BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false",

@ -847,7 +847,7 @@ namespace BTCPayServer.Services.Invoices
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = 8;
int precision = Network?.Divisibility ?? 8;
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0;
@ -857,8 +857,8 @@ namespace BTCPayServer.Services.Invoices
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
paid += _.GetValue(paymentMethods, GetId());
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
paid += _.GetValue(paymentMethods, GetId(), null, precision);
if (!paidEnough)
{
totalDue += txFee;
@ -991,18 +991,18 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618
return this;
}
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision)
{
value = value ?? this.GetCryptoPaymentData().GetValue();
var to = paymentMethodId;
var from = this.GetPaymentMethodId();
if (to == from)
return decimal.Round(value.Value, 8);
return decimal.Round(value.Value, precision);
var fromRate = paymentMethods[from].Rate;
var toRate = paymentMethods[to].Rate;
var fiatValue = fromRate * decimal.Round(value.Value, 8);
var fiatValue = fromRate * decimal.Round(value.Value, precision);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return otherCurrencyValue;
}

@ -96,8 +96,14 @@ namespace BTCPayServer.Services.Rates
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode);
}
_CurrencyProviders.TryAdd("SATS",
new NumberFormatInfo()
{
CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3
});
}
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
}
@ -180,7 +186,7 @@ namespace BTCPayServer.Services.Rates
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = 8,
Divisibility = network.Divisibility,
Name = network.CryptoCode,
Crypto = true
}))
@ -189,6 +195,15 @@ namespace BTCPayServer.Services.Rates
}
}
dico.TryAdd("SATS", new CurrencyData()
{
Code = "SATS",
Crypto = true,
Divisibility = 0,
Name = "Satoshis",
Symbol = "Sats",
});
return dico.Values.ToArray();
}

@ -2,7 +2,7 @@
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@{
ViewData["Title"] = "Log in";
Layout = "_WelcomeLayout.cshtml";
Layout = "_LayoutWelcome";
}
@if (TempData.HasStatusMessage())
{

@ -2,7 +2,7 @@
@{
ViewData["Title"] = "Register";
var useBasicLayout = ViewData["UseBasicLayout"] is true;
Layout = useBasicLayout ? "../Shared/_Layout.cshtml" : "_WelcomeLayout.cshtml";
Layout = useBasicLayout ? "_Layout" : "_LayoutWelcome";
}
@if (TempData.HasStatusMessage())
{

@ -56,9 +56,13 @@
<td style="text-align:right">
@if (app.IsOwner)
{
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a>
<span> - </span>
}
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a><span> - </span>
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a>
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id" target="_blank"
title="View in New Window"><span class="fa fa-external-link"></span></a>
<span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
</td>
</tr>

@ -215,13 +215,21 @@
</div>
<input type="hidden" asp-for="NotificationEmailWarning" />
<div class="form-group">
<button name="command" type="submit" class="btn btn-primary" value="save" id="SaveSettings">Save settings</button>
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
@if ((Model.Description != null) && (Model.Title != null) && (Model.TargetCurrency != null))
<button type="submit" class="btn btn-primary" id="SaveSettings">Save Settings</button>
<div class="btn-group">
<a class="btn btn-outline-primary" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-outline-primary" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
@if (Model.ModelWithMinimumData)
{
<button name="command" type="submit" value="viewapp" class="btn btn-secondary" id="ViewApp">View App</button>
<div class="btn-group">
<a class="btn btn-outline-primary" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View App</a>
<a class="btn btn-outline-primary" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId"
target="viewapp_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
}
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
<a class="btn btn-outline-primary" target="_blank" asp-action="ListApps">Back to the app list</a>
</div>
</form>
</div>
@ -255,13 +263,12 @@
</div>
<div class="col-sm-3">
<label>Price</label>*
<input
class="js-product-price form-control mb-2"
inputmode="numeric"
pattern="\d*"
step="any"
type="number"
value="{price}" />
<input class="js-product-price form-control mb-2"
inputmode="numeric"
pattern="\d*"
step="any"
type="number"
value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
@ -285,7 +292,7 @@
<div class="form-row">
<div class="col">
<label>Inventory (leave blank to not use inventory feature)</label>
<input type="number" step="1" class="js-product-inventory form-control mb-2" value="{inventory}"/>
<input type="number" step="1" class="js-product-inventory form-control mb-2" value="{inventory}" />
</div>
</div>
</div>

@ -145,10 +145,18 @@
</div>
<input type="hidden" asp-for="NotificationEmailWarning" />
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" id="SaveSettings" />
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary" target="_blank" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View App</a>
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
<button type="submit" class="btn btn-primary" id="SaveSettings">Save Settings</button>
<div class="btn-group">
<a class="btn btn-outline-primary" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-outline-primary" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
<div class="btn-group">
<a class="btn btn-outline-primary" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View App</a>
<a class="btn btn-outline-primary" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id"
target="viewapp_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
<a class="btn btn-outline-primary" target="_blank" asp-action="ListApps">Back to the app list</a>
</div>
<div class="form-group">
<div class="accordion" id="accordian-dev-info">
@ -253,12 +261,11 @@
</div>
<div class="col-sm-3">
<label>Price</label>*
<input
class="js-product-price form-control mb-2"
inputmode="numeric"
pattern="\d*"
type="number"
value="{price}" />
<input class="js-product-price form-control mb-2"
inputmode="numeric"
pattern="\d*"
type="number"
value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
@ -282,7 +289,7 @@
<div class="form-row">
<div class="col">
<label>Inventory (leave blank to not use inventory feature)</label>
<input type="number" step="1" class="js-product-inventory form-control mb-2" value="{inventory}"/>
<input type="number" step="1" class="js-product-inventory form-control mb-2" value="{inventory}" />
</div>
</div>
</div>

@ -33,27 +33,27 @@
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
}
</head>
<body>
@if (Context.Request.Query.ContainsKey("simple"))
{
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
}
else
{
<noscript>
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
</noscript>
if (Model.AnimationsEnabled)
@if (Context.Request.Query.ContainsKey("simple"))
{
<canvas id="fireworks"></canvas>
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
}
else
{
<noscript>
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model)
</noscript>
if (Model.AnimationsEnabled)
{
<canvas id="fireworks"></canvas>
}
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model)
}
@await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model)
}
</body>
</html>

@ -0,0 +1,15 @@
@{
ViewData["ErrorTitle"] = "404 - Page not found";
}
<p class="lead-login">
This is like searching for a person more beautiful than <a href="https://twitter.com/NicolasDorier" target="_blank">Nicolas Dorier</a>.
<br /><br />
<a href="https://twitter.com/NicolasDorier" target="_blank">
<img src="~/img/errorpages/404_nicolas.jpg" alt="Nicolas Dorier beauty" title="Slowly stroke the image" />
</a>
<br /><br />
It doesn't exist.
<br /><br />
You can always try <a href="/">navigating back to home</a>.
</p>

@ -0,0 +1,15 @@
@{
ViewData["ErrorTitle"] = "429 - Too Many Requests";
}
<p class="lead-login">
Please send requests slower. Or face the wrath of <a href="https://twitter.com/r0ckstardev" target="_blank">Vin Diesel</a>.
<br /><br />
<a href="https://twitter.com/r0ckstardev" target="_blank">
<img src="~/img/errorpages/429_rockstardev.jpg" alt="Vin is angry because you caused 429" title="Move away that cursor" />
</a>
<br /><br />
You sure you want to risk that?
<br /><br />
Slowly <a href="/">navigate back to home</a>.
</p>

@ -0,0 +1,15 @@
@{
ViewData["ErrorTitle"] = "500 - Internal Server Error";
}
<p class="lead-login">
Whoops, something really went wrong! <a href="https://twitter.com/mrkukks">Mr Kukks</a> is so sorry.
<br /><br />
<a href="https://twitter.com/mrkukks" target="_blank">
<img src="~/img/errorpages/500_mrkukks.jpg" alt="Mr Kukks puppy eyes" title="The most innocent look you'll ever see" />
</a>
<br /><br />
Consult server log and consider submitting issue on BTCPayServer GitHub.
<br /><br />
<a href="/">Navigate back to home</a>.
</p>

@ -0,0 +1,19 @@
@using System.Net
@model int?
@{
ViewData["ErrorTitle"] = "Generic Error occurred";
if (Model.HasValue)
{
var httpCode = (HttpStatusCode)Model.Value;
ViewData["ErrorTitle"] = $"{(int)httpCode} - {httpCode.ToString()}";
}
}
<p class="lead-login">
Generic error occurred, HTTP Code: @Model
<br /><br />
Consult server log for more details.
<br /><br />
<a href="/">Navigate back to home</a>.
<br /><br />
</p>

@ -0,0 +1,118 @@
@{
Layout = null;
}
@inject BTCPayServer.Services.BTCPayServerEnvironment env
<!DOCTYPE html>
<html lang="en">
<head>
<partial name="Header" />
<link href="~/main/fonts/Montserrat.css" rel="stylesheet">
<style>
.content-wrapper {
padding: 20px 0;
}
@@media screen and (min-width: 768px) {
.content-wrapper {
padding: 20px 0;
}
}
.col-head {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
text-align: center;
}
@@media screen and (min-width: 768px) {
.col-head {
text-align: left;
flex-direction: row;
justify-content: start;
}
}
.head-logo {
height: 70px;
margin-bottom: 1rem;
}
@@media screen and (min-width: 768px) {
.head-logo {
height: 100px;
margin-bottom: 0;
margin-right: 50px;
}
}
.lead-title {
font-family: Montserrat;
font-style: normal;
font-weight: bold;
font-size: 24px;
line-height: 1.2;
/* or 150% */
letter-spacing: 0.1em;
}
@@media screen and (min-width: 768px) {
.lead-title {
font-size: 40px;
}
}
.lead-login {
font-family: Montserrat;
font-style: normal;
font-weight: normal;
font-size: 18px;
line-height: 33px;
/* or 183% */
letter-spacing: 0.1em;
}
.lead-h {
font-family: Montserrat;
font-style: normal;
margin-bottom: 30px;
font-weight: 600;
font-size: 14px;
line-height: 18px;
/* identical to box height, or 129% */
letter-spacing: 0.1em;
text-transform: uppercase;
}
</style>
</head>
<body>
<center>
<section class="content-wrapper">
<!-- Dummy navbar-brand, hackish way to keep test AssertNoError passing -->
<div class="navbar-brand" style="display:none;"></div>
<div class="container">
<div class="row">
<div class="col-12 col-head" style="justify-content:center;">
<a asp-controller="Home" asp-action="Index"><img src="~/img/btcpay-logo.svg" alt="BTCPay Server" class="head-logo" /></a>
<h1 class="lead-title text-uppercase">@ViewData["ErrorTitle"]</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<hr class="primary ml-0" style="margin:20px auto;" />
@RenderBody()
</div>
</div>
<div class="row">
<div class="col-12">
<hr class="primary ml-0" style="margin:20px auto;" />
@await Html.PartialAsync("_BTCPaySupporters")
</div>
</div>
</div>
</section>
</center>
</body>
</html>

@ -0,0 +1,3 @@
@{
Layout = "_LayoutError";
}

@ -3,12 +3,11 @@
<div class="row">
<div class="col-md-12 invoice-payments">
<h3>Paid summary</h3>
<h3>Current status</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th>Payment method</th>
<th>Address</th>
<th class="text-right">Rate</th>
<th class="text-right">Paid</th>
<th class="text-right">Due</th>
@ -23,9 +22,6 @@
{
<tr>
<td>@payment.PaymentMethod</td>
<td title="@payment.Address">
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
</td>
<td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td>

@ -0,0 +1,165 @@
@model LndServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>C-Lightning @Model.ConnectionType</h4>
<partial name="_StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes Clightning-Rest's service for outside consumption, you will find connection information here.<br /></span>
</p>
</div>
<div class="form-group">
<h5>Compatible wallets</h5>
</div>
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://github.com/ZeusLN/zeus" target="_blank">
<img src="~/img/zeus.jpg" height="100" />
</a>
<p><a href="https://github.com/ZeusLN/zeus" target="_blank">Zeus</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use this QR Code to connect external software to your C-Lightning instance.<br /></span>
<span>This QR Code is only valid for 10 minutes</span>
</p>
</div>
<div class="form-group">
@if (Model.QRCode == null)
{
<div class="form-group">
<form method="post">
<button type="submit" class="btn btn-primary">Show QR Code</button>
</form>
</div>
}
else
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Model.QRCode"></div>
</div>
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
<div id="detailsQR" class="collapse">
<div class="form-group">
<label>QR Code data</label>
<input asp-for="QRCode" readonly class="form-control" />
</div>
<div class="form-group">
Click <a href="@Model.QRCodeLink" target="_blank">here</a> to open the configuration file.
</div>
</div>
}
<div class="form-group">
<h5>More details...</h5>
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
</div>
<div id="details" class="collapse">
@if (Model.Uri == null)
{
<div class="form-group">
<label asp-for="Host"></label>
<input asp-for="Host" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="SSL"></label>
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
</div>
}
else
{
<div class="form-group">
<label asp-for="Uri"></label>
<input asp-for="Uri" readonly class="form-control" />
</div>
}
@if (Model.Macaroon != null)
{
<div class="form-group">
<label asp-for="Macaroon"></label>
<input asp-for="Macaroon" readonly class="form-control" />
</div>
}
@if (Model.AdminMacaroon != null)
{
<div class="form-group">
<label asp-for="AdminMacaroon"></label>
<input asp-for="AdminMacaroon" readonly class="form-control" />
</div>
}
@if (Model.InvoiceMacaroon != null)
{
<div class="form-group">
<label asp-for="InvoiceMacaroon"></label>
<input asp-for="InvoiceMacaroon" readonly class="form-control" />
</div>
}
@if (Model.ReadonlyMacaroon != null)
{
<div class="form-group">
<label asp-for="ReadonlyMacaroon"></label>
<input asp-for="ReadonlyMacaroon" readonly class="form-control" />
</div>
}
@if (Model.GRPCSSLCipherSuites != null)
{
<div class="form-group">
<label asp-for="GRPCSSLCipherSuites"></label>
<input asp-for="GRPCSSLCipherSuites" readonly class="form-control" />
</div>
}
@if (Model.CertificateThumbprint != null)
{
<div class="form-group">
<label asp-for="CertificateThumbprint"></label>
<input asp-for="CertificateThumbprint" readonly class="form-control" />
</div>
}
</div>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
@if(Model.QRCode != null)
{
<script type="text/javascript" src="~/js/qrcode.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: @Safe.Json(Model.QRCode),
width: 200,
height: 200,
useSVG: true
});
</script>
}
}

@ -0,0 +1,33 @@
@model LightningWalletServices
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>BTCPay Server Configurator</h4>
<partial name="_StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>This page exposes information to use the configured BTCPay Server Configurator to modify this setup.</span>
</p>
</div>
<a href="@Model.ServiceLink" target="_blank" class="form-group">
<label>Service</label>
<input asp-for="ServiceLink" readonly class="form-control" />
</a>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -16,7 +16,7 @@
This is recommended if you are hosting BTCPayServer at home and wish to have a clearnet HTTPS address to access your server.
</span>
</p>
<p>Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate. Check the documentation for <a href="https://docs.btcpayserver.org/features/dynamicdns" target="_blank">more information</a>.</p>
<p>Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate. Check the documentation for <a href="https://docs.btcpayserver.org/deployment/advanced-deployment/dynamicdns" target="_blank">more information</a>.</p>
</div>
<form method="post" asp-action="DynamicDnsService">
<button id="AddDynamicDNS" class="btn btn-primary" type="submit"><span class="fa fa-plus"></span> Add Dynamic DNS</button>

@ -29,7 +29,7 @@
</tr>
</thead>
<tbody>
@foreach (var s in Model.ExternalServices)
@foreach (var s in Model.ExternalServices.Where(service => !string.IsNullOrEmpty(service.CryptoCode)))
{
<tr>
<td>@s.CryptoCode</td>
@ -52,7 +52,7 @@
</div>
</div>
@if (Model.OtherExternalServices.Count != 0)
@if (Model.OtherExternalServices.Count != 0 || Model.ExternalServices.Any(service => string.IsNullOrEmpty(service.CryptoCode)))
{
<div class="row">
<div class="col-md-8">
@ -69,6 +69,15 @@
</tr>
</thead>
<tbody>
@foreach (var s in Model.ExternalServices.Where(service => string.IsNullOrEmpty(service.CryptoCode)))
{
<tr>
<td>@s.DisplayName</td>
<td style="text-align: right">
<a asp-action="Service" asp-route-serviceName="@s.ServiceName">See information</a>
</td>
</tr>
}
@foreach (var s in Model.OtherExternalServices)
{
<tr>

@ -18,7 +18,9 @@
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="" data-Server="smtp.gmail.com" data-Port="587" data-EnableSSL="true">Gmail.com</a>
<a class="dropdown-item" href="" data-Server="mail.yahoo.com" data-Port="587" data-EnableSSL="true">Yahoo.com</a>
<a class="dropdown-item" href="" data-Server="smtp.mailgun.org" data-Port="587" data-EnableSSL="true">Mailgun</a>
<a class="dropdown-item" href="" data-Server="smtp.office365.com" data-Port="587" data-EnableSSL="true">Office365</a>
<a class="dropdown-item" href="" data-Server="smtp.sendgrid.net" data-Port="587" data-EnableSSL="true">SendGrid</a>
</div>
</div>
<p></p>

@ -6,7 +6,7 @@
<html>
<head></head>
<body>
<form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController">
<form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-route-walletId="@this.Context.GetRouteValue("walletId").ToString()">
@foreach(var o in Model.Parameters) {
<input type="hidden" name="@o.Key" value="@o.Value" />
}

@ -14,7 +14,7 @@
<p>
Your node is synching the entire blockchain and validating the consensus rules...
</p>
@foreach (var line in dashboard.GetAll())
@foreach (var line in dashboard.GetAll().Where(summary => summary.Network.ShowSyncSummary))
{
<h4>@line.Network.CryptoCode</h4>
@if (line.Status == null)

@ -0,0 +1,41 @@
<h3 class="lead-h">BTCPayServer Supporters <a href="https://foundation.btcpayserver.org/" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a></h3>
<div class="figure">
<a href="https://twitter.com/sqcrypto" target="_blank">
<img src="~/img/squarecrypto.svg" alt="Sponsor Square Crypto" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://twitter.com/sqcrypto" class="text-muted small" target="_blank">Square Crypto</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://www.dglab.com/en/" target="_blank">
<img src="~/img/dglab.svg" alt="Sponsor DG lab" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://www.dglab.com/en/" class="text-muted small" target="_blank">DG Lab</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://acinq.co/" target="_blank">
<img src="~/img/acinq-logo.svg" alt="Sponsor ACINQ" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://acinq.co/" class="text-muted small" target="_blank">ACINQ</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://lunanode.com/" target="_blank">
<img src="~/img/lunanode.svg" alt="Sponsor LunaNode" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://lunanode.com/" class="text-muted small" target="_blank">LunaNode</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://walletofsatoshi.com/" target="_blank">
<img src="~/img/walletofsatoshi.svg" alt="Sponsor Wallet of Satoshi" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://walletofsatoshi.com/" class="text-muted small" target="_blank">Wallet of Satoshi</a>
</div>
</div>

@ -102,47 +102,7 @@
<div class="col-md-7 order-md-1 order-2">
<hr class="primary ml-0" style="margin:30px auto;">
<p class="lead-login" style="margin-bottom:69px;">BTCPay Server is a self-hosted, open-source cryptocurrency payment processor. It is secure, private, censorship-resistant and free.</p>
<h3 class="lead-h">Our supporters <a href="https://foundation.btcpayserver.org/" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a></h3>
<div class="figure">
<a href="https://twitter.com/sqcrypto" target="_blank">
<img src="~/img/squarecrypto.svg" alt="Sponsor Square Crypto" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://twitter.com/sqcrypto" class="text-muted small" target="_blank">Square Crypto</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://www.dglab.com/en/" target="_blank">
<img src="~/img/dglab.svg" alt="Sponsor DG lab" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://www.dglab.com/en/" class="text-muted small" target="_blank">DG Lab</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://acinq.co/" target="_blank">
<img src="~/img/acinq-logo.svg" alt="Sponsor ACINQ" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://acinq.co/" class="text-muted small" target="_blank">ACINQ</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://lunanode.com/" target="_blank">
<img src="~/img/lunanode.svg" alt="Sponsor LunaNode" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://lunanode.com/" class="text-muted small" target="_blank">LunaNode</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://walletofsatoshi.com/" target="_blank">
<img src="~/img/walletofsatoshi.svg" alt="Sponsor Wallet of Satoshi" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://walletofsatoshi.com/" class="text-muted small" target="_blank">Wallet of Satoshi</a>
</div>
</div>
@await Html.PartialAsync("_BTCPaySupporters")
</div>
<div class="col-md-5 order-md-2 order-1">
@RenderBody()

@ -81,7 +81,7 @@
Import from...
</button>
<div class="dropdown-menu dropdown-menu-right">
<button class="dropdown-item" type="button">... Coldcard (air gap)</button>
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#coldcardimport">... Coldcard (air gap)</button>
<button class="dropdown-item check-for-ledger" type="button">... Ledger Wallet</button>
@if (Model.CryptoCode == "BTC")
{

@ -63,7 +63,7 @@
<div class="modal-dialog" role="document">
<form class="modal-content" form method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Import Coldcard Wallet</h5>
<h5 class="modal-title" id="coldcardimportLabel">Import Coldcard Wallet</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>

@ -20,7 +20,9 @@
<label asp-for="ExistingMnemonic">Existing Seed</label>
<input type="text" asp-for="ExistingMnemonic" class="form-control" />
<span asp-validation-for="ExistingMnemonic" class="text-danger"></span>
<p class="text-black-50">You can choose to import an existing mnemonic seed phrase. If you leave blank, we will generate one for you.</p>
<small class="form-text text-muted">
You can choose to import an existing mnemonic seed phrase. If you leave blank, we will generate one for you.
</small>
</div>
<div class="form-group">
<label asp-for="Passphrase">Passphrase (optional)</label>
@ -56,14 +58,17 @@
<input type="checkbox" class="form-check-inline" asp-for="SavePrivateKeys" />
<label asp-for="SavePrivateKeys">Is hot wallet</label>
<span asp-validation-for="SavePrivateKeys" class="text-danger"></span>
<p class="text-danger">If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds.</p>
<small class="form-text text-danger">
If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds.
</small>
</div>
<div class="form-group">
<input type="checkbox" class="form-check-inline" asp-for="ImportKeysToRPC" />
<label asp-for="ImportKeysToRPC">Import keys to RPC</label>
<span asp-validation-for="ImportKeysToRPC" class="text-danger"></span>
<p class="text-black-50">If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside <code>Is hot wallet</code>, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded).</p>
<small class="form-text text-muted">
If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside <code>Is hot wallet</code>, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded).
</small>
</div>
</div>
<div class="modal-footer">

@ -19,7 +19,7 @@
<div class="form-group">
<h5>Scripting</h5>
<p>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</p>
<p>We are retrieving the rate of each exchange either directly, via <a href="https://www.coingecko.com/" target="_blank">CoinGecko (free)</a> or <a href="https://bitcoinaverage.com/" target="_blank">BitcoinAverage (commercial)</a></p>
<p>We are retrieving the rate of each exchange either directly, via <a href="https://www.coingecko.com/" target="_blank">CoinGecko (free)</a>.</p>
<div class="accordion" id="accordion-info">
<div class="card">
<div class="card-header" id="direct-header">

@ -97,9 +97,9 @@
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.DerivationSchemes)
@foreach(var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed))
{
<tr>
<tr class="@(@scheme.Collapsed? "collapsed": "")">
<td>@scheme.Crypto</td>
<td class="smMaxWidth text-truncate">@scheme.Value</td>
<td style="text-align:center;">
@ -121,6 +121,13 @@
</td>
</tr>
}
@if (Model.DerivationSchemes.Any(scheme => scheme.Collapsed))
{
<tr class="only-for-js">
<td colspan="4"><button class="btn btn-link" id="toggle-assets" type="button">Show additional assets</button></td>
</tr>
}
</tbody>
</table>
</div>
@ -250,4 +257,14 @@
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script>
$(document).ready(function(){
$(".collapsed").hide();
$("#toggle-assets").click(function() {
$(".collapsed").show();
$(this).parents("tr").hide();
});
});
</script>
}

@ -28,8 +28,9 @@
{
<h3>Decoded PSBT</h3>
<div class="form-group">
<form method="post" asp-action="WalletPSBT">
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" asp-for="NBXSeedAvailable" />
<input type="hidden" asp-for="PSBT" />
<input type="hidden" asp-for="FileName" />
<div class="dropdown d-inline-block" style="margin-top:16px;">
@ -43,6 +44,10 @@
@if (Model.CryptoCode == "BTC") {
<button name="command" type="submit" class="dropdown-item" value="vault">... the vault (preview)</button>
}
@if (Model.NBXSeedAvailable)
{
<button name="command" type="submit" class="dropdown-item" value="nbx-seed">... the seed saved in NBXplorer</button>
}
</div>
</div>
<div class="dropdown d-inline-block">
@ -60,7 +65,7 @@
<pre><code class="json">@Model.Decoded</code></pre>
}
<h3>PSBT to decode</h3>
<form class="form-group" method="post" asp-action="WalletPSBT" enctype="multipart/form-data">
<form class="form-group" method="post" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")" enctype="multipart/form-data">
<div class="form-group">
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
<span asp-validation-for="PSBT" class="text-danger"></span>

@ -2,13 +2,24 @@
@{
Layout = "../Shared/_Layout.cshtml";
}
<section>
<div class="container">
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage"/>
</div>
</div>
}
@if (Model.GlobalError != null)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>@Model.GlobalError</span><br />
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<span>@Model.GlobalError</span><br/>
</div>
}
<div class="row">
@ -18,14 +29,16 @@
@if (Model.CanCalculateBalance)
{
<p>
If you broadcast this transaction, your balance will change: @if (Model.Positive)
If you broadcast this transaction, your balance will change:
@if (Model.Positive)
{
<span style="color:green;">@Model.BalanceChange</span>
}
else
{
<span style="color:red;">@Model.BalanceChange</span>
}, do you want to continue?
}
, do you want to continue?
</p>
}
else
@ -40,36 +53,38 @@
<h4 class="text-left">Inputs</h4>
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th style="text-align:left" class="col-md-auto">
Index
</th>
<th style="text-align:right">Amount</th>
</tr>
<tr>
<th style="text-align:left" class="col-md-auto">
Index
</th>
<th style="text-align:right">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var input in Model.Inputs)
{
<tr>
@if (input.Error != null)
{
<td style="text-align:left">@input.Index <span class="fa fa-exclamation-triangle" style="color:red;" title="@input.Error"></span></td>
}
else
{
<td style="text-align:left">@input.Index</td>
}
@foreach (var input in Model.Inputs)
{
<tr>
@if (input.Error != null)
{
<td style="text-align:left">
@input.Index <span class="fa fa-exclamation-triangle" style="color:red;" title="@input.Error"></span>
</td>
}
else
{
<td style="text-align:left">@input.Index</td>
}
@if (input.Positive)
{
<td style="text-align:right; color:green;">@input.BalanceChange</td>
}
else
{
<td style="text-align:right; color:red;">@input.BalanceChange</td>
}
</tr>
}
@if (input.Positive)
{
<td style="text-align:right; color:green;">@input.BalanceChange</td>
}
else
{
<td style="text-align:right; color:red;">@input.BalanceChange</td>
}
</tr>
}
</tbody>
</table>
</div>
@ -82,28 +97,28 @@
<h4 class="text-left">Outputs</h4>
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th style="text-align:left" class="col-md-auto">
Destination
</th>
<th style="text-align:right">Amount</th>
</tr>
<tr>
<th style="text-align:left" class="col-md-auto">
Destination
</th>
<th style="text-align:right">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var destination in Model.Destinations)
{
<tr>
<td style="text-align:left">@destination.Destination</td>
@if (destination.Positive)
{
<td style="text-align:right; color:green;">@destination.Balance</td>
}
else
{
<td style="text-align:right; color:red;">@destination.Balance</td>
}
</tr>
}
@foreach (var destination in Model.Destinations)
{
<tr>
<td style="text-align:left">@destination.Destination</td>
@if (destination.Positive)
{
<td style="text-align:right; color:green;">@destination.Balance</td>
}
else
{
<td style="text-align:right; color:red;">@destination.Balance</td>
}
</tr>
}
</tbody>
</table>
</div>
@ -122,12 +137,13 @@
<div class="row">
<div class="col-lg-12 text-center">
<form method="post" asp-action="WalletPSBTReady" asp-route-walletId="@this.Context.GetRouteValue("walletId")">
<input type="hidden" asp-for="PSBT" />
<input type="hidden" asp-for="SigningKey" />
<input type="hidden" asp-for="SigningKeyPath" />
<input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="SigningKey"/>
<input type="hidden" asp-for="SigningKeyPath"/>
@if (!Model.HasErrors)
{
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button> <span> or </span>
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button>
<span> or </span>
}
<button type="submit" class="btn btn-secondary" name="command" value="analyze-psbt">Export as PSBT</button>
</form>

@ -5,16 +5,25 @@
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" />
</div>
</div>
}
<div class="row">
<div class="@(Model.Outputs.Count==1? "col-lg-6 transaction-output-form": "col-lg-8")">
<form method="post">
<input type="hidden" asp-for="Divisibility" />
<input type="hidden" asp-for="NBXSeedAvailable" />
<input type="hidden" asp-for="Fiat" />
<input type="hidden" asp-for="Rate" />
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" name="BIP21" id="BIP21" />
<ul class="text-danger">
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
{
@ -117,6 +126,7 @@
</div>
</div>
}
<div class="card">
<button class="btn btn-light collapsed" type="button" data-toggle="collapse" data-target="#accordian-advanced" aria-expanded="false" aria-controls="accordian-advanced">
Advanced settings
@ -155,9 +165,14 @@
@if (Model.CryptoCode == "BTC") {
<button name="command" type="submit" class="dropdown-item" value="vault">... the vault (preview)</button>
}
@if (Model.NBXSeedAvailable)
{
<button name="command" type="submit" class="dropdown-item" value="nbx-seed">... the seed saved in NBXplorer</button>
}
</div>
</div>
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination </button>
<button type="button" id="bip21parse" class="ml-1 btn btn-secondary">Parse BIP21</button>
</div>
</form>
</div>

@ -6,14 +6,22 @@
}
<h4>Sign the transaction with Ledger</h4>
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" />
</div>
</div>
}
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div class="col-md-10">
<form id="broadcastForm" asp-action="WalletPSBTReady" asp-route-walletId="@this.Context.GetRouteValue("walletId")" method="post" style="display:none;">
<input type="hidden" id="PSBT" asp-for="PSBT" />
<form id="broadcastForm" asp-action="SubmitLedger" asp-route-walletId="@this.Context.GetRouteValue("walletId")" method="post" style="display:none;">
<input type="hidden" id="PSBT" asp-for="PSBT" value="@Model.PSBT"/>
<input type="hidden" asp-for="HintChange" />
<input type="hidden" asp-for="WebsocketPath" />
</form>

@ -6,15 +6,23 @@
}
<h4>Sign the transaction with BTCPayServer Vault</h4>
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" />
</div>
</div>
}
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div id="body" class="col-md-10">
<form id="broadcastForm" asp-action="WalletPSBTReady" asp-route-walletId="@this.Context.GetRouteValue("walletId")" method="post" style="display:none;">
<form id="broadcastForm" asp-action="SubmitVault" asp-route-walletId="@this.Context.GetRouteValue("walletId")" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" id="PSBT" asp-for="PSBT" />
<input type="hidden" id="PSBT" asp-for="PSBT" value="@Model.PSBT"/>
<input type="hidden" asp-for="WebsocketPath" />
</form>
<div id="vaultPlaceholder"></div>

@ -57,7 +57,7 @@
}
<div class="row">
<div class="col-md-12">
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan">rescan your wallet</a>. <br />
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan" asp-route-walletId="@this.Context.GetRouteValue("walletId")">rescan your wallet</a>. <br />
If some transactions appear in BTCPay Server, but are missing on Electrum or another wallet, <a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-wallet#missing-payments-in-my-software-or-hardware-wallet">follow those instructions</a>.
</div>
</div>

Binary file not shown.

After

(image error) Size: 14 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

After

(image error) Size: 18 KiB

Binary file not shown.

After

(image error) Size: 31 KiB

Binary file not shown.

Before

(image error) Size: 11 KiB

After

(image error) Size: 10 KiB

@ -44,4 +44,12 @@ $(function () {
updateFiatValue(outputAmountElement);
return false;
});
$("#bip21parse").on("click", function(){
var bip21 = prompt("Paste BIP21 here");
if(bip21){
$("#BIP21").val(bip21);
$("form").submit();
}
});
});

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.0.3.153</Version>
<Version>1.0.3.158</Version>
</PropertyGroup>
</Project>

@ -11,11 +11,11 @@
BTCPay Server is a free and open-source cryptocurrency payment processor which allows you to receive payments in Bitcoin and altcoins directly, with no fees, transaction cost or a middleman.
BTCPay is a non-custodial invoicing system which eliminates the involvement of a third-party. Payments with BTCPay go directly to your wallet, which increases the privacy and security. Your private keys are never uploaded to the server. There is no address re-use, since each invoice generates a new address deriving from your xpubkey.
BTCPay Server is a non-custodial invoicing system which eliminates the involvement of a third-party. Payments with BTCPay Server go directly to your wallet, which increases the privacy and security. Your private keys are never uploaded to the server. There is no address re-use, since each invoice generates a new address deriving from your xpubkey.
The software is built in C# and conforms to the invoice [API of BitPay](https://bitpay.com/api). It allows for your website to be easily migrated from BitPay and configured as a self-hosted payment processor.
You can run BTCPay as a self-hosted solution on your own server, or use a [third-party host](https://github.com/btcpayserver/btcpayserver-doc/blob/master/ThirdPartyHosting.md).
You can run BTCPay Server as a self-hosted solution on your own server, or use a [third-party host](https://github.com/btcpayserver/btcpayserver-doc/blob/master/ThirdPartyHosting.md).
The self-hosted solution allows you not only to attach an unlimited number of stores and use the Lightning Network but also become the payment processor for others.
@ -36,25 +36,26 @@ Thanks to the [apps](https://github.com/btcpayserver/btcpayserver-doc/blob/maste
## Features
* Direct, peer-to-peer Bitcoin and altcoin payments
* No transaction fees (other than those for the crypto networks)
* Direct, peer-to-peer Bitcoin payments
* No transaction fees (other than the [network fee](https://en.bitcoin.it/wiki/Miner_fees))
* No processing fees
* No middleman
* No KYC
* User has complete control over private keys
* Non-custodial (complete control over the private key)
* Enhanced privacy
* Enhanced security
* Self-hosted
* SegWit support
* Lightning Network support (LND and c-lightning)
* Lightning Network support (LND, c-lightning, Eclair and Ptarmigan)
* Tor support
* Opt-in [altcoin](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Altcoins.md) integrations
* Full compatibility with BitPay API (easy migration)
* Process payments for others
* Easy-embeddable Payment buttons
* Point of sale app
* Crowdfunding app
* Payment requests
* Internal Web Wallet
* Payment Requests
* Internal, full-node reliant wallet with [hardware wallet integration](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Vault.md)
## Getting Started
@ -71,6 +72,7 @@ If you have trouble using BTCPay, consider joining [communities listed on offici
Main community chat is located on [Mattermost](https://chat.btcpayserver.org/).
## Contributing
BTCPay is built and maintained entirely by volunteer contributors around the internet. We welcome and appreciate new contributions.
If you're a developer looking to help, but you're not sure where to begin, check the [good first issue label](https://github.com/btcpayserver/btcpayserver/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), which contains small pieces of work that have been specifically flagged as being friendly to new contributors.
@ -139,6 +141,10 @@ For more information, see the documentation: [How to deploy a BTCPay server inst
Bitcoin is the only focus of the project and its core developers. However, opt in integrations are present for [several altcoins](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Altcoins.md). Altcoins are maintained by their respective communities.
## License
BTCPay Server software, logo and designs are provided under [MIT License](https://github.com/btcpayserver/btcpayserver/blob/master/LICENSE).
## Supporters
The BTCPay Server Project is proudly supported by these entities through the [BTCPay Server Foundation](https://foundation.btcpayserver.org/).