Compare commits

..

3 Commits

49 changed files with 408 additions and 574 deletions

View File

@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.7" />
<PackageReference Include="NBitcoin" Version="6.0.6" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

View File

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.0.3" />
<PackageReference Include="NBXplorer.Client" Version="4.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>

View File

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="6.0.7" />
<PackageReference Include="NBitcoin" Version="6.0.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

View File

@ -993,83 +993,103 @@ normal:
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))"));
//let's see what we actually support now
void AssertOuptutDescriptor(string descriptor, string expectedKeyPath, string expectedFingerprint, string expectedDerivationScheme)
{
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(descriptor);
Assert.Equal(KeyPath.Parse(expectedKeyPath), Assert.Single(parsedDescriptor.AccountKeySettings).AccountKeyPath);
Assert.Equal(HDFingerprint.Parse(expectedFingerprint), Assert.Single(parsedDescriptor.AccountKeySettings).RootFingerprint);
Assert.Equal(expectedDerivationScheme, parsedDescriptor.AccountDerivation.ToString());
Assert.Contains(parsedDescriptor.GetOutputDescriptors(), o =>
{
var outputStr = o.ToString();
// Remove checksum, tests don't have it
outputStr = outputStr.Substring(0, outputStr.Length - 9);
return outputStr == descriptor;
});
}
//standard legacy hd wallet
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal(KeyPath.Parse("44'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)",
expectedKeyPath: "44'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]"
);
//masterfingerprint and key path are optional
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
//a master fingerprint must always be present if youre providing rooted path
Assert.Throws<ParsingException>(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
//but a different deriv path from standard (0/*) is not supported
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
//p2sh-segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(wpkh([d34db33f/49'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(KeyPath.Parse("49'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[p2sh]",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "sh(wpkh([d34db33f/49'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))",
expectedKeyPath: "49'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[p2sh]"
);
//segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"wpkh([d34db33f/84'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal(KeyPath.Parse("84'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "wpkh([d34db33f/84'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)",
expectedKeyPath: "84'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
);
//multisig tests
//legacy
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(multi(1,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
var strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.Item1).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
var strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner);
Assert.True(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]-[keeporder]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]-[keeporder]",parsedDescriptor.AccountDerivation.ToString() );
//segwit
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(parsedDescriptor.Item1).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner);
Assert.False(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]",parsedDescriptor.AccountDerivation.ToString() );
//segwit-p2sh
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.Item1).Inner).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner).Inner);
Assert.False(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]-[p2sh]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]-[p2sh]",parsedDescriptor.AccountDerivation.ToString() );
//sorted
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(sortedmulti(1,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
}
}
}

View File

@ -1081,7 +1081,7 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-confirm-payment")).Click();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("InProgress-view")).Click();

View File

@ -13,7 +13,6 @@ using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
@ -186,23 +185,22 @@ namespace BTCPayServer.Tests
fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent));
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFiles(fileList));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds"));
string[] uploadFileList = (string[])uploadFormFileResult.RouteValues["fileIds"];
var fileId = uploadFileList[0];
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
//check if file was uploaded and saved in db
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId));
Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]);
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
//verify file is available and the same
var net = new System.Net.WebClient();
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId]));
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
Assert.Equal(fileContent, data);
//create a temporary link to file
@ -234,8 +232,10 @@ namespace BTCPayServer.Tests
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}

View File

@ -69,7 +69,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -84,7 +84,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.56
image: nicolasdorier/nbxplorer:2.1.55
restart: unless-stopped
ports:
- "32838:32838"
@ -118,7 +118,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -146,7 +146,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -195,7 +195,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -66,7 +66,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -81,7 +81,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.56
image: nicolasdorier/nbxplorer:2.1.55
restart: unless-stopped
ports:
- "32838:32838"
@ -105,7 +105,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -133,7 +133,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -182,7 +182,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -46,7 +46,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.0" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.1" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.10" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.9" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />

View File

@ -339,7 +339,7 @@ namespace BTCPayServer.Controllers
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Wallet > Manage > Payouts.",
Html = "Share this page with a customer so they can claim a refund <br />Once claimed you need to initiate a refund from Wallet > Payouts",
Severity = StatusMessageModel.StatusSeverity.Success
});
(await ctx.Invoices.FindAsync(invoice.Id)).CurrentRefundId = ppId;

View File

@ -22,51 +22,24 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
public partial class ServerController
{
[HttpGet("server/files")]
public async Task<IActionResult> Files([FromQuery] string[] fileIds = null)
[HttpGet("server/files/{fileId?}")]
public async Task<IActionResult> Files(string fileId = null)
{
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
var model = new ViewFilesViewModel()
{
Files = await _StoredFileRepository.GetFiles(),
DirectUrlByFiles = null,
Files = await _StoredFileRepository.GetFiles(),
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
DirectFileUrl = fileUrl,
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
};
if (fileIds != null && fileIds.Length > 0)
{
bool allFilesExist = true;
Dictionary<string, string> directUrlByFiles = new Dictionary<string, string>();
foreach (string filename in fileIds)
{
string fileUrl = await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), filename);
if (fileUrl == null)
{
allFilesExist = false;
break;
}
directUrlByFiles.Add(filename, fileUrl);
}
if (!allFilesExist)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Some of the files were not found",
Severity = StatusMessageModel.StatusSeverity.Warning,
});
}
else
{
model.DirectUrlByFiles = directUrlByFiles;
}
}
return View(model);
}
@ -78,7 +51,7 @@ namespace BTCPayServer.Controllers
await _FileService.RemoveFile(fileId, null);
return RedirectToAction(nameof(Files), new
{
fileIds = Array.Empty<string>(),
fileId = "",
statusMessage = "File removed"
});
}
@ -154,7 +127,7 @@ namespace BTCPayServer.Controllers
});
return RedirectToAction(nameof(Files), new
{
fileIds = new string[] { fileId }
fileId
});
}
@ -217,10 +190,18 @@ namespace BTCPayServer.Controllers
Severity = statusMessageSeverity
});
return RedirectToAction(nameof(Files), new
{
fileIds = fileIds.ToArray(),
});
if (fileIds.Count == 1)
{
return RedirectToAction(nameof(Files), new
{
statusMessage = "File added!",
fileId = fileIds[0]
});
}
else
{
return RedirectToAction(nameof(Files));
}
}
else
{

View File

@ -455,7 +455,7 @@ namespace BTCPayServer.Controllers
[Route("server/services/{serviceName}/{cryptoCode?}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, ulong? nonce = null)
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
var service = GetService(serviceName, cryptoCode);
if (service == null)
@ -603,7 +603,7 @@ namespace BTCPayServer.Controllers
return View(nameof(LightningChargeServices), vm);
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, ulong? nonce, string view = nameof(LndServices))
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce, string view = nameof(LndServices))
{
var model = new LndServicesViewModel();
if (service.Type == ExternalServiceTypes.LNDGRPC)
@ -645,14 +645,14 @@ namespace BTCPayServer.Controllers
return View(view, model);
}
private static ulong GetConfigKey(string type, string serviceName, string cryptoCode, ulong nonce)
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
{
return ((ulong)(uint)HashCode.Combine(type, serviceName, cryptoCode, nonce) | (nonce & 0xffffffff00000000UL));
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
[AllowAnonymous]
public IActionResult GetLNDConfig(ulong configKey)
public IActionResult GetLNDConfig(uint configKey)
{
var conf = _LnConfigProvider.GetConfig(configKey);
if (conf == null)
@ -712,7 +712,7 @@ namespace BTCPayServer.Controllers
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt64();
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });

View File

@ -689,16 +689,7 @@ namespace BTCPayServer.Controllers
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = network;
var result = parser.ParseOutputDescriptor(derivationScheme);
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network)
}).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()];
return derivationSchemeSettings;
return parser.ParseOutputDescriptor(derivationScheme);
}
catch (Exception)
{

View File

@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner,
HintWalletWarning = blob.Hints.Wallet && blob.Hints.Lightning
HintWalletWarning = blob.Hints.Wallet
});
}
return View(result);

