Compare commits
46 Commits
v1.0.3.96
...
v1.0.3.100
Author | SHA1 | Date | |
---|---|---|---|
2719849a54 | |||
3011fecf0f | |||
6da0a9a201 | |||
572fe3eacb | |||
ff82f15246 | |||
b214e3f6df | |||
cb9130fdf9 | |||
925dc869a2 | |||
5f1aa619cd | |||
541c748ecb | |||
e853bddbc8 | |||
79d26b5d95 | |||
840f52a75b | |||
f955302c74 | |||
95e7d3dfc4 | |||
75f2749b19 | |||
01e5b319d1 | |||
e504163bc7 | |||
aba3f7d6bd | |||
8d74023d30 | |||
602625fc17 | |||
bbeb2d5009 | |||
51faa39636 | |||
f37bfbf9f9 | |||
ba9928831e | |||
2b6bd3d751 | |||
e96ca21c89 | |||
6ee10fe98b | |||
a567c19759 | |||
bb3a087d39 | |||
5a92fe736f | |||
88390402a4 | |||
538eb66672 | |||
0b6dfe0fd3 | |||
d5579ef2b5 | |||
836c3a5b3a | |||
f2da64adad | |||
e5704abfb3 | |||
3bf4eea1fe | |||
aa23222339 | |||
68c1670c70 | |||
914eaaaa51 | |||
5831ba2143 | |||
c167a24f09 | |||
a539d27c62 | |||
d7fc079376 |
@ -144,6 +144,7 @@ namespace BTCPayServer.Tests
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
@ -226,6 +227,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public BTCPayNetworkProvider Networks { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
|
26
BTCPayServer.Tests/Extensions.cs
Normal file
26
BTCPayServer.Tests/Extensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static T AssertViewModel<T>(this IActionResult result)
|
||||
{
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
public static async Task<T> AssertViewModelAsync<T>(this Task<IActionResult> task)
|
||||
{
|
||||
var result = await task;
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
}
|
||||
}
|
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PSBTTests
|
||||
{
|
||||
public PSBTTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPlayWithPSBT()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
|
||||
Amount = 0.1m,
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmLedger.SuccessPath);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletSend(walletId, sendModel, command: "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Errors);
|
||||
Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.ExtKey);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
// Can use uploaded file?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -190,21 +190,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private async Task CanUploadRemoveFiles(ServerController controller)
|
||||
{
|
||||
var filename = "uploadtestfile.txt";
|
||||
var fileContent = "content";
|
||||
File.WriteAllText(filename, fileContent);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
var formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "text/plain";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(formFile));
|
||||
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
|
||||
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
|
||||
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
|
||||
Assert.Equal("Files", uploadFormFileResult.ActionName);
|
||||
|
81
BTCPayServer.Tests/TestUtils.cs
Normal file
81
BTCPayServer.Tests/TestUtils.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class TestUtils
|
||||
{
|
||||
public static FormFile GetFormFile(string filename, string content)
|
||||
{
|
||||
File.WriteAllText(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "text/plain";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static FormFile GetFormFile(string filename, byte[] content)
|
||||
{
|
||||
File.WriteAllBytes(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "application/octet-stream";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -58,6 +58,9 @@ using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -1457,59 +1460,70 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet);
|
||||
var testnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Testnet);
|
||||
var regtestNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
var mainnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork("BTC"));
|
||||
var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork("BTC"));
|
||||
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
|
||||
// Passing electrum stuff
|
||||
// Native
|
||||
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
// Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit
|
||||
result = testnetParser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
|
||||
result = mainnetParser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
Assert.Equal("xpub68fZn8w5ZTP5X4zymr1B1vKsMtJUiudtN2DZHQzJJc87gW1tXh7S4SALCsQijUzXstg2reVyuZYFuPnTDKXNiNgDZNpNiC4BrVzaaGEaRHj", result.ToString());
|
||||
// P2SH
|
||||
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
|
||||
result = testnetParser.Parse("upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
|
||||
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
|
||||
|
||||
result = mainnetParser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
|
||||
Assert.Equal("xpub661MyMwAqRbcGiYMrHB74DtmLBrNPSsUU6PV7ALAtpYyFhkc6TrUuLhxhET4VgwgQPnPfvYvEAHojf7QmQRj8imudHFoC7hju4f9xxri8wR-[p2sh]", result.ToString());
|
||||
|
||||
// if prefix not recognize, assume it is segwit
|
||||
result = testnetParser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", result.ToString());
|
||||
////////////////
|
||||
|
||||
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
testnetParser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey;
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
testnetParser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey;
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[legacy]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse($"{tpub}-[legacy]");
|
||||
testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
|
||||
result = testnetParser.Parse($"{tpub}-[legacy]");
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
parser = new DerivationSchemeParser(Network.RegTest);
|
||||
var parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC"));
|
||||
var parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString());
|
||||
|
||||
// Let's make sure we can't generate segwit with dogecoin
|
||||
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
|
||||
parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
|
||||
parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
|
||||
|
||||
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
|
||||
parsed = parser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
|
||||
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
|
||||
parsed = regtestParser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
|
||||
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanDisablePaymentMethods()
|
||||
public void CanAddDerivationSchemes()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
@ -1519,7 +1533,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
|
||||
var btcNetwork = tester.PayTester.Networks.GetNetwork("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.5m,
|
||||
@ -1540,16 +1554,18 @@ namespace BTCPayServer.Tests
|
||||
lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
|
||||
Assert.False(lightningVM.Enabled);
|
||||
|
||||
// Only Enabling/Disabling the payment method must redirect to store page
|
||||
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
Assert.True(derivationVM.Enabled);
|
||||
derivationVM.Enabled = false;
|
||||
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
// Confirmation
|
||||
controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult();
|
||||
Assert.False(derivationVM.Enabled);
|
||||
|
||||
// Clicking next without changing anything should send to the confirmation screen
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
Assert.False(derivationVM.Enabled);
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
Assert.True(derivationVM.Confirmation);
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
@ -1563,6 +1579,78 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||
|
||||
// Removing the derivation scheme, should redirect to store page
|
||||
var oldScheme = derivationVM.DerivationScheme;
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
derivationVM.DerivationScheme = null;
|
||||
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
|
||||
|
||||
// Setting it again should redirect to the confirmation page
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
derivationVM.DerivationScheme = oldScheme;
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
Assert.True(derivationVM.Confirmation);
|
||||
|
||||
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
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
|
||||
|
||||
// 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}";
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content);
|
||||
derivationVM.Enabled = true;
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
Assert.True(derivationVM.Confirmation);
|
||||
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
|
||||
|
||||
// Now let's check that no data has been lost in the process
|
||||
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
|
||||
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks).OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
|
||||
DerivationSchemeSettings.TryParseFromColdcard(content, onchainBTC.Network, out var expected);
|
||||
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
|
||||
|
||||
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
var wallet = tester.PayTester.GetController<WalletsController>();
|
||||
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendModel()
|
||||
{
|
||||
Amount = 0.5m,
|
||||
Destination = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork).ToString(),
|
||||
FeeSatoshiPerByte = 1
|
||||
}, default).GetAwaiter().GetResult();
|
||||
|
||||
Assert.NotNull(psbt);
|
||||
|
||||
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey().AsHDKeyCache();
|
||||
Assert.All(psbt.PSBT.Inputs, input =>
|
||||
{
|
||||
var keyPath = input.HDKeyPaths.Single();
|
||||
Assert.StartsWith(onchainBTC.AccountKeyPath.ToString(), keyPath.Value.Item2.ToString());
|
||||
Assert.Equal(root.Derive(keyPath.Value.Item2).GetPublicKey(), keyPath.Key);
|
||||
Assert.Equal(keyPath.Value.Item1, onchainBTC.RootFingerprint.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1992,8 +2080,8 @@ donation:
|
||||
var firstPayment = productPartDue - missingMoney;
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
// Check that for the second payment, network fee are included
|
||||
due = Money.Parse(invoice.CryptoInfo[0].Due);
|
||||
@ -2574,6 +2662,34 @@ donation:
|
||||
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void ParseDerivationSchemeSettings()
|
||||
{
|
||||
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork("BTC");
|
||||
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey();
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"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}", mainnet, out var settings));
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.RootFingerprint);
|
||||
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
|
||||
Assert.Equal("49'/0'/0'", settings.AccountKeyPath.ToString());
|
||||
Assert.Equal("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD", settings.AccountOriginal);
|
||||
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, settings.AccountDerivation.Derive(new KeyPath()).ScriptPubKey);
|
||||
|
||||
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork("BTC");
|
||||
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
|
||||
// Should be segwit
|
||||
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]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CheckParseStatusMessageModel()
|
||||
@ -2721,42 +2837,5 @@ donation:
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
|
||||
}
|
||||
|
||||
public static class TestUtils
|
||||
{
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.39
|
||||
image: nicolasdorier/nbxplorer:2.0.0.40
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -10,6 +10,12 @@ using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public enum DerivationType
|
||||
{
|
||||
Legacy,
|
||||
SegwitP2SH,
|
||||
Segwit
|
||||
}
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
@ -44,7 +50,6 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
@ -64,7 +69,8 @@ namespace BTCPayServer
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public bool SupportRBF { get; internal set; }
|
||||
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
|
@ -25,7 +25,22 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : 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}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ namespace BTCPayServer
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
|
||||
: new KeyPath("1'"),
|
||||
MinFee = Money.Satoshis(1m)
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ namespace BTCPayServer
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,20 +10,21 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Groestlcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
|
||||
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
|
||||
|
@ -17,14 +17,30 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://live.blockcypher.com/ltc/tx/{0}/"
|
||||
: "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
|
||||
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy },
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH },
|
||||
{0x04b24746U, DerivationType.Segwit },
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy },
|
||||
{0x044a5262U, DerivationType.SegwitP2SH },
|
||||
{0x045f1cf6U, DerivationType.Segwit }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,22 @@ namespace BTCPayServer
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks)
|
||||
{
|
||||
if(network.Value.ElectrumMapping.Count == 0)
|
||||
{
|
||||
network.Value.ElectrumMapping = GetNetwork("BTC").ElectrumMapping;
|
||||
if (!network.Value.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
{
|
||||
network.Value.ElectrumMapping =
|
||||
network.Value.ElectrumMapping
|
||||
.Where(kv => kv.Value == DerivationType.Legacy)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.96</Version>
|
||||
<Version>1.0.3.100</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -47,10 +47,11 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.14" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.11" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
@ -74,7 +75,7 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
|
||||
<PackageReference Include="U2F.Core" Version="2.0.1" />
|
||||
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -181,6 +182,15 @@
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBT.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletRescan.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
|
@ -58,30 +58,32 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
|
||||
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
|
||||
{
|
||||
StatusMessage = "Device added!"
|
||||
});
|
||||
}
|
||||
|
||||
throw new Exception("Could not add device.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = e.Message
|
||||
}
|
||||
});
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -40,6 +43,14 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
class GetXPubs
|
||||
{
|
||||
public BitcoinExtPubKey ExtPubKey { get; set; }
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
public HDFingerprint RootFingerprint { get; set; }
|
||||
public string Source { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
|
||||
public async Task<IActionResult> AddDerivationSchemeLedger(
|
||||
@ -52,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
|
||||
@ -70,7 +81,18 @@ namespace BTCPayServer.Controllers
|
||||
var k = KeyPath.Parse(keyPath);
|
||||
if (k.Indexes.Length == 0)
|
||||
throw new FormatException("Invalid key path");
|
||||
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
|
||||
var getxpubResult = new GetXPubs();
|
||||
getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = segwit,
|
||||
Legacy = !segwit
|
||||
});
|
||||
getxpubResult.DerivationScheme = derivation;
|
||||
getxpubResult.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).ExtPubKey.PubKey.GetHDFingerPrint();
|
||||
getxpubResult.Source = hw.Device;
|
||||
result = getxpubResult;
|
||||
}
|
||||
}
|
||||
@ -84,7 +106,7 @@ namespace BTCPayServer.Controllers
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings));
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
@ -99,22 +121,30 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
|
||||
if (derivation != null)
|
||||
{
|
||||
vm.DerivationScheme = derivation.AccountDerivation.ToString();
|
||||
vm.Config = derivation.ToJson();
|
||||
}
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm,
|
||||
string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -126,45 +156,92 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
DerivationSchemeSettings strategy = null;
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(c => c.DerivationStrategyBase.ToString())
|
||||
.FirstOrDefault();
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
if (!string.IsNullOrEmpty(vm.Config))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Config file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
if (vm.ColdcardPublicFile != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy))
|
||||
{
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Coldcard public file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
strategy = newStrategy;
|
||||
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strategy = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var oldConfig = vm.Config;
|
||||
vm.Config = strategy == null ? null : strategy.ToJson();
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is clicking on continue after changing the config
|
||||
(!vm.Confirmation && oldConfig != vm.Config) ||
|
||||
// - The user is clickingon continue without changing config nor enabling/disabling
|
||||
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
|
||||
|
||||
showAddress = showAddress && strategy != null;
|
||||
if (!showAddress)
|
||||
@ -172,10 +249,9 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
await wallet.TrackAsync(strategy.AccountDerivation);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
@ -185,8 +261,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
if (oldConfig != vm.Config)
|
||||
StatusMessage = $"Derivation settings for {network.CryptoCode} has been modified.";
|
||||
if (willBeExcluded != wasExcluded)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
StatusMessage = $"On-Chain payments for {network.CryptoCode} has been {label}.";
|
||||
}
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
@ -203,27 +285,43 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
|
||||
{
|
||||
strategy.AccountDerivation = newStrategy.AccountDerivation;
|
||||
strategy.AccountOriginal = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
vm.StatusMessage =
|
||||
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
using (var stream = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
return await stream.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
var line = strategy.AccountDerivation.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
|
@ -470,7 +470,7 @@ namespace BTCPayServer.Controllers
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.ToDictionary(c => c.Network.CryptoCode);
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
@ -478,7 +478,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||
Value = strategy?.ToPrettyString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
|
||||
});
|
||||
@ -596,11 +596,11 @@ namespace BTCPayServer.Controllers
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class WalletsController
|
||||
{
|
||||
|
||||
[NonAction]
|
||||
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
if (network.SupportRBF)
|
||||
{
|
||||
psbtRequest.RBF = !sendModel.DisableRBF;
|
||||
}
|
||||
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(sendModel.Amount.Value);
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
}
|
||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
|
||||
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
|
||||
if (psbt == null)
|
||||
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||
return psbt;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt")]
|
||||
public IActionResult WalletPSBT()
|
||||
{
|
||||
return View(new WalletPSBTViewModel());
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
public async Task<IActionResult> WalletPSBT(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
vm.Decoded = psbt.ToString();
|
||||
return View(vm);
|
||||
}
|
||||
else if (command == "ledger")
|
||||
{
|
||||
return ViewWalletSendLedger(psbt);
|
||||
}
|
||||
else if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
return ViewPSBT(psbt, errors);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "combine")
|
||||
{
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
|
||||
return FilePSBT(psbt, "psbt-export.psbt");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public IActionResult WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string psbt = null)
|
||||
{
|
||||
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
|
||||
{
|
||||
PSBT psbt = null;
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.AddRange(errors.Select(e => e.ToString()));
|
||||
return View(vm);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Error while broadcasting: " + ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return ViewPSBT(psbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Unknown command");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
|
||||
{
|
||||
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
|
||||
}
|
||||
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
|
||||
{
|
||||
return View(nameof(WalletPSBT), new WalletPSBTViewModel()
|
||||
{
|
||||
Decoded = psbt.ToString(),
|
||||
PSBT = psbt.ToBase64(),
|
||||
Errors = errors?.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private IActionResult FilePSBT(PSBT psbt, string fileName)
|
||||
{
|
||||
return File(psbt.ToBytes(), "application/octet-stream", fileName);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/combine")]
|
||||
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
StatusMessage = "PSBT Successfully combined!";
|
||||
return ViewPSBT(sourcePSBT);
|
||||
}
|
||||
}
|
||||
}
|
@ -80,9 +80,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
DerivationStrategy: d.AccountDerivation,
|
||||
Network: d.Network)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(_ => (Wallet: _.Wallet,
|
||||
@ -117,13 +117,12 @@ namespace BTCPayServer.Controllers
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
@ -151,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
@ -171,11 +170,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
@ -212,7 +211,7 @@ namespace BTCPayServer.Controllers
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
|
||||
if (destination == null)
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
|
||||
@ -227,76 +226,44 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
var sendModel = new WalletSendLedgerModel()
|
||||
{
|
||||
Destination = vm.Destination,
|
||||
Amount = vm.Amount.Value,
|
||||
SubstractFees = vm.SubstractFees,
|
||||
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
|
||||
NoChange = vm.NoChange
|
||||
};
|
||||
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||
var psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
|
||||
|
||||
if (command == "ledger")
|
||||
{
|
||||
return RedirectToAction(nameof(WalletSendLedger), sendModel);
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||
var psbt = await CreatePSBT(network, derivationScheme, sendModel, cancellation);
|
||||
return File(psbt.PSBT.ToBytes(), "application/octet-stream", $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationStrategyBase derivationScheme, WalletSendLedgerModel sendModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(sendModel.Amount);
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
}
|
||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||
|
||||
var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken));
|
||||
if (psbt == null)
|
||||
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||
|
||||
if (network.MinFee != null)
|
||||
{
|
||||
psbt.PSBT.TryGetFee(out var fee);
|
||||
if (fee < network.MinFee)
|
||||
try
|
||||
{
|
||||
psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee };
|
||||
psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken));
|
||||
if (command == "analyze-psbt")
|
||||
return ViewPSBT(psbt.PSBT);
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger")]
|
||||
public async Task<IActionResult> WalletSendLedger(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendLedgerModel vm)
|
||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
return View(vm);
|
||||
return View("WalletSendLedger", new WalletSendLedgerModel()
|
||||
{
|
||||
PSBT = psbt.ToBase64(),
|
||||
HintChange = hintChange?.ToString(),
|
||||
WebsocketPath = this.Url.Action(nameof(LedgerConnection)),
|
||||
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
||||
});
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
@ -312,6 +279,19 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (transaction != null)
|
||||
{
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var derivationSettings = await GetDerivationSchemeSettings(walletId);
|
||||
wallet.InvalidateCache(derivationSettings.AccountDerivation);
|
||||
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})";
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
@ -320,8 +300,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
@ -330,7 +309,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
|
||||
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
|
||||
if (scanProgress != null)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
@ -364,14 +343,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
@ -393,18 +371,24 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
|
||||
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId, StoreData store)
|
||||
{
|
||||
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private async Task<DerivationSchemeSettings> GetDerivationSchemeSettings(WalletId walletId)
|
||||
{
|
||||
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
return GetDerivationSchemeSettings(walletId, store);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
@ -425,17 +409,6 @@ namespace BTCPayServer.Controllers
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/success")]
|
||||
public IActionResult WalletSendLedgerSuccess(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
string txid)
|
||||
{
|
||||
StatusMessage = $"Transaction broadcasted ({txid})";
|
||||
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/ws")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
@ -446,16 +419,18 @@ namespace BTCPayServer.Controllers
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
bool noChange = false,
|
||||
string destination = null, string amount = null, string feeRate = null, bool substractFees = false
|
||||
string psbt = null,
|
||||
string hintChange = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
|
||||
var cryptoCode = walletId.CryptoCode;
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||
var derivationSettings = GetDerivationSchemeSettings(walletId, storeData);
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
@ -463,54 +438,11 @@ namespace BTCPayServer.Controllers
|
||||
using (var signTimeout = new CancellationTokenSource())
|
||||
{
|
||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
var model = new WalletSendLedgerModel();
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
|
||||
model.Destination = destination.Trim();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
model.FeeSatoshiPerByte = int.Parse(feeRate, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch { }
|
||||
if (model.FeeSatoshiPerByte <= 0)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
model.Amount = Money.Parse(amount).ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
catch { }
|
||||
if (model.Amount <= 0m)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
model.SubstractFees = substractFees;
|
||||
model.NoChange = noChange;
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
@ -520,61 +452,58 @@ namespace BTCPayServer.Controllers
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
|
||||
var psbt = await CreatePSBT(network, derivationScheme, model, normalOperationTimeout.Token);
|
||||
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||
// Some deployment have the wallet root key path saved in the store blob
|
||||
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
|
||||
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
|
||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||
if (derivationSettings.AccountKeyPath == null)
|
||||
{
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
|
||||
storeData.SetStoreBlob(storeBlob);
|
||||
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
||||
derivationSettings.AccountDerivation,
|
||||
normalOperationTimeout.Token);
|
||||
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
// If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
|
||||
else
|
||||
{
|
||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||
// but some deployment does not have it, so let's use AccountKeyPath instead
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
|
||||
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
|
||||
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
// We have the root fingerprint, we can check the root from it
|
||||
else
|
||||
{
|
||||
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
}
|
||||
|
||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
|
||||
// NBX only know the path relative to the account xpub.
|
||||
// Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign
|
||||
// Note that the fingerprint of the hd keys are now 0, which is wrong
|
||||
// However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway.
|
||||
foreach (var o in psbt.PSBT.Inputs.OfType<PSBTCoin>().Concat(psbt.PSBT.Outputs))
|
||||
var psbtResponse = new CreatePSBTResponse()
|
||||
{
|
||||
foreach (var keypath in o.HDKeyPaths.ToList())
|
||||
{
|
||||
var newKeyPath = foundKeyPath.Derive(keypath.Value.Item2);
|
||||
o.HDKeyPaths.Remove(keypath.Key);
|
||||
o.HDKeyPaths.Add(keypath.Key, Tuple.Create(default(HDFingerprint), newKeyPath));
|
||||
}
|
||||
}
|
||||
PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork),
|
||||
ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork)
|
||||
};
|
||||
|
||||
|
||||
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
||||
|
||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
if(!psbt.PSBT.TryFinalize(out var errors))
|
||||
{
|
||||
throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})");
|
||||
}
|
||||
var transaction = psbt.PSBT.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
wallet.InvalidateCache(derivationScheme);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@ -599,16 +528,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new Exception("The derivation scheme is not provided");
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
return directStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -618,6 +537,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
[JsonProperty("psbt")]
|
||||
public string PSBT { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Data
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
yield return DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +98,11 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public void SetSupportedPaymentMethod(ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
SetSupportedPaymentMethod(null, supportedPaymentMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set or remove a new supported payment method for the store
|
||||
/// </summary>
|
||||
@ -105,8 +110,16 @@ namespace BTCPayServer.Data
|
||||
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
|
||||
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
throw new InvalidOperationException("Argument mismatch");
|
||||
if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
{
|
||||
throw new InvalidOperationException("Incoherent arguments, this should never happen");
|
||||
}
|
||||
if (supportedPaymentMethod == null && paymentMethodId == null)
|
||||
throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified");
|
||||
if (supportedPaymentMethod != null && paymentMethodId == null)
|
||||
{
|
||||
paymentMethodId = supportedPaymentMethod.PaymentId;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
@ -134,7 +147,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -430,23 +443,9 @@ namespace BTCPayServer.Data
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null)
|
||||
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
|
||||
else
|
||||
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
|
||||
}
|
||||
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
|
||||
return KeyPath.Parse(k);
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
[Obsolete("Use DerivationSchemeSettings instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; }
|
||||
|
||||
public EmailSettings EmailSettings { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
@ -12,14 +12,51 @@ namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public BTCPayNetwork BtcPayNetwork { get; }
|
||||
|
||||
public Network Network => BtcPayNetwork.NBitcoinNetwork;
|
||||
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
Dictionary<uint, string[]> ElectrumMapping = new Dictionary<uint, string[]>();
|
||||
|
||||
public DerivationSchemeParser(BTCPayNetwork expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
if (expectedNetwork == null)
|
||||
throw new ArgumentNullException(nameof(expectedNetwork));
|
||||
BtcPayNetwork = expectedNetwork;
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase ParseElectrum(string str)
|
||||
{
|
||||
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(str);
|
||||
if (data.Length < 4)
|
||||
throw new FormatException();
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network);
|
||||
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
throw new FormatException();
|
||||
}
|
||||
if (type == DerivationType.Segwit)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
|
||||
if (type == DerivationType.Legacy)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
|
||||
if (type == DerivationType.SegwitP2SH)
|
||||
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
@ -41,7 +78,7 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
if(!Network.Consensus.SupportSegwit)
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
|
||||
try
|
||||
@ -53,15 +90,6 @@ namespace BTCPayServer
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
bool hasLabel = false;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
@ -84,17 +112,22 @@ namespace BTCPayServer
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString();
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
foreach (var label in labels)
|
||||
switch (type)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
case DerivationType.Legacy:
|
||||
hintedLabels.Add("legacy");
|
||||
break;
|
||||
case DerivationType.SegwitP2SH:
|
||||
hintedLabels.Add("p2sh");
|
||||
break;
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
@ -136,7 +169,7 @@ namespace BTCPayServer
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
@ -149,20 +182,20 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeSettings : ISupportedPaymentMethod
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
strategy = null;
|
||||
try
|
||||
{
|
||||
strategy = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(config);
|
||||
strategy.Network = network;
|
||||
}
|
||||
catch { }
|
||||
return strategy != null;
|
||||
}
|
||||
|
||||
public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||
{
|
||||
settings = null;
|
||||
if (coldcardExport == null)
|
||||
throw new ArgumentNullException(nameof(coldcardExport));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Source = "Coldcard";
|
||||
var derivationSchemeParser = new DerivationSchemeParser(network);
|
||||
JObject jobj = null;
|
||||
try
|
||||
{
|
||||
jobj = JObject.Parse(coldcardExport);
|
||||
jobj = (JObject)jobj["keystore"];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("xpub"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("label"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = jobj["label"].Value<string>();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("ckcc_xfp"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("derivation"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
settings = result;
|
||||
settings.Network = network;
|
||||
return true;
|
||||
}
|
||||
|
||||
public DerivationSchemeSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
AccountDerivation = derivationStrategy;
|
||||
Network = network;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Source { get; set; }
|
||||
public KeyPath AccountKeyPath { get; set; }
|
||||
|
||||
public DerivationStrategyBase AccountDerivation { get; set; }
|
||||
public string AccountOriginal { get; set; }
|
||||
|
||||
public HDFingerprint? RootFingerprint { get; set; }
|
||||
|
||||
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public BitcoinExtPubKey AccountKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return ExplicitAccountKey ?? new BitcoinExtPubKey(AccountDerivation.GetExtPubKeys().First(), Network.NBitcoinNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
||||
{
|
||||
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
|
||||
{
|
||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||
{
|
||||
AccountKey = AccountKey,
|
||||
AccountKeyPath = AccountKeyPath,
|
||||
MasterFingerprint = fp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return AccountDerivation.ToString();
|
||||
}
|
||||
public string ToPrettyString()
|
||||
{
|
||||
return !string.IsNullOrEmpty(Label) ? Label :
|
||||
!String.IsNullOrEmpty(AccountOriginal) ? AccountOriginal :
|
||||
ToString();
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return Network.NBXplorerNetwork.Serializer.ToString(this);
|
||||
}
|
||||
|
||||
public void RebaseKeyPaths(PSBT psbt)
|
||||
{
|
||||
foreach (var rebase in GetPSBTRebaseKeyRules())
|
||||
{
|
||||
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy : ISupportedPaymentMethod
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
}
|
||||
|
||||
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationStrategy(result, network);
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _DerivationStrategy.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -71,6 +71,12 @@ namespace BTCPayServer.HostedServices
|
||||
settings.ConvertCrowdfundOldSettings = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertWalletKeyPathRoots)
|
||||
{
|
||||
await ConvertConvertWalletKeyPathRoots();
|
||||
settings.ConvertWalletKeyPathRoots = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -79,6 +85,35 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertConvertWalletKeyPathRoots()
|
||||
{
|
||||
bool save = false;
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.WalletKeyPathRoots == null)
|
||||
continue;
|
||||
foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<DerivationSchemeSettings>())
|
||||
{
|
||||
if (blob.WalletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), out var root))
|
||||
{
|
||||
scheme.AccountKeyPath = new NBitcoin.KeyPath(root);
|
||||
store.SetSupportedPaymentMethod(scheme);
|
||||
save = true;
|
||||
}
|
||||
}
|
||||
blob.WalletKeyPathRoots = null;
|
||||
store.SetStoreBlob(blob);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
if (save)
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertCrowdfundOldSettings()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
@ -159,7 +194,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
store.SetSupportedPaymentMethod(method.PaymentId, method);
|
||||
store.SetSupportedPaymentMethod(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationSchemeSettings>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
|
||||
@ -25,6 +26,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
public string KeyPath { get; set; }
|
||||
public string RootFingerprint { get; set; }
|
||||
[Display(Name = "Hint address")]
|
||||
public string HintAddress { get; set; }
|
||||
public bool Confirmation { get; set; }
|
||||
@ -32,5 +34,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public string StatusMessage { get; internal set; }
|
||||
public KeyPath RootKeyPath { get; set; }
|
||||
|
||||
[Display(Name = "Coldcard Wallet File")]
|
||||
public IFormFile ColdcardPublicFile{ get; set; }
|
||||
public string Config { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string AccountKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletPSBTCombineViewModel
|
||||
{
|
||||
public string OtherPSBT { get; set; }
|
||||
[Display(Name = "PSBT to combine with...")]
|
||||
public string PSBT { get; set; }
|
||||
[Display(Name = "Upload PSBT from file...")]
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
public PSBT GetSourcePSBT(Network network)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OtherPSBT))
|
||||
{
|
||||
try
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(OtherPSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
if (UploadedPSBTFile.Length > 500 * 1024)
|
||||
return null;
|
||||
byte[] bytes = new byte[UploadedPSBTFile.Length];
|
||||
using (var stream = UploadedPSBTFile.OpenReadStream())
|
||||
{
|
||||
await stream.ReadAsync(bytes, 0, (int)UploadedPSBTFile.Length);
|
||||
}
|
||||
try
|
||||
{
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(PSBT))
|
||||
{
|
||||
try
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletPSBTReadyViewModel
|
||||
{
|
||||
public string PSBT { get; set; }
|
||||
public List<string> Errors { get; set; }
|
||||
}
|
||||
}
|
25
BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs
Normal file
25
BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletPSBTViewModel
|
||||
{
|
||||
public string Decoded { get; set; }
|
||||
public string PSBT { get; set; }
|
||||
public List<string> Errors { get; set; } = new List<string>();
|
||||
|
||||
internal PSBT GetPSBT(Network network)
|
||||
{
|
||||
try
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,10 +7,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSendLedgerModel
|
||||
{
|
||||
public int FeeSatoshiPerByte { get; set; }
|
||||
public bool SubstractFees { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public bool NoChange { get; set; }
|
||||
public string WebsocketPath { get; set; }
|
||||
public string PSBT { get; set; }
|
||||
public string HintChange { get; set; }
|
||||
public string SuccessPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -35,5 +35,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public int Divisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
public string RateError { get; set; }
|
||||
public bool SupportRBF { get; set; }
|
||||
[Display(Name = "Disable RBF")]
|
||||
public bool DisableRBF { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase<DerivationStrategy>
|
||||
public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase<DerivationSchemeSettings>
|
||||
{
|
||||
ExplorerClientProvider _ExplorerProvider;
|
||||
private IFeeProviderFactory _FeeRateProviderFactory;
|
||||
@ -33,16 +33,16 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
public Task<BitcoinAddress> ReserveAddress;
|
||||
}
|
||||
|
||||
public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network)
|
||||
public override object PreparePayment(DerivationSchemeSettings supportedPaymentMethod, StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
return new Prepare()
|
||||
{
|
||||
GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
|
||||
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase)
|
||||
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.AccountDerivation)
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
if (!_ExplorerProvider.IsAvailable(network))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
@ -345,8 +345,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(InvoiceEntity invoice, BTCPayNetwork network)
|
||||
{
|
||||
return invoice.GetSupportedPaymentMethod<DerivationStrategy>(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), _ExplorerClients.NetworkProviders)
|
||||
.Select(d => d.DerivationStrategyBase)
|
||||
return invoice.GetSupportedPaymentMethod<DerivationSchemeSettings>(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), _ExplorerClients.NetworkProviders)
|
||||
.Select(d => d.AccountDerivation)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
|
@ -12,10 +12,19 @@ namespace BTCPayServer.Payments
|
||||
{
|
||||
public static ISupportedPaymentMethod Deserialize(PaymentMethodId paymentMethodId, JToken value, BTCPayNetwork network)
|
||||
{
|
||||
// Legacy
|
||||
if (paymentMethodId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
return BTCPayServer.DerivationStrategy.Parse(((JValue)value).Value<string>(), network);
|
||||
if (value is JObject jobj)
|
||||
{
|
||||
var scheme = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(jobj);
|
||||
scheme.Network = network;
|
||||
return scheme;
|
||||
}
|
||||
// Legacy
|
||||
else
|
||||
{
|
||||
return BTCPayServer.DerivationSchemeSettings.Parse(((JValue)value).Value<string>(), network);
|
||||
}
|
||||
}
|
||||
//////////
|
||||
else if (paymentMethodId.PaymentType == PaymentTypes.LightningLike)
|
||||
@ -44,7 +53,9 @@ namespace BTCPayServer.Payments
|
||||
// Legacy
|
||||
if (factory.PaymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
return new JValue(((DerivationStrategy)factory).DerivationStrategyBase.ToString());
|
||||
var derivation = (DerivationSchemeSettings)factory;
|
||||
var str = derivation.Network.NBXplorerNetwork.Serializer.ToString(derivation);
|
||||
return JObject.Parse(str);
|
||||
}
|
||||
//////////////
|
||||
else
|
||||
|
@ -20,125 +20,30 @@ namespace BTCPayServer.Services
|
||||
public HardwareWalletException(string message) : base(message) { }
|
||||
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
public class HardwareWalletService : IDisposable
|
||||
public abstract class HardwareWalletService : IDisposable
|
||||
{
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
||||
public abstract string Device { get; }
|
||||
public abstract Task<LedgerTestResult> Test(CancellationToken cancellation);
|
||||
|
||||
public abstract Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation);
|
||||
public virtual async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
private readonly WebSocket webSocket;
|
||||
|
||||
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
||||
{
|
||||
if (webSocket == null)
|
||||
throw new ArgumentNullException(nameof(webSocket));
|
||||
this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync();
|
||||
List<byte[]> responses = new List<byte[]>();
|
||||
try
|
||||
{
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Semaphore.Release();
|
||||
}
|
||||
return responses.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Semaphore.Dispose();
|
||||
}
|
||||
return (await GetExtPubKey(network, keyPath, cancellation)).GetPublicKey();
|
||||
}
|
||||
|
||||
private readonly LedgerClient _Ledger;
|
||||
public LedgerClient Ledger
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Ledger;
|
||||
}
|
||||
}
|
||||
WebSocketTransport _Transport = null;
|
||||
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
|
||||
{
|
||||
if (ledgerWallet == null)
|
||||
throw new ArgumentNullException(nameof(ledgerWallet));
|
||||
_Transport = new WebSocketTransport(ledgerWallet);
|
||||
_Ledger = new LedgerClient(_Transport);
|
||||
_Ledger.MaxAPDUSize = 90;
|
||||
}
|
||||
|
||||
public async Task<LedgerTestResult> Test(CancellationToken cancellation)
|
||||
{
|
||||
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
|
||||
return new LedgerTestResult() { Success = true };
|
||||
}
|
||||
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
|
||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||
var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = segwit,
|
||||
Legacy = !segwit
|
||||
});
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath };
|
||||
}
|
||||
|
||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
|
||||
try
|
||||
{
|
||||
pubKey.GetAddress(network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
||||
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
|
||||
}
|
||||
var fingerprint = onlyChaincode ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
|
||||
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||
return extpubkey;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new HardwareWalletException("Unsupported ledger app");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation);
|
||||
return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey;
|
||||
}
|
||||
|
||||
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
||||
public async Task<KeyPath> FindKeyPathFromDerivation(BTCPayNetwork network, DerivationStrategyBase derivationScheme, CancellationToken cancellation)
|
||||
{
|
||||
var pubKeys = derivationScheme.GetExtPubKeys().Select(k => k.GetPublicKey()).ToArray();
|
||||
var derivation = derivationScheme.Derive(new KeyPath(0));
|
||||
List<KeyPath> derivations = new List<KeyPath>();
|
||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
derivations.Add(new KeyPath("49'"));
|
||||
{
|
||||
if (derivation.Redeem?.IsWitness is true ||
|
||||
derivation.ScriptPubKey.IsWitness) // Native or p2sh segwit
|
||||
derivations.Add(new KeyPath("49'"));
|
||||
if (derivation.Redeem == null && derivation.ScriptPubKey.IsWitness) // Native segwit
|
||||
derivations.Add(new KeyPath("84'"));
|
||||
}
|
||||
derivations.Add(new KeyPath("44'"));
|
||||
KeyPath foundKeyPath = null;
|
||||
foreach (var account in
|
||||
@ -146,70 +51,22 @@ namespace BTCPayServer.Services
|
||||
.Select(purpose => purpose.Derive(network.CoinType))
|
||||
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
|
||||
{
|
||||
try
|
||||
var pubkey = await GetPubKey(network, account, cancellation);
|
||||
if (pubKeys.Contains(pubkey))
|
||||
{
|
||||
var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation);
|
||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
||||
{
|
||||
foundKeyPath = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
|
||||
foundKeyPath = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return foundKeyPath;
|
||||
}
|
||||
|
||||
public async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unsigned = psbt.GetGlobalTransaction();
|
||||
var changeKeyPath = psbt.Outputs
|
||||
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
||||
.FirstOrDefault();
|
||||
var signatureRequests = psbt
|
||||
.Inputs
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
||||
.Select(i => new SignatureRequest()
|
||||
{
|
||||
InputCoin = i.GetSignableCoin(),
|
||||
InputTransaction = i.NonWitnessUtxo,
|
||||
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
||||
PubKey = i.HDKeyPaths.First().Key
|
||||
}).ToArray();
|
||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
if (signedTransaction == null)
|
||||
throw new Exception("The ledger failed to sign the transaction");
|
||||
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
psbt = psbt.Clone();
|
||||
foreach (var signature in signatureRequests)
|
||||
{
|
||||
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
|
||||
if (input == null)
|
||||
continue;
|
||||
input.PartialSigs.Add(signature.PubKey, signature.Signature);
|
||||
}
|
||||
return psbt;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("The ledger failed to sign the transaction", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (_Transport != null)
|
||||
_Transport.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,11 +75,4 @@ namespace BTCPayServer.Services
|
||||
public bool Success { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
public class GetXPubResult
|
||||
{
|
||||
public string ExtPubKey { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
|
||||
public KeyPath KeyPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
yield return BTCPayServer.DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
@ -238,8 +238,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
obj.Add(strat.PaymentId.ToString(), PaymentMethodExtensions.Serialize(strat));
|
||||
#pragma warning disable CS0618
|
||||
if (strat.PaymentId.IsBTCOnChain)
|
||||
DerivationStrategy = ((JValue)PaymentMethodExtensions.Serialize(strat)).Value<string>();
|
||||
// This field should eventually disappear
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
DerivationStrategies = JsonConvert.SerializeObject(obj);
|
||||
#pragma warning restore CS0618
|
||||
|
160
BTCPayServer/Services/LedgerHardwareWalletService.cs
Normal file
160
BTCPayServer/Services/LedgerHardwareWalletService.cs
Normal file
@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LedgerWallet;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class LedgerHardwareWalletService : HardwareWalletService
|
||||
{
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
||||
{
|
||||
private readonly WebSocket webSocket;
|
||||
|
||||
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
||||
{
|
||||
if (webSocket == null)
|
||||
throw new ArgumentNullException(nameof(webSocket));
|
||||
this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync();
|
||||
List<byte[]> responses = new List<byte[]>();
|
||||
try
|
||||
{
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Semaphore.Release();
|
||||
}
|
||||
return responses.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LedgerClient _Ledger;
|
||||
public LedgerClient Ledger
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Ledger;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Device => "Ledger wallet";
|
||||
|
||||
WebSocketTransport _Transport = null;
|
||||
public LedgerHardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
|
||||
{
|
||||
if (ledgerWallet == null)
|
||||
throw new ArgumentNullException(nameof(ledgerWallet));
|
||||
_Transport = new WebSocketTransport(ledgerWallet);
|
||||
_Ledger = new LedgerClient(_Transport);
|
||||
_Ledger.MaxAPDUSize = 90;
|
||||
}
|
||||
|
||||
public override async Task<LedgerTestResult> Test(CancellationToken cancellation)
|
||||
{
|
||||
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
|
||||
return new LedgerTestResult() { Success = true };
|
||||
}
|
||||
|
||||
public override async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return await GetExtPubKey(network, keyPath, false, cancellation);
|
||||
}
|
||||
public override async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return (await GetExtPubKey(network, keyPath, false, cancellation)).GetPublicKey();
|
||||
}
|
||||
|
||||
private async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
||||
{
|
||||
var pubKey = await Ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
|
||||
try
|
||||
{
|
||||
pubKey.GetAddress(network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
||||
throw new HardwareWalletException($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
|
||||
}
|
||||
var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await Ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
|
||||
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(),
|
||||
pubKey.ChainCode,
|
||||
(byte)account.Indexes.Length,
|
||||
parentFP,
|
||||
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||
return extpubkey;
|
||||
}
|
||||
|
||||
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken)
|
||||
{
|
||||
var unsigned = psbt.GetGlobalTransaction();
|
||||
var changeKeyPath = psbt.Outputs
|
||||
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
||||
.FirstOrDefault();
|
||||
var signatureRequests = psbt
|
||||
.Inputs
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
||||
.Select(i => new SignatureRequest()
|
||||
{
|
||||
InputCoin = i.GetSignableCoin(),
|
||||
InputTransaction = i.NonWitnessUtxo,
|
||||
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
||||
PubKey = i.HDKeyPaths.First().Key
|
||||
}).ToArray();
|
||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
if (signedTransaction == null)
|
||||
throw new HardwareWalletException("The ledger failed to sign the transaction");
|
||||
|
||||
psbt = psbt.Clone();
|
||||
foreach (var signature in signatureRequests)
|
||||
{
|
||||
if (signature.Signature == null)
|
||||
continue;
|
||||
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
|
||||
if (input == null)
|
||||
continue;
|
||||
input.PartialSigs.Add(signature.PubKey, signature.Signature);
|
||||
}
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (_Transport != null)
|
||||
_Transport.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -12,5 +12,6 @@ namespace BTCPayServer.Services
|
||||
public bool ConvertMultiplierToSpread { get; set; }
|
||||
public bool ConvertNetworkFeeProperty { get; set; }
|
||||
public bool ConvertCrowdfundOldSettings { get; set; }
|
||||
public bool ConvertWalletKeyPathRoots { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -187,24 +187,26 @@ namespace BTCPayServer.Services.U2F
|
||||
var requests = new List<U2FDeviceAuthenticationRequest>();
|
||||
|
||||
|
||||
var challenge = global::U2F.Core.Crypto.U2F.GenerateChallenge();
|
||||
|
||||
var serverChallenges = new List<ServerChallenge>();
|
||||
foreach (var registeredDevice in devices)
|
||||
{
|
||||
serverChallenges.Add(new ServerChallenge
|
||||
{
|
||||
appId = appId,
|
||||
challenge = challenge,
|
||||
keyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(),
|
||||
version = global::U2F.Core.Crypto.U2F.U2FVersion,
|
||||
});
|
||||
|
||||
var challenge = global::U2F.Core.Crypto.U2F.StartAuthentication(appId,
|
||||
new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey,
|
||||
registeredDevice.AttestationCert, (uint)registeredDevice.Counter));
|
||||
serverChallenges.Add(new ServerChallenge()
|
||||
{
|
||||
challenge = challenge.Challenge,
|
||||
appId = challenge.AppId,
|
||||
version = challenge.Version,
|
||||
keyHandle = challenge.KeyHandle
|
||||
});
|
||||
|
||||
requests.Add(
|
||||
new U2FDeviceAuthenticationRequest()
|
||||
{
|
||||
AppId = appId,
|
||||
Challenge = challenge,
|
||||
Challenge = challenge.Challenge,
|
||||
KeyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(),
|
||||
Version = global::U2F.Core.Crypto.U2F.U2FVersion
|
||||
});
|
||||
|
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">PRS
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
|
@ -43,7 +43,10 @@
|
||||
<p><a href="https://play.google.com/store/apps/details?id=com.greenaddress.greenbits_android_wallet" target="_blank">Blockstream Green Wallet</a></p>
|
||||
</div>
|
||||
<div class="col-lg-3 mr-auto text-center">
|
||||
|
||||
<a href="https://www.wasabiwallet.io/" target="_blank">
|
||||
<img src="~/img/wasabi.png" height="100" />
|
||||
</a>
|
||||
<p><a href="https://www.wasabiwallet.io/" target="_blank">Wasabi Wallet</a> <a href="https://www.reddit.com/r/WasabiWallet/comments/aqlyia/how_to_connect_wasabi_wallet_to_my_own_full/" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a></p>
|
||||
</div>
|
||||
<div class="col-lg-3 mr-auto text-center">
|
||||
|
||||
|
@ -14,6 +14,33 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="modal fade" id="coldcardimport" tabindex="-1" role="dialog" aria-labelledby="coldcardimport" aria-hidden="true">
|
||||
<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>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You may import your Coldcard wallet by exporting the public details from <kbd>Advanced->MicroSD Card->Electrum Wallet</kbd> and uploading it here.</p>
|
||||
<div class="form-group">
|
||||
<label asp-for="ColdcardPublicFile"></label>
|
||||
|
||||
<input type="file" class="form-control-file" asp-for="ColdcardPublicFile" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
@if (!Model.Confirmation)
|
||||
{
|
||||
@ -23,6 +50,10 @@
|
||||
</div>
|
||||
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
|
||||
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||
<input id="Source" asp-for="Source" type="hidden" />
|
||||
<input id="RootFingerprint" asp-for="RootFingerprint" type="hidden" />
|
||||
<input id="AccountKey" asp-for="AccountKey" type="hidden" />
|
||||
<input id="Config" asp-for="Config" type="hidden" />
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
@ -36,9 +67,14 @@
|
||||
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
|
||||
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
|
||||
</p>
|
||||
<div id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||
<div id="ledger-info" class="form-text text-muted display-when-ledger-connected">
|
||||
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
|
||||
<div class="dropdown">
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-primary mr-2 " data-toggle="modal" data-target="#coldcardimport">
|
||||
Import Coldcard wallet
|
||||
</button>
|
||||
<div class="dropdown display-when-ledger-connected">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="ledgerAccountsDropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Select ledger wallet account
|
||||
</button>
|
||||
@ -49,7 +85,9 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span>BTCPay format memo</span>
|
||||
@ -102,8 +140,12 @@
|
||||
</div>
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||
<input id="Source" asp-for="Source" type="hidden" />
|
||||
<input id="RootFingerprint" asp-for="RootFingerprint" type="hidden" />
|
||||
<input id="AccountKey" asp-for="AccountKey" type="hidden" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<input type="hidden" asp-for="Enabled" />
|
||||
<input id="Config" asp-for="Config" type="hidden" />
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
|
61
BTCPayServer/Views/Wallets/WalletPSBT.cshtml
Normal file
61
BTCPayServer/Views/Wallets/WalletPSBT.cshtml
Normal file
@ -0,0 +1,61 @@
|
||||
@model WalletPSBTViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "PSBT";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.PSBT);
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 text-center">
|
||||
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
@if (Model.Errors != null && Model.Errors.Count != 0)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
@foreach (var error in Model.Errors)
|
||||
{
|
||||
<span>@error</span><br />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Decoded))
|
||||
{
|
||||
<h3>Decoded PSBT</h3>
|
||||
<div class="form-group">
|
||||
<form method="post" asp-action="WalletPSBT">
|
||||
<div class="dropdown" style="margin-top:16px;">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Sign with...
|
||||
</button>
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT files</button>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="broadcast">Broadcast</button>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="combine">Combine with another PSBT</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<pre><code class="json">@Model.Decoded</code></pre>
|
||||
}
|
||||
<h3>PSBT to decode</h3>
|
||||
<form class="form-group" method="post" asp-action="WalletPSBT">
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
|
||||
<span asp-validation-for="PSBT" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Decode</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
|
||||
<script src="~/vendor/highlightjs/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
}
|
25
BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml
Normal file
25
BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@model WalletPSBTCombineViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "PSBT";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.PSBT);
|
||||
}
|
||||
|
||||
<h4>Combine PSBT</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form class="form-group" method="post" asp-action="WalletPSBTCombine">
|
||||
<input type="hidden" asp-for="OtherPSBT" />
|
||||
<div class="form-group">
|
||||
<label asp-for="PSBT"></label>
|
||||
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
|
||||
<span asp-validation-for="PSBT" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UploadedPSBTFile"></label>
|
||||
<input type="file" class="form-control-file" asp-for="UploadedPSBTFile">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Combine</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
35
BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml
Normal file
35
BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@model WalletPSBTReadyViewModel
|
||||
@{
|
||||
Layout = "../Shared/_Layout.cshtml";
|
||||
}
|
||||
<section>
|
||||
<div class="container">
|
||||
@if (Model.Errors != 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">×</span></button>
|
||||
@foreach (var error in Model.Errors)
|
||||
{
|
||||
<span>@error</span><br />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">Transaction signed</h2>
|
||||
<hr class="primary">
|
||||
<p>Your transaction has been signed, what do you want to do next?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post" asp-action="WalletPSBTReady">
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button> or
|
||||
<button type="submit" class="btn btn-primary" name="command" value="analyze-psbt">Export as PSBT</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -71,6 +71,14 @@
|
||||
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="NoChange" class="form-check" />
|
||||
</div>
|
||||
@if (Model.SupportRBF)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="DisableRBF"></label>
|
||||
<a href="https://bitcoin.org/en/glossary/rbf" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="DisableRBF" class="form-check" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,6 +90,7 @@
|
||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... analyze the PSBT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,18 +12,17 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<input type="hidden" asp-for="Destination" />
|
||||
<input type="hidden" asp-for="Amount" />
|
||||
<input type="hidden" asp-for="FeeSatoshiPerByte" />
|
||||
<input type="hidden" asp-for="SubstractFees" />
|
||||
<input type="hidden" asp-for="NoChange" />
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<input type="hidden" asp-for="HintChange" />
|
||||
<input type="hidden" asp-for="SuccessPath" />
|
||||
<input type="hidden" asp-for="WebsocketPath" />
|
||||
<p>
|
||||
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
|
||||
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />
|
||||
If your Ledger wallet is not detected:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Make sure you are running the Ledger app with version superior or equal to 1.2.4</li>
|
||||
<li>Make sure you are running the Ledger app with version superior or equal to <b>1.3.9</b></li>
|
||||
<li>Use Google Chrome browser and open the coin app on your Ledger</li>
|
||||
</ul>
|
||||
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Views.Wallets
|
||||
{
|
||||
Send,
|
||||
Transactions,
|
||||
Rescan
|
||||
Rescan,
|
||||
PSBT
|
||||
}
|
||||
}
|
||||
|
@ -4,5 +4,6 @@
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions">Transactions</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan">Rescan</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT">PSBT</a>
|
||||
</div>
|
||||
|
||||
|
BIN
BTCPayServer/wwwroot/img/wasabi.png
Normal file
BIN
BTCPayServer/wwwroot/img/wasabi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -24,7 +24,7 @@
|
||||
$("#ledger-loading").css("display", id === "ledger-loading" ? "block" : "none");
|
||||
$("#no-ledger-info").css("display", id === "no-ledger-info" ? "block" : "none");
|
||||
$("#ledger-validate").css("display", id === "ledger-validate" ? "block" : "none");
|
||||
$("#ledger-info").css("display", id === "ledger-info" ? "block" : "none");
|
||||
$(".display-when-ledger-connected").css("display", id === "ledger-info" ? "block" : "none");
|
||||
}
|
||||
function Write(prefix, type, message) {
|
||||
if (type === "error") {
|
||||
@ -48,7 +48,10 @@
|
||||
|
||||
showFeedback("ledger-info");
|
||||
|
||||
$("#DerivationScheme").val(result.extPubKey);
|
||||
$("#DerivationScheme").val(result.derivationScheme);
|
||||
$("#RootFingerprint").val(result.rootFingerprint);
|
||||
$("#AccountKey").val(result.extPubKey);
|
||||
$("#Source").val(result.source);
|
||||
$("#DerivationSchemeFormat").val("BTCPay");
|
||||
$("#KeyPath").val(keypath);
|
||||
})
|
||||
|
@ -1,10 +1,9 @@
|
||||
$(function () {
|
||||
var destination = $("#Destination").val();
|
||||
var amount = $("#Amount").val();
|
||||
var fee = $("#FeeSatoshiPerByte").val();
|
||||
var substractFee = $("#SubstractFees").val();
|
||||
var noChange = $("#NoChange").val();
|
||||
|
||||
var psbt = $("#PSBT").val();
|
||||
var hintChange = $("#HintChange").val();
|
||||
var successPath = $("#SuccessPath").val();
|
||||
var websocketPath = $("#WebsocketPath").val();
|
||||
|
||||
var loc = window.location, ws_uri;
|
||||
if (loc.protocol === "https:") {
|
||||
ws_uri = "wss:";
|
||||
@ -12,10 +11,7 @@
|
||||
ws_uri = "ws:";
|
||||
}
|
||||
ws_uri += "//" + loc.host;
|
||||
ws_uri += loc.pathname + "/ws";
|
||||
|
||||
var successCallback = loc.protocol + "//" + loc.host + loc.pathname + "/success";
|
||||
|
||||
ws_uri += websocketPath;
|
||||
var ledgerDetected = false;
|
||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(ws_uri);
|
||||
function WriteAlert(type, message) {
|
||||
@ -43,18 +39,10 @@
|
||||
return false;
|
||||
$(".crypto-info").css("display", "block");
|
||||
var args = "";
|
||||
args += "&destination=" + destination;
|
||||
args += "&amount=" + amount;
|
||||
args += "&feeRate=" + fee;
|
||||
args += "&substractFees=" + substractFee;
|
||||
args += "&noChange=" + noChange;
|
||||
|
||||
if (noChange === "True") {
|
||||
WriteAlert("warning", 'WARNING: Because you want to make sure no change UTXO is created, you will end up sending more than the chosen amount to your destination. Please validate the transaction on your ledger');
|
||||
}
|
||||
else {
|
||||
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||
}
|
||||
args += "&psbt=" + encodeURIComponent(psbt);
|
||||
args += "&hintChange=" + encodeURIComponent(hintChange);
|
||||
|
||||
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||
|
||||
var confirmButton = $("#confirm-button");
|
||||
confirmButton.prop("disabled", true);
|
||||
@ -74,8 +62,7 @@
|
||||
if (result.error) {
|
||||
WriteAlert("danger", result.error);
|
||||
} else {
|
||||
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
|
||||
window.location.replace(successCallback + "?txid=" + result.transactionId);
|
||||
window.location.replace(loc.protocol + "//" + loc.host + successPath + "?psbt=" + encodeURIComponent(result.psbt));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -6,7 +6,7 @@
|
||||
"Awaiting Payment...": "Esperando el pago...",
|
||||
"Pay with": "Pagar con",
|
||||
"Contact and Refund Email": "Contacto y correo electrónico de reembolso",
|
||||
"Contact_Body": "Por favor indica una dirección de correo electrónico a continuación. Nos pondremos en contacto contigo en esta dirección si hay algún problema con tu pago.",
|
||||
"Contact_Body": "Por favor indica una dirección de correo electrónico. Nos pondremos en contacto contigo en esta dirección si hay algún problema con tu pago.",
|
||||
"Your email": "Tu correo electrónico",
|
||||
"Continue": "Continuar",
|
||||
"Please enter a valid email address": "Por favor ingresa un correo electrónico válido",
|
||||
@ -18,12 +18,12 @@
|
||||
"Copy": "Copiar",
|
||||
"Conversion": "Conversión",
|
||||
"Open in wallet": "Abrir en la billetera",
|
||||
"CompletePay_Body": "Para completar tu pago, envía {{btcDue}} {{cryptoCode}} a la siguiente dirección:",
|
||||
"CompletePay_Body": "Para completar tu pago, envía exactamente {{btcDue}} {{cryptoCode}} a la siguiente dirección:",
|
||||
"Amount": "Cantidad",
|
||||
"Address": "Dirección",
|
||||
"Copied": "Copiado",
|
||||
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando altcoins que no soportamos directamente.",
|
||||
"ConversionTab_BodyDesc": "Este servicio es prestado por terceros. Ten en cuenta que no tenemos control sobre cómo ellos re-enviarán tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
|
||||
"ConversionTab_BodyDesc": "Este servicio es prestado por terceros. Ten en cuenta que no tenemos control sobre el reenvío de tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
|
||||
"ConversionTab_CalculateAmount_Error": "Reintentar",
|
||||
"ConversionTab_LoadCurrencies_Error": "Reintentar",
|
||||
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos con Lightning Network.",
|
||||
@ -32,7 +32,7 @@
|
||||
"Invoice expired": "La factura expiró",
|
||||
"What happened?": "¿Qué sucedió?",
|
||||
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \nPuedes regresar a {{storeName}} si deseas volver a enviar tu pago.",
|
||||
"InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.",
|
||||
"InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red. Todavía no hemos recibido tus fondos.",
|
||||
"InvoiceExpired_Body_3": "Si recibimos el pago despues, procesaremos tu orden o te contactaremos para un reembolso...",
|
||||
"Invoice ID": "ID de la factura",
|
||||
"Order ID": "ID del pedido",
|
||||
|
@ -19,3 +19,7 @@
|
||||
.only-for-js, .input-group-clear{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.display-when-ledger-connected{
|
||||
display: none;
|
||||
}
|
||||
|
Reference in New Issue
Block a user