View File

@ -204,18 +204,10 @@ namespace BTCPayServer.Controllers
});
}
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
var handler = _payoutHandlers
.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
if (handler != null)
{
var result = await handler.DoSpecificAction(command, payoutIds, walletId.StoreId);
if (result != null)
{
TempData.SetStatusMessageModel(result);
}
}
switch (command)
{
case "approve-pay":
case "approve":
{
@ -272,7 +264,8 @@ namespace BTCPayServer.Controllers
{
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
}
case "pay":
@ -297,10 +290,7 @@ namespace BTCPayServer.Controllers
}
if(bip21.Any())
{
TempData.SetStatusMessageModel(null);
return RedirectToAction(nameof(WalletSend), new {walletId, bip21});
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
@ -347,7 +337,8 @@ namespace BTCPayServer.Controllers
{
Message = "Payouts marked as paid", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
}
case "cancel":
@ -357,10 +348,25 @@ namespace BTCPayServer.Controllers
{
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
}
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
var handler = _payoutHandlers
.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
if (handler != null)
{
var result = await handler.DoSpecificAction(command, payoutIds, walletId.StoreId);
TempData.SetStatusMessageModel(result);
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
return NotFound();
}
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId,

View File

@ -630,23 +630,16 @@ namespace BTCPayServer.Controllers
}
transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;
var inputName =
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
try
{
var address = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
if (address is TaprootAddress)
{
var supportTaproot = _dashboard.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanSupportTaproot;
if (!(supportTaproot is true))
{
ModelState.AddModelError(inputName, "You need to update your full node, and/or NBXplorer (Version >= 2.1.56) to be able to send to a taproot address.");
}
}
BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
}
catch
{
var inputName =
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
ModelState.AddModelError(inputName, "Invalid address");
}
@ -1099,6 +1092,7 @@ namespace BTCPayServer.Controllers
UriScheme = derivationSchemeSettings.Network.UriScheme,
Label = derivationSchemeSettings.Label,
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
OutputDescriptors = derivationSchemeSettings.GetOutputDescriptors().Select(d => d.ToString()).ToArray(),
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal,
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString(),
NBXSeedAvailable = derivationSchemeSettings.IsHotWallet &&

View File

@ -70,11 +70,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
destination = destination.Trim();
try
{
// This doesn't work properly, (payouts are not detected), we can reactivate later when we fix the bug https://github.com/btcpayserver/btcpayserver/issues/2765
//if (destination.StartsWith($"{network.UriScheme}:", StringComparison.OrdinalIgnoreCase))
//{
// return Task.FromResult<IClaimDestination>(new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)));
//}
if (destination.StartsWith($"{network.UriScheme}:", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<IClaimDestination>(new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)));
}
return Task.FromResult<IClaimDestination>(new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)));
}
@ -144,6 +143,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{
{PayoutState.AwaitingPayment, new List<(string Action, string Text)>()
{
("confirm-payment", "Confirm payouts as paid"),
("reject-payment", "Reject payout transaction")
}}
};
@ -153,7 +153,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{
switch (action)
{
case "mark-paid":
case "confirm-payment":
await using (var context = _dbContextFactory.CreateContext())
{
var payouts = (await context.Payouts

View File

@ -20,9 +20,23 @@ namespace BTCPayServer
BtcPayNetwork = expectedNetwork;
}
public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str)
public DerivationSchemeSettings ParseOutputDescriptor(string str)
{
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
DerivationSchemeSettings CreateDerivationSchemeSettings(DerivationStrategyBase derivationStrategy, RootedKeyPath[] origins)
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = BtcPayNetwork;
derivationSchemeSettings.AccountOriginal = str.Trim();
derivationSchemeSettings.AccountDerivation = derivationStrategy;
derivationSchemeSettings.AccountKeySettings = origins?.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = derivationStrategy.GetExtPubKeys().ElementAt(i).GetWif(Network)
}).ToArray() ?? new AccountKeySettings[derivationStrategy.GetExtPubKeys().Count()];
return derivationSchemeSettings;
}
DerivationSchemeSettings ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
string suffix = "")
{
switch (pubKeyProvider)
@ -35,10 +49,10 @@ namespace BTCPayServer
throw new FormatException("Custom change paths are not supported.");
}
return (Parse($"{hd.Extkey}{suffix}"), null);
return CreateDerivationSchemeSettings(Parse($"{hd.Extkey}{suffix}"), null);
case PubKeyProvider.Origin origin:
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
return (innerResult.Item1, new[] {origin.KeyOriginInfo});
return CreateDerivationSchemeSettings(innerResult.AccountDerivation, new[] {origin.KeyOriginInfo});
default:
throw new ArgumentOutOfRangeException();
}
@ -58,10 +72,10 @@ namespace BTCPayServer
throw new FormatException("Only output descriptors of one format are supported.");
case OutputDescriptor.Multi multi:
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
return (
return CreateDerivationSchemeSettings(
Parse(
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"),
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.AccountDerivation.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"),
xpubs.SelectMany(tuple => tuple.AccountKeySettings.Select(a => a.GetRootedKeyPath())).ToArray());
case OutputDescriptor.PKH pkh:
return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]");
case OutputDescriptor.SH sh:
@ -76,7 +90,7 @@ namespace BTCPayServer
sh.Inner is OutputDescriptor.WSH)
{
var ds = ParseOutputDescriptor(sh.Inner.ToString());
return (Parse(ds.Item1 + suffix), ds.Item2);
return CreateDerivationSchemeSettings(Parse(ds.AccountDerivation.ToString() + suffix), ds.AccountKeySettings.Select(d => d.GetRootedKeyPath()).ToArray());
};
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
case OutputDescriptor.WPKH wpkh:

View File

@ -5,6 +5,7 @@ using System.Text;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Scripting;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -52,15 +53,7 @@ namespace BTCPayServer
{
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
derivationSchemeSettings = derivationSchemeParser.ParseOutputDescriptor(xpub);
return true;
}
catch (Exception)
@ -382,6 +375,45 @@ namespace BTCPayServer
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath);
}
}
static KeyPath[] TrackedPaths = new[] { new KeyPath("0"), new KeyPath("1") };
public OutputDescriptor[] GetOutputDescriptors()
{
var signing = this.GetSigningAccountKeySettings();
if (!signing.RootFingerprint.HasValue || signing.AccountKeyPath is null)
return Array.Empty<OutputDescriptor>();
if (this.AccountDerivation is DirectDerivationStrategy direct)
{
var hdkey = direct.GetExtPubKeys().First().GetWif(this.Network.NBitcoinNetwork);
if (direct.ScriptPubKeyType() == ScriptPubKeyType.Segwit)
{
return TrackedPaths.Select(path => OutputDescriptor.NewWPKH(
PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork)).ToArray();
}
else if (direct.ScriptPubKeyType() == ScriptPubKeyType.Legacy)
{
return TrackedPaths.Select(path => OutputDescriptor.NewPKH(
PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork)).ToArray();
}
}
else if (this.AccountDerivation is P2SHDerivationStrategy p2sh)
{
if (this.AccountDerivation.ScriptPubKeyType() == ScriptPubKeyType.SegwitP2SH)
{
var hdkey = p2sh.GetExtPubKeys().First().GetWif(this.Network.NBitcoinNetwork);
return TrackedPaths.Select(path => OutputDescriptor.NewSH(
OutputDescriptor.NewWPKH(PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork), Network.NBitcoinNetwork)).ToArray();
}
}
// TODO support multisig
return Array.Empty<OutputDescriptor>();
}
}
public class AccountKeySettings
{

View File

@ -150,11 +150,7 @@ namespace BTCPayServer.Hosting
foreach (var paymentMethod in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<DerivationSchemeSettings>())
{
paymentMethod.IsHotWallet = paymentMethod.Source == "NBXplorer";
if (paymentMethod.IsHotWallet)
{
paymentMethod.Source = "NBXplorerGenerated";
store.SetSupportedPaymentMethod(paymentMethod);
}
paymentMethod.Source = "NBXplorerGenerated";
}
}
await ctx.SaveChangesAsync();

View File

@ -159,7 +159,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public DateTime ReceivedDate { get; set; }
public string Link { get; set; }
public string Id { get; set; }
public string Destination { get; set; }
}
}
}

View File

@ -6,7 +6,8 @@ namespace BTCPayServer.Models.ServerViewModels
public class ViewFilesViewModel
{
public List<StoredFile> Files { get; set; }
public Dictionary<string, string> DirectUrlByFiles { get; set; }
public string DirectFileUrl { get; set; }
public string SelectedFileId { get; set; }
public bool StorageConfigured { get; set; }
}
}

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.WalletViewModels
public string Label { get; set; }
[DisplayName("Derivation scheme")]
public string DerivationScheme { get; set; }
public string[] OutputDescriptors { get; set; }
public string DerivationSchemeInput { get; set; }
[Display(Name = "Is signing key")]
public string SelectedSigningKey { get; set; }

View File

@ -102,44 +102,6 @@ namespace BTCPayServer.PaymentRequest
Invoices = invoices.Select(entity =>
{
var state = entity.GetInvoiceState();
var payments = entity
.GetPayments(true)
.Select(paymentEntity =>
{
var paymentData = paymentEntity.GetCryptoPaymentData();
var paymentMethodId = paymentEntity.GetPaymentMethodId();
if (paymentData is null || paymentMethodId is null)
{
return null;
}
string txId = paymentData.GetPaymentId();
string link = GetTransactionLink(paymentMethodId, txId);
var paymentMethod = entity.GetPaymentMethod(paymentMethodId);
var amount = paymentData.GetValue();
var rate = paymentMethod.Rate;
var paid = (amount - paymentEntity.NetworkFee) * rate;
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _currencies.FormatCurrency(paid, blob.Currency),
RateFormatted = _currencies.FormatCurrency(rate, blob.Currency),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
Destination = paymentData.GetDestination()
};
})
.Where(payment => payment != null)
.ToList();
if (state.Status == InvoiceStatusLegacy.Invalid ||
state.Status == InvoiceStatusLegacy.Expired && !payments.Any())
return null;
return new ViewPaymentRequestViewModel.PaymentRequestInvoice
{
Id = entity.Id,
@ -149,11 +111,40 @@ namespace BTCPayServer.PaymentRequest
ExpiryDate = entity.ExpirationTime.DateTime,
State = state,
StateFormatted = state.ToString(),
Payments = payments
Payments = entity
.GetPayments(true)
.Select(paymentEntity =>
{
var paymentData = paymentEntity.GetCryptoPaymentData();
var paymentMethodId = paymentEntity.GetPaymentMethodId();
if (paymentData is null || paymentMethodId is null)
{
return null;
}
string txId = paymentData.GetPaymentId();
string link = GetTransactionLink(paymentMethodId, txId);
var paymentMethod = entity.GetPaymentMethod(paymentMethodId);
var amount = paymentData.GetValue();
var rate = paymentMethod.Rate;
var paid = (amount - paymentEntity.NetworkFee) * rate;
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _currencies.FormatCurrency(paid, blob.Currency),
RateFormatted = _currencies.FormatCurrency(rate, blob.Currency),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId
};
})
.Where(payment => payment != null)
.ToList()
};
})
.Where(invoice => invoice != null)
.ToList()
}).ToList()
};
}

View File

@ -55,14 +55,11 @@ namespace BTCPayServer.Plugins
continue;
}
detectedPlugins = detectedPlugins.Select(plugin =>
foreach (var btcPayServerPlugin in detectedPlugins)
{
plugin.SystemPlugin = true;
return plugin;
});
loadedPlugins.Add((null,systemExtension, CreateEmbeddedFileProviderForAssembly(systemExtension)));
btcPayServerPlugin.SystemPlugin = true;
loadedPlugins.Add((null,systemExtension, CreateEmbeddedFileProviderForAssembly(systemExtension)));
}
plugins.AddRange(detectedPlugins);
}
var orderFilePath = Path.Combine(pluginsFolder, "order");

View File

@ -1,10 +1,7 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
public class MigrationSettings
{
[JsonProperty("MigrateHotwalletProperty2")]
public bool MigrateHotwalletProperty { get; set; }
public bool MigrateU2FToFIDO2{ get; set; }
public bool UnreachableStoreCheck { get; set; }

View File

@ -113,8 +113,10 @@
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close">
<vc:icon symbol="close" />
<button type="button" class="close text-white" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
<i class="fa fa-times fa-fw"></i>
</span>
</button>
</div>
<div class="modal-body p-0">
@ -256,7 +258,7 @@
</div>
<!-- Sidebar -->
<nav id="sidebar">
<nav id="sidebar" class="bg-dark text-white">
<div class="bg-primary p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ms-5" href="#">

View File

@ -9,27 +9,27 @@
ViewData["Title"] = Model.Title;
Layout = null;
var theme = await _settingsRepository.GetTheme();
string StatusClass(InvoiceState state)
string StatusTextClass(InvoiceState state)
{
switch (state.Status.ToModernStatus())
{
case InvoiceStatus.Settled:
case InvoiceStatus.Processing:
return "success";
return "text-success";
case InvoiceStatus.Expired:
switch (state.ExceptionStatus)
{
case InvoiceExceptionStatus.PaidLate:
case InvoiceExceptionStatus.PaidPartial:
case InvoiceExceptionStatus.PaidOver:
return "warning";
return "text-warning";
default:
return "danger";
return "text-danger";
}
case InvoiceStatus.Invalid:
return "danger";
return "text-danger";
default:
return "warning";
return "text-warning";
}
}
}
@ -56,11 +56,6 @@
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
<script src="~/modal/btcpay.js" asp-append-version="true"></script>
@Safe.Raw(Model.EmbeddedCSS)
<style>
.invoice { margin-top: var(--btcpay-space-s); }
.invoice + .invoice { margin-top: var(--btcpay-space-m); }
.invoice .badge { font-size: var(--btcpay-font-size-s); }
</style>
<noscript>
<style>
.hide-when-js, [v-cloak] { display: block !important; }
@ -246,32 +241,30 @@
}
else
{
@foreach (var invoice in Model.Invoices)
{
<table class="invoice table">
<thead>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
</thead>
<tbody>
<tr class="table-borderless table-light">
<table class="table my-0">
<thead>
<tr class="table-borderless">
<th class="fw-normal text-secondary" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Id</td>
<td>@invoice.ExpiryDate.ToString("g")</td>
<td class="text-end">@invoice.AmountFormatted</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
</td>
<td class="text-end text-print-default @StatusTextClass(invoice.State)">@invoice.StateFormatted</td>
</tr>
@if (invoice.Payments != null && invoice.Payments.Any())
if (invoice.Payments != null && invoice.Payments.Any())
{
<tr class="table-borderless table-light">
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary ps-3">Transaction Id</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
@ -280,30 +273,26 @@
@foreach (var payment in invoice.Payments)
{
<tr class="table-borderless table-light">
<td class="text-break"><code>@payment.Destination</code></td>
<td class="ps-3 text-break">
@if (!string.IsNullOrEmpty(payment.Link))
{
<a href="@payment.Link" class="text-print-default" rel="noreferrer noopener" target="_blank">@payment.Id</a>
}
else
{
<span>@payment.Id</span>
}
</td>
<td>@payment.ReceivedDate.ToString("g")</td>
<td class="text-end">@payment.PaidFormatted</td>
<td class="text-end">@payment.RateFormatted</td>
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
@if (!string.IsNullOrEmpty(payment.Link))
{
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
}
else
{
<span class="text-break">@payment.Id</span>
}
</td>
</tr>
}
}
</tbody>
</table>
}
}
</tbody>
</table>
}
</noscript>
@ -311,10 +300,10 @@
<p class="text-muted">No payments made yet.</p>
</template>
<template v-else>
<table v-for="invoice of srvModel.invoices" :key="invoice.id" class="invoice table">
<table class="table my-0">
<thead>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
@ -322,38 +311,32 @@
</tr>
</thead>
<tbody>
<tr class="table-borderless table-light">
<td>{{invoice.id}}</td>
<td v-text="formatDate(invoice.expiryDate)"></td>
<td class="text-end">{{invoice.amountFormatted}}</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
</td>
</tr>
<template v-if="invoice.payments && invoice.payments.length > 0">
<tr class="table-borderless table-light">
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
<template v-for="invoice of srvModel.invoices" :key="invoice.id">
<tr>
<td>{{invoice.id}}</td>
<td v-text="formatDate(invoice.expiryDate)"></td>
<td class="text-end">{{invoice.amountFormatted}}</td>
<td class="text-end"></td>
<td class="text-end text-print-default" :class="statusTextClass(invoice.stateFormatted)">{{invoice.stateFormatted}}</td>
</tr>
<template v-for="payment of invoice.payments">
<template v-if="invoice.payments && invoice.payments.length > 0">
<tr class="table-borderless table-light">
<td class="text-break"><code>{{payment.destination}}</code></td>
<th class="fw-normal text-secondary ps-3">Transaction Id</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
</tr>
<tr v-for="payment of invoice.payments" class="table-borderless table-light">
<td class="ps-3 text-break">
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
<span v-else>{{payment.id}}</span>
</td>
<td v-text="formatDate(payment.receivedDate)"></td>
<td class="text-end">{{payment.paidFormatted}}</td>
<td class="text-end">{{payment.rateFormatted}}</td>
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
<span v-else>{{payment.id}}</span>
</td>
</tr>
</template>
</template>
</tbody>

View File

@ -45,7 +45,7 @@ else
<td>@file.Timestamp.ToBrowserDate()</td>
<td>@file.ApplicationUser.UserName</td>
<td class="text-end">
<a href="@Url.Action("Files", "Server", new { fileIds = new string[] { file.Id} })">Get Link</a>
<a asp-action="Files" asp-route-fileId="@file.Id">Get Link</a>
- <a asp-action="CreateTemporaryFileUrl" asp-route-fileId="@file.Id">Get Temp Link</a>
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
</td>
@ -63,38 +63,31 @@ else
}
@if(Model.DirectUrlByFiles!=null && Model.DirectUrlByFiles.Count > 0)
@if (!string.IsNullOrEmpty(Model.SelectedFileId))
{
foreach (KeyValuePair<string, string> fileUrlPair in Model.DirectUrlByFiles)
{
var fileId = fileUrlPair.Key;
var fileUrl = fileUrlPair.Value;
var file = Model.Files.Single(storedFile => storedFile.Id.Equals(fileId, StringComparison.InvariantCultureIgnoreCase));
<div class="card mb-2">
<div class="card-text">
<ul class="list-group list-group-flush">
<li class="list-group-item">
@file.FileName
</li>
<li class="list-group-item">
<strong>URL:</strong>
<a asp-action="GetFile" asp-controller="Storage" asp-route-fileId="@fileId" target="_blank">
@Url.Action("GetFile", "Storage", new
{
fileId = fileId
}, Context.Request.Scheme, Context.Request.Host.ToString())
</a>
</li>
<li class="list-group-item">
<strong>Direct URL:</strong>
<a href="@fileUrl" target="_blank" rel="noreferrer noopener">@fileUrl</a>
</li>
</ul>
</div>
var file = Model.Files.Single(storedFile => storedFile.Id.Equals(Model.SelectedFileId, StringComparison.InvariantCultureIgnoreCase));
<div class="card mb-2">
<div class="card-text">
<ul class="list-group list-group-flush">
<li class="list-group-item">
@file.FileName
</li>
<li class="list-group-item">
<strong>URL:</strong>
<a asp-action="GetFile" asp-controller="Storage" asp-route-fileId="@Model.SelectedFileId" target="_blank">
@Url.Action("GetFile", "Storage", new
{
fileId = Model.SelectedFileId
}, Context.Request.Scheme, Context.Request.Host.ToString())
</a>
</li>
<li class="list-group-item">
<strong>Direct URL:</strong>
<a href="@Model.DirectFileUrl" target="_blank" rel="noreferrer noopener">@Model.DirectFileUrl</a>
</li>
</ul>
</div>
}
</div>
}
@if (Model.StorageConfigured)

View File

@ -15,6 +15,10 @@
<div>
@if (Model.Uri == null) // if GRPC
{
<a href="https://www.pebble.indiesquare.me/" target="_blank" class="d-inline-block me-3 text-center" rel="noreferrer noopener">
<img src="~/img/pebblewallet.jpg" width="100" height="100" asp-append-version="true" alt="Pebble" />
<div class="mt-2">Pebble</div>
</a>
<a href="https://zaphq.io/" target="_blank" class="d-inline-block me-3 text-center" rel="noreferrer noopener">
<img src="~/img/zapwallet.jpg" width="100" height="100" asp-append-version="true" alt="Zap" />
<div class="mt-2">Zap</div>

View File

@ -64,20 +64,17 @@
@if (Model.Outputs.Count == 1)
{
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[0].DestinationAddress" class="form-label"></label>
<button type="submit" name="command" value="add-output" class="d-inline-block ms-2 btn text-secondary btn-link p-0 mb-2">Add another destination</button>
</div>
<input asp-for="Outputs[0].DestinationAddress" class="form-control font-monospace" autofocus autocomplete="off" />
<label asp-for="Outputs[0].DestinationAddress" class="form-label"></label>
<input asp-for="Outputs[0].DestinationAddress" class="form-control font-monospace" />
<span asp-validation-for="Outputs[0].DestinationAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Outputs[0].Amount" class="form-label"></label>
<div class="input-group">
<input asp-for="Outputs[0].Amount" type="number" step="any" min="0" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<input asp-for="Outputs[0].Amount" type="number" step="any" min="0" asp-format="{0}" class="form-control output-amount" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0">=</span>
<input type="number" class="input-group-text fiat-value-edit-input py-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<input type="number" class="input-group-text fiat-value-edit-input py-0" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0">@Model.Fiat</span>
</div>
</div>
@ -94,29 +91,28 @@
}
else
{
<div class="list-group list-group-flush mt-n3 mb-4">
<div class="list-group-item">
<h5 class="mb-0">Destinations</h5>
</div>
<div class="list-group mb-4">
@for (var index = 0; index < Model.Outputs.Count; index++)
{
<div class="list-group-item transaction-output-form px-0 py-4">
<div class="list-group-item transaction-output-form">
<div class="row">
<div class="col-sm-12 col-lg-10">
<div class="form-group">
<label asp-for="Outputs[index].DestinationAddress" class="form-label"></label>
<input asp-for="Outputs[index].DestinationAddress" class="form-control" autocomplete="off" />
<input asp-for="Outputs[index].DestinationAddress" class="form-control" />
<span asp-validation-for="Outputs[index].DestinationAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Outputs[index].Amount" class="form-label"></label>
<div class="input-group">
<input asp-for="Outputs[index].Amount" type="number" min="0" step="any" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0">=</span>
<input type="number" class="input-group-text fiat-value-edit-input py-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0">@Model.Fiat</span>
</div>
<input asp-for="Outputs[index].Amount" type="number" step="any" asp-format="{0}" class="form-control output-amount" />
<span class="input-group-text fiat-value" style="display:none;"></span>
</div>
<p class="form-text text-secondary crypto-info mb-2">
Your available balance is
Your current balance is
<button type="button" class="crypto-balance-link btn btn-link p-0 align-baseline">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
@if (Model.ImmatureBalance > 0)
{
@ -148,7 +144,7 @@
}
<div class="form-group my-4">
<label asp-for="FeeSatoshiPerByte" class="form-label"></label>
<input asp-for="FeeSatoshiPerByte" type="number" min="0" step="any" class="form-control" style="max-width:14ch;" />
<input asp-for="FeeSatoshiPerByte" type="number" step="any" class="form-control" style="max-width:14ch;" />
<span asp-validation-for="FeeSatoshiPerByte" class="text-danger"></span>
<span id="FeeRate-Error" class="text-danger"></span>
@if (Model.RecommendedSatoshiPerByte.Any())
@ -236,6 +232,7 @@
</div>
<div class="form-group d-flex mt-2">
<button type="submit" id="SignTransaction" name="command" value="@(Model.NBXSeedAvailable ? "nbx-seed" : "sign")" class="btn btn-primary">Sign transaction</button>
<button type="submit" name="command" value="add-output" class="ms-2 btn btn-secondary">Add another destination</button>
<button type="button" id="bip21parse" class="ms-2 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="ms-2 btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button>
</div>

View File

@ -1,4 +1,4 @@
@using Newtonsoft.Json
@using Newtonsoft.Json
@using System.Text
@using NBitcoin.DataEncoders
@model WalletSettingsViewModel
@ -12,26 +12,36 @@
<div class="row">
<div class="col-md-8 col-lg-6">
<form method="post" asp-action="WalletSettings">
<input type="hidden" asp-for="StoreName"/>
<input type="hidden" asp-for="UriScheme"/>
<input type="hidden" asp-for="StoreName" />
<input type="hidden" asp-for="UriScheme" />
<div class="form-group">
<label asp-for="Label" class="form-label"></label>
<input asp-for="Label" class="form-control"/>
<input asp-for="Label" class="form-control" />
<span asp-validation-for="Label" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DerivationScheme" class="form-label"></label>
<input asp-for="DerivationScheme" class="form-control" readonly/>
<input asp-for="DerivationScheme" class="form-control" readonly />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div>
@if (!string.IsNullOrEmpty(Model.DerivationSchemeInput) && Model.DerivationSchemeInput != Model.DerivationScheme)
{
<div class="form-group">
<label asp-for="DerivationSchemeInput" class="form-label"></label>
<input asp-for="DerivationSchemeInput" class="form-control" readonly/>
<input asp-for="DerivationSchemeInput" class="form-control" readonly />
<span asp-validation-for="DerivationSchemeInput" class="text-danger"></span>
</div>
}
@if (Model.OutputDescriptors.Length != 0)
{
for (int i = 0; i < Model.OutputDescriptors.Length; i++)
{
<div class="form-group">
<label asp-for="@Model.OutputDescriptors[i]" class="form-label"></label>
<input asp-for="@Model.OutputDescriptors[i]" class="form-control" readonly />
</div>
}
}
@for (var i = 0; i < Model.AccountKeys.Count; i++)
{
<div class="d-flex mt-5 mb-3">
@ -40,25 +50,25 @@
</div>
<div class="form-group">
<label asp-for="@Model.AccountKeys[i].AccountKey" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly/>
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly />
<span asp-validation-for="@Model.AccountKeys[i].AccountKey" class="text-danger"></span>
</div>
<div class="row">
<div class="form-group col-auto">
<label asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" style="max-width:16ch;"/>
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" style="max-width:16ch;" />
<span asp-validation-for="@Model.AccountKeys[i].MasterFingerprint" class="text-danger"></span>
</div>
<div class="form-group col-auto">
<label asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" style="max-width:16ch;"/>
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" style="max-width:16ch;" />
<span asp-validation-for="@Model.AccountKeys[i].AccountKeyPath" class="text-danger"></span>
</div>
</div>
@if (Model.IsMultiSig)
{
<div class="form-check">
<input asp-for="SelectedSigningKey" class="form-check-input" type="radio" value="@Model.AccountKeys[i].AccountKey"/>
<input asp-for="SelectedSigningKey" class="form-check-input" type="radio" value="@Model.AccountKeys[i].AccountKey" />
<label asp-for="SelectedSigningKey" class="form-check-label"></label>
</div>
}

View File

@ -192,6 +192,7 @@
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
"wwwroot/vendor/bc-ur/web-bundle.js",
"wwwroot/vendor/ur-registry/urlib.min.js",
"wwwroot/vendor/vue-qrcode-reader/VueQrcodeReader.umd.min.js",
"wwwroot/js/wallet/**/*.js"
],
@ -212,6 +213,7 @@
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vue-qrcode/vue-qrcode.min.js",
"wwwroot/vendor/bc-ur/web-bundle.js",
"wwwroot/vendor/ur-registry/urlib.min.js",
"wwwroot/vendor/vue-qrcode-reader/VueQrcodeReader.umd.min.js"
],
"minify": {

View File

@ -76,8 +76,7 @@
overflow-x: hidden;
overflow-y: auto;
z-index: 999;
color: var(--btcpay-white);
background: var(--btcpay-bg-dark);
background: #e1e6ea;
transition: all 0.3s;
-webkit-overflow-scrolling: touch;
}

View File

@ -1,7 +1,7 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "bg-BG",
"currentLanguage": "български",
"currentLanguage": "Английски",
"lang": "Език",
"Awaiting Payment...": "Очаква се платеж...",
"Pay with": "Плати с",
@ -49,4 +49,4 @@
"Close": "Затвори",
"NotPaid_ExtraTransaction": "Фактурата не а платена изцяло. Моля пратете остатъка в допълнителна транзакция. ",
"Recommended_Fee": "Препоръчена тарифа: {{feeRate}} сат/байт (sat/byte)"
}
}

View File

@ -1,7 +1,7 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "fi-FI",
"currentLanguage": "Suomi",
"currentLanguage": "Englanti",
"lang": "Kieli",
"Awaiting Payment...": "Odotaa maksua...",
"Pay with": "Maksa käyttämällä",
@ -49,4 +49,4 @@
"Close": "Sulje",
"NotPaid_ExtraTransaction": "Laskua ei ole maksettu kokonaan. Lähetä uusi maksu erääntyvän summan kattamiseksi.",
"Recommended_Fee": "Suositeltu siirtomaksu: {{feeRate}} sat/tavu"
}
}

View File

@ -1,52 +0,0 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "he",
"currentLanguage": "עברית",
"lang": "שפה",
"Awaiting Payment...": "ממתין לתשלום...",
"Pay with": "תשלום עם",
"Contact and Refund Email": "דוא\"ל ליצירת קשר והחזר",
"Contact_Body": "נא להזין כתובת דוא\"ל למטה.\nאנחנו ניצור אתך קשר בכתובת זו במקרא של בעיה עם התשלום שלך.",
"Your email": "הדוא\"ל שלך",
"Continue": "המשך",
"Please enter a valid email address": "נא להזין כתובת דוא\"ל תקינה",
"Order Amount": "סה\"כ הזמנה",
"Network Cost": "עלות רשת",
"Already Paid": "כבר שולם",
"Due": "חוב",
"Scan": "סריקה",
"Copy": "העתקה",
"Conversion": "המרה",
"Open in wallet": "פתיחת ארנק",
"CompletePay_Body": "להשלים את התשלום, נא לשלוח {{btcDue}} {{cryptoCode}} לכתובת הבאה.",
"Amount": "סכום",
"Address": "כתובת",
"Copied": "הועתק",
"ConversionTab_BodyTop": "ניתן לשלם {{cryptoCode}} {{btcDue}} בעזרת שיטקוינים אחרים מאלו שהמוכר/ת מקבל/ת ישירות.",
"ConversionTab_BodyDesc": "שירות זה מסופק על ידי צד שלישי. שימו לב שאין לנו שליטה על איך ספקים יעבירו את הכסף שלך. החשבונית תחשב כשולמה רק כשהכספים מתקבלים על שרשרת הבלוקים של {{cryptocode}}.",
"ConversionTab_CalculateAmount_Error": "נסו שוב",
"ConversionTab_LoadCurrencies_Error": "נסו שוב",
"ConversionTab_Lightning": "לא קיימים ספקי המרה לתשלומים לרשת הברק.",
"ConversionTab_CurrencyList_Select_Option": "נא לבחור מטבע להמיר",
"Invoice expiring soon...": "חשבונית תפוג תוקף בקרוב...",
"Invoice expired": "חשבונית פגה תוקף",
"What happened?": "מה קרה?",
"InvoiceExpired_Body_1": "חשבונית זו פגה תוקף. חשבונית תקפה רק למשך {{maxTimeMinutes}} דקות.\nניתן לחזור אל {{storeName}} אם ברצונך לשלוח את תשלומך שוב.",
"InvoiceExpired_Body_2": "אם ניסית לשלוח תשלום, הוא תרם התקבל על ידי הרשת. עדיין לא קיבלנו את הכספים שלך.",
"InvoiceExpired_Body_3": "אם נקבל זאת במועד מאוחר יותר, אנחנו נטפל בהזמנתך, או ניצור קשר אתך להסדיר החזר...",
"Invoice ID": "מספר חשבונית",
"Order ID": "מספר הזמנה",
"Return to StoreName": "חזרה אל {{storeName}}",
"This invoice has been paid": "החשבונית שולמה",
"This invoice has been archived": "חשבונית זו נגנזה",
"Archived_Body": "נא לפנות לחנות למידע על הזמנה או סיוע",
"BOLT 11 Invoice": "חשבונית BOLT 11",
"Node Info": "מידע צומת",
"txCount": "{{count}} פעולה",
"txCount_plural": "{{count}} פעולות",
"Pay with CoinSwitch": "תשלום עם CoinSwitch",
"Pay with Changelly": "תשלום עם Changelly",
"Close": "סגירה",
"NotPaid_ExtraTransaction": "החשבונית לא שולמה במלואה. נא לשלוח תשלום נוסף לכיסוי סכום החוב.",
"Recommended_Fee": "עמלה מומלצת: {{feeRate}} סאט/בית"
}

View File

@ -1,7 +1,7 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "kk-KZ",
"currentLanguage": "қазақ",
"currentLanguage": "Қазақша",
"lang": "Тіл",
"Awaiting Payment...": "Күтіп тұрған төлем…",
"Pay with": "Төлеу",
@ -49,4 +49,4 @@
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
}
}

View File

@ -1,52 +0,0 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "ko",
"currentLanguage": "한국어",
"lang": "언어",
"Awaiting Payment...": "지불 대기 중...",
"Pay with": "Pay with",
"Contact and Refund Email": "연락 및 환불 이메일",
"Contact_Body": "Please provide an email address below. Well contact you at this address if there is an issue with your payment.",
"Your email": "자신의 이메일",
"Continue": "계속하기",
"Please enter a valid email address": "유효한 이메일 주소를 입력해주세요",
"Order Amount": "총 주문 금액",
"Network Cost": "네트워크 수수료",
"Already Paid": "지불 완료",
"Due": "지불 대기 중",
"Scan": "스캔",
"Copy": "복사",
"Conversion": "환율",
"Open in wallet": "지갑에서 열기",
"CompletePay_Body": "To complete your payment, please send {{btcDue}} {{cryptoCode}} to the address below.",
"Amount": "금액",
"Address": "주소",
"Copied": "복사 완료",
"ConversionTab_BodyTop": "You can pay {{btcDue}} {{cryptoCode}} using altcoins other than the ones merchant directly supports.",
"ConversionTab_BodyDesc": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "다시 시도",
"ConversionTab_LoadCurrencies_Error": "다시 시도",
"ConversionTab_Lightning": "No conversion providers available for Lightning Network payments.",
"ConversionTab_CurrencyList_Select_Option": "환전할 화폐를 선택하세요",
"Invoice expiring soon...": "인보이스가 곧 만료 됨...",
"Invoice expired": "인보이스 만료",
"What happened?": "무슨 일인가요?",
"InvoiceExpired_Body_1": "This invoice has expired. An invoice is only valid for {{maxTimeMinutes}} minutes. \nYou can return to {{storeName}} if you would like to submit your payment again.",
"InvoiceExpired_Body_2": "If you tried to send a payment, it has not yet been accepted by the network. We have not yet received your funds.",
"InvoiceExpired_Body_3": "",
"Invoice ID": "인보이스 ID",
"Order ID": "주문 ID",
"Return to StoreName": "{{storeName}}으로 돌아가기",
"This invoice has been paid": "이 인보이스는 지불 완료됐습니다.",
"This invoice has been archived": "This invoice has been archived",
"Archived_Body": "Please contact the store for order information or assistance",
"BOLT 11 Invoice": "BOLT 11 인보이스",
"Node Info": "노드 정보",
"txCount": "{{count}} 거래",
"txCount_plural": "{{count}} 거래",
"Pay with CoinSwitch": "CoinSwitch를 이용해 지불하기",
"Pay with Changelly": "Changelly를 이용해 지불하기",
"Close": "닫기",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "추천하는 수수료: {{feeRate}} sat/byte"
}

View File

@ -3,13 +3,13 @@
"code": "nl-NL",
"currentLanguage": "Nederlands",
"lang": "Taal",
"Awaiting Payment...": "Wacht op betaling...",
"Pay with": "Betaal met",
"Contact and Refund Email": "E-mailadres voor opvolging en terugbetaling",
"Contact_Body": "Laat hieronder uw e-mailadres achter. We nemen contact met u op mochten er problemen zijn met uw betaling.",
"Your email": "Uw e-mailadres",
"Awaiting Payment...": "In afwachting van de betaling...",
"Pay with": "Betalen met",
"Contact and Refund Email": "Email adres voor opvolging en terugbetaling",
"Contact_Body": "Laat hieronder uw email adres achter. We nemen contact met u op als er problemen zijn met uw betaling.",
"Your email": "Je email adres",
"Continue": "Verder",
"Please enter a valid email address": "Vul een geldig e-mailadres in",
"Please enter a valid email address": "Vul een geldig email adres in",
"Order Amount": "Bedrag van uw bestelling",
"Network Cost": "Netwerkkosten",
"Already Paid": "Reeds betaald",
@ -18,24 +18,24 @@
"Copy": "Kopiëren",
"Conversion": "Omzetting",
"Open in wallet": "Wallet openen",
"CompletePay_Body": "Om de betaling af te ronden, stuur alstublieft {{btcDue}} {{cryptoCode}} naar het hieronder vermelde adres.",
"CompletePay_Body": "Om de betaling af te ronden, stuur alstublieft {{btcDue}} {{cryptoCode}} naar het hieronder vemelde adres.",
"Amount": "Bedrag",
"Address": "Adres",
"Copied": "Gekopieerd",
"ConversionTab_BodyTop": "U kunt altcoins gebruiken die niet ondersteund zijn door de verkoper, om {{btcDue}} {{cryptoCode}} te betalen.",
"ConversionTab_BodyDesc": "Deze dienst wordt door een 3e partij geleverd. Wij hebben daardoor geen zicht op uw fondsen. De factuur wordt pas als betaald beschouwd, wanneer de fondsen door de {{ cryptoCode }} blockchain aanvaard zijn.",
"ConversionTab_BodyDesc": "Deze dienst wordt door een 3e partij geleverd. Wij hebben daardoor geen zicht over uw fondsen. De factuur wordt pas als betaald beschouwd, wanneer de fondsen door de {{ cryptoCode }} blockchain aanvaard zijn.",
"ConversionTab_CalculateAmount_Error": "Opnieuw proberen",
"ConversionTab_LoadCurrencies_Error": "Opnieuw proberen",
"ConversionTab_Lightning": "Geen conversie leverancier beschikbaar voor de betalingen op het Lightning Network",
"ConversionTab_CurrencyList_Select_Option": "Selecteer een valuta om te converteren",
"Invoice expiring soon...": "Factuur verloopt binnenkort...",
"Invoice expired": "Factuur vervallen",
"What happened?": "Wat is er gebeurd?",
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is slechts geldig voor {{maxTimeMinutes}} minuten. \nU kan teruggaan naar {{storeName}} als u de betaling opnieuw wil proberen.",
"InvoiceExpired_Body_2": "Als u een betaling uitvoerde, dan werd deze nog niet bevestigd door het netwerk. We hebben uw betaling nog niet ontvangen.",
"InvoiceExpired_Body_3": "Als we uw betaling later ontvangen, zullen we uw order verwerken of nemen we contact op om een terugbetaling te regelen...",
"Invoice expiring soon...": "De factuur verloopt binnenkort...",
"Invoice expired": "Vervallen factuur",
"What happened?": "Wat gebeurde er?",
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is alleen geldig voor {{maxTimeMinutes}} minuten. \nJe kan terug komen naar {{storeName}} als je de betaling opnieuw wilt proberen",
"InvoiceExpired_Body_2": "Als u een betaling uitvoerde, dan werd dit nog niet bevestigd door het netwerk. We hebben uw fondsen nog niet ontvangen.",
"InvoiceExpired_Body_3": "Indien we het later ontvangen, zullen we uw order verwerken of nemen we contact op om een terugbetaling te regelen...",
"Invoice ID": "Factuurnummer",
"Order ID": "Ordernummer",
"Order ID": "Bestllingsnummer",
"Return to StoreName": "Terug naar {{storeName}}",
"This invoice has been paid": "Deze factuur is betaald",
"This invoice has been archived": "Deze factuur is gearchiveerd",

View File

@ -32,7 +32,7 @@
"Invoice expired": "Fatura expirada",
"What happened?": "O que aconteceu?",
"InvoiceExpired_Body_1": "Esta fatura expirou. Uma fatura é válida por apenas {{maxTimeMinutes}} minutos. \nVocê pode voltar para {{storeName}} se quiser tentar enviar o pagamento novamente.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento, ele ainda não foi aceito pela rede. Nós ainda não recebemos o valor enviado.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento e ele ainda não foi aceito pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
"InvoiceExpired_Body_3": "Se recebermos mais tarde, vamos processar o pedido ou entrar em contato para combinar o reembolso...",
"Invoice ID": "Nº da Fatura",
"Order ID": "Nº do Pedido",

View File

@ -217,12 +217,6 @@ h2 small .fa-question-circle-o {
.w-150px { width: 150px; }
.w-175px { width: 175px; }
.w-200px { width: 200px; }
.w-225px { width: 225px; }
.w-250px { width: 250px; }
.w-275px { width: 275px; }
.w-300px { width: 300px; }
.w-325px { width: 325px; }
.w-350px { width: 350px; }
/* Print */
@media print {
@ -230,7 +224,7 @@ h2 small .fa-question-circle-o {
.table th {
background: transparent;
}
.bg-tile.h-100.p-3 {
.jumbotron {
padding: 1rem 0 !important;
}
.text-print-default {

View File

@ -148,7 +148,7 @@ function inputChanges(event, buttonSize) {
// Fixed amount: Add price and currency as hidden inputs
if (isFixedAmount) {
if (srvModel.price)
if (srvModel.price !== '')
html += addInput(priceInputName, srvModel.price);
if(allowCurrencySelection){
html += addInput("currency", srvModel.currency);
@ -158,8 +158,7 @@ function inputChanges(event, buttonSize) {
else if (isCustomAmount) {
html += ' <div class="btcpay-custom-container">\n <div class="btcpay-custom">\n';
html += srvModel.simpleInput ? '' : addPlusMinusButton("-", srvModel.step, srvModel.min, srvModel.max);
if (srvModel.price)
html += ' ' + addInputPrice(priceInputName, srvModel.price, widthInput, "", "number", srvModel.min, srvModel.max, srvModel.step);
html += ' ' + addInputPrice(priceInputName, srvModel.price, widthInput, "", "number", srvModel.min, srvModel.max, srvModel.step);
html += srvModel.simpleInput ? '' : addPlusMinusButton("+", srvModel.step, srvModel.min, srvModel.max);
html += ' </div>\n';
if(allowCurrencySelection) {
@ -199,7 +198,7 @@ function inputChanges(event, buttonSize) {
var url = new URL(form.getAttribute("action"));
var formData = new FormData(form);
formData.forEach((value, key) => {
if (key !== "jsonResponse") {
if(key !== "jsonResponse"){
url.searchParams.append(key, value);
}
});

View File

@ -108,26 +108,26 @@ addLoadEvent(function (ev) {
this.pay();
}
},
statusClass: function (state) {
statusTextClass: function (state) {
var [, status,, exceptionStatus] = state.match(/(\w*)\s?(\((\w*)\))?/) || [];
switch (status) {
case "confirmed":
case "complete":
case "paid":
return "success";
return "text-success";
case "expired":
switch (exceptionStatus) {
case "paidLate":
case "paidPartial":
case "paidOver":
return "warning";
return "text-warning";
default:
return "danger";
return "text-danger";
}
case "invalid":
return "danger";
return "text-danger";
default:
return "warning";
return "text-warning";
}
}
},

View File

@ -26,8 +26,7 @@
"items": {
"type": "string"
}
},
"example": "1000&orderId=1001&orderId=1002"
}
},
{
"name": "status",
@ -1031,7 +1030,7 @@
"allOf": [ { "$ref": "#/components/schemas/TimeSpanMinutes" } ]
},
"monitoringMinutes": {
"type": "number",
"type": "integer",
"nullable": true,
"description": "The number of minutes after an invoice expired after which we are still monitoring for incoming payments. Defaults to the store's settings. (The default store settings is 1440, 1 day)",
"allOf": [ { "$ref": "#/components/schemas/TimeSpanMinutes" } ]

View File

@ -61,7 +61,7 @@
"tags": [
"Store Wallet (On Chain)"
],
"summary": "Get store on-chain wallet fee rate",
"summary": "Get store on-chain wallet overview",
"parameters": [
{
"name": "storeId",

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.2.0</Version>
<Version>1.1.2</Version>
</PropertyGroup>
</Project>

View File

@ -1,68 +1,5 @@
# Changelog
## 1.2.0
### Improvements:
* Migrate to Bootstrap5 (#2490) @dennisreimann
* Greenfield: Server Info: Support all currency codes for sync status (#2511) @kukks
* Greenfield: Add StoreId to Invoice model (#2592) @kukks
* Greenfield: Change `enabledOnly` filter to `enabled` @kukks
* Self host PoS app default images (#2449) @dennisreimann
* Various UI Tweaks and improvements (#2558 #2562 #2568 #2572 #2606 #2608 #2615 #2627 #2628 #2649 #2645 #2673 #2646 #2647 #2745 #2746) @dstrukt @dennisreimann @woutersamaey @johanf85 @bolatovumar
* Notify users to use newer BTCPay Vault app if necessary @nicolasdorier
* Set lightning invoice fallback in QR code as uppercase (#2492) @bjarnemagnussen @Kukks
* Optimize payout database fetching @nicolasdorier
* Wallet Signing UI improvements (#2559) @dennisreimann
* Add payjoin to hot wallet setup and turn on by default (#2450) @dennisreimann
* Add permission code to API page (#2599) @woutersamaey @dennisreimann
* Introduce Server paging for Payouts List (#2564) @kukks @dennisreimann
* Hide referer URL to hide our BTCPay Server URL (#2655) @woutersamaey
* Deeper accessibility for plugin system @kukks
* Add webhook delivery status indicator (#2679) @bolatovumar
* Auto-select store when creating a new invoice (#2680) @bolatovumar
* Save paymentRequestId in Metadata when creating invoice for Payment Request (#2644) @woutersamaey
* Support multiple file upload (#2705) @cypherbeerus
* Improve Dutch translation (https://github.com/btcpayserver/btcpayserver/commit/7ac83575d4c50e42f2ecc02c8bf80f66697b6d57) @woutersamaey
* Improve Portuguese translation (https://github.com/btcpayserver/btcpayserver/commit/7ac83575d4c50e42f2ecc02c8bf80f66697b6d57) rafaelpac
* Improve payment view (#2748) @dennisreimann @dstrukt
* Improve Wallet Send UI (#2750) @dennisreimann
* Show new store warning icon only if neither on-chain wallet nor LN is configured (#2760) @bolatovumar
* Update successful refund message (#2764) @cypherbeerus
* Fix translation on finnish, bulgarian, Kazath (fa91174b1a310e46a37e1862f2b9c263f5e26408, 10e3595a829052573a9918eacafabc6d10e03ea6 965beebc6624906a1f3127623576088dee23e9bf) @NicolasDorier
### New features:
* Greenfield: Delete User API (#2340) @bolatovumar @kukks
* Can create invoices without a specific amount: Top-up invoices (#2730 #2659) @NicolasDorier
* Greenfield: Add misc/permissions to document the hierarchical structure (#2654) @nicolasdorier
* Greenfield: Add "skip" and "limit" params for onchain txs API endpoint (#2688) @bolatovumar
* Greenfield: Add `CanModifyInvoices` permission (#2595) @kukks
* Greenfield: Add text search terms to an invoice (#2648) @NicolasDorier
* Greenfield: Add Get store Payment methods API (#2545) @kukks @bolatovumar
* GreenField: Add Generate Store OnChain Wallet API (#2708) @kukks
* Test Webhooks functionality (#2474) @bolatovumar
* Allow marking payout as paid manually (#2539) @Kukks
* Pull payments: Detect External OnChain Payouts (#2462) @Kukks
* Auto-detect language on payment page (#2552) @woutersamaey @Kukks
* Support spending to Taproot (#2718) @nicolasdorier
* Show Immature Balance in walletsend page (#2731 @732) @sageprogrammer @nicolasdorier
* Add hebrew translation for checkout (https://github.com/btcpayserver/btcpayserver/commit/7ac83575d4c50e42f2ecc02c8bf80f66697b6d57) @jonathanalevi
* Add korean translation for checkout (https://github.com/btcpayserver/btcpayserver/commit/7ac83575d4c50e42f2ecc02c8bf80f66697b6d57) Saeyoung Kim
### Bug fixes:
* Fix issue with mysql migration and maxLength (#2541) @jkljajic
* Fix broken shopify links @kukks
* Fix bug with LN payment method API endpoint throwing 500 (#2567) @bolatovumar
* Fix various wording and typos @pavlenex @britttttk @Zaxounette Jimi Ford
* Fix visual bug with invoices search help text overlapping invoice action buttons (#2583) @bolatovumar
* Fix: Invoice Search Text crashes invoice creation when value is too long (#2675) @kukks
* Greenfield documentation fixes (#2657 #2674 #2681 #2598) @woutersamaey @bolatovumar
* Re-enable "Create" button for invoices on correct form input (#2694) @bolatovumar
* Fix: Payment Request status does not update on invoice marked events or when pr amount is changed (#2700) @kukks
* Properly clip taxIncluded and invoice's amount (#2724) @nicolasdorier
* Fix PoS bug on dark mode (#2743) @dennisreimann
* Remove support for payout to a Bitcoin Url (#2766) @NicolasDorier
* Fix: Support Clightning 0.10.1 @kukks
## 1.1.2
* Fix: Unable to activate shopify integration @Kukks

View File

@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{29290EC7-0
docker-entrypoint.sh = docker-entrypoint.sh
.circleci\run-tests.sh = .circleci\run-tests.sh
Build\Version.csproj = Build\Version.csproj
Changelog.md = Changelog.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Rating", "BTCPayServer.Rating\BTCPayServer.Rating.csproj", "{6DC77459-D52F-45EE-B3F3-315043D33A1B}"