Files
BTCPayServerPlugins/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/StoreLiquidController.cs
Kukks 9b90e10b66 wip
2024-10-18 09:46:16 +02:00

298 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Common;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using NBitcoin;
using NBXplorer;
using XX;
namespace BTCPayServer.Plugins.LiquidPlus.Controllers
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public class StoreLiquidController : Controller
{
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IExplorerClientProvider _explorerClientProvider;
public StoreLiquidController(PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
StoreRepository storeRepository, BTCPayNetworkProvider btcPayNetworkProvider,
IExplorerClientProvider explorerClientProvider)
{
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_explorerClientProvider = explorerClientProvider;
}
[HttpGet("stores/{storeId}/liquid")]
public async Task<IActionResult> GenerateLiquidScript(string storeId,
Dictionary<string, BitcoinExtKey> bitcoinExtKeys = null)
{
Dictionary<string, string> generated = new Dictionary<string, string>();
var allNetworks = _btcPayNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
.GroupBy(network => network.NetworkCryptoCode);
var allNetworkCodes = allNetworks
.SelectMany(networks => networks.Select(network => network.CryptoCode.ToUpperInvariant()))
.ToArray()
.Distinct();
Dictionary<string, BitcoinExtKey> privKeys = bitcoinExtKeys ?? new Dictionary<string, BitcoinExtKey>();
var store = await _storeRepository.FindStore(storeId);
var pms = store
.GetPaymentMethodConfigs<DerivationSchemeSettings>(_paymentMethodHandlerDictionary)
.Select(pair => (PaymentMethodId: pair.Key, DerivationSchemeSettings: pair.Value,
CryptoCode: pair.Key.ToString().Split("-")[0])).ToArray();
var paymentMethodsGroupedByNetworkCode =
pms
.Where(settings => allNetworkCodes.Contains(settings.CryptoCode))
.GroupBy(data =>
_btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(data.CryptoCode).NetworkCryptoCode);
if (paymentMethodsGroupedByNetworkCode.Any() is false)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Message = "There are no wallets configured that use Liquid or an elements side-chain."
});
return View(new GenerateLiquidImportScripts());
}
foreach (var der in paymentMethodsGroupedByNetworkCode)
{
var network = _btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(der.Key);
var nbxnet = network.NBXplorerNetwork;
var sb = new StringBuilder();
var explorerClient = _explorerClientProvider.GetExplorerClient(der.Key);
var status = await explorerClient.GetStatusAsync();
if (status.BitcoinStatus is null)
{
sb.AppendLine($"{der.Key} node is not available. Try again later.");
generated.Add(der.Key, sb.ToString());
continue;
}
var derivationSchemesForNetwork = der.GroupBy(data => data.DerivationSchemeSettings);
foreach (var paymentMethodDerivationScheme in derivationSchemesForNetwork)
{
var derivatonScheme = paymentMethodDerivationScheme.Key.AccountDerivation;
var sameWalletCryptoCodes = paymentMethodDerivationScheme.Select(data => data.CryptoCode).ToArray();
var matchedExistingKey = privKeys.Where(pair => sameWalletCryptoCodes.Contains(pair.Key));
BitcoinExtKey key = null;
if (matchedExistingKey.Any())
{
key = matchedExistingKey.First().Value;
}
else
{
key = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivatonScheme,
WellknownMetadataKeys.AccountHDKey);
}
if (key != null)
{
foreach (var paymentMethodData in paymentMethodDerivationScheme)
{
privKeys.TryAdd(paymentMethodData.CryptoCode, key);
}
}
var utxos = await explorerClient.GetUTXOsAsync(derivatonScheme, CancellationToken.None);
foreach (var utxo in utxos.GetUnspentUTXOs())
{
var addr = nbxnet.CreateAddress(derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey);
if (key is null)
{
sb.AppendLine(
$"elements-cli importaddress \"{addr}\" \"{utxo.KeyPath} from {derivatonScheme}\" false");
}
else
{
sb.AppendLine(
$"elements-cli importprivkey \"{key.Derive(utxo.KeyPath).PrivateKey.GetWif(nbxnet.NBitcoinNetwork)}\" \"{utxo.KeyPath} from {derivatonScheme}\" false");
}
if (!derivatonScheme.Unblinded())
{
var blindingKey =
NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(
derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey, nbxnet.NBitcoinNetwork);
sb.AppendLine($"elements-cli importblindingkey {addr} {blindingKey.ToHex()}");
}
}
}
if (sb.Length > 0)
{
sb.AppendLine("elements-cli stop");
sb.AppendLine("elementsd -rescan");
}
generated.Add(der.Key, sb.ToString());
}
return View(new GenerateLiquidImportScripts()
{
Wallets = paymentMethodsGroupedByNetworkCode.SelectMany(settings =>
settings.Select(data =>
new GenerateLiquidImportScripts.GenerateLiquidImportScriptWalletKeyVm()
{
CryptoCode = data.CryptoCode,
KeyPresent = privKeys.ContainsKey(data.CryptoCode),
ManualKey = null
}).ToArray()).ToArray(),
Scripts = generated
});
}
[HttpPost("stores/{storeId}/liquid")]
public async Task<IActionResult> GenerateLiquidScript(string storeId, GenerateLiquidImportScripts vm)
{
Dictionary<string, BitcoinExtKey> privKeys = new Dictionary<string, BitcoinExtKey>();
for (var index = 0; index < vm.Wallets.Length; index++)
{
var wallet = vm.Wallets[index];
if (string.IsNullOrEmpty(wallet.ManualKey))
continue;
var n =
_btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(wallet.CryptoCode);
ExtKey extKey = null;
try
{
var mnemonic = new Mnemonic(wallet.ManualKey);
extKey = mnemonic.DeriveExtKey();
}
catch (Exception)
{
}
if (extKey == null)
{
try
{
extKey = ExtKey.Parse(wallet.ManualKey, n.NBitcoinNetwork);
}
catch (Exception)
{
}
}
if (extKey == null)
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey,
"Invalid key (must be seed or root xprv or account xprv)", this);
continue;
}
var der = HttpContext.GetStoreData()
.GetDerivationSchemeSettings(_paymentMethodHandlerDictionary, wallet.CryptoCode).AccountDerivation;
if (der.GetExtPubKeys().Count() > 1)
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle multsig", this);
continue;
}
var first = der
.GetExtPubKeys().First();
if (first != extKey.Neuter())
{
KeyPath kp = null;
switch (der.ScriptPubKeyType())
{
case ScriptPubKeyType.Legacy:
kp = new KeyPath($"m/44'/{n.CoinType}/0'");
break;
case ScriptPubKeyType.Segwit:
kp = new KeyPath($"m/84'/{n.CoinType}/0'");
break;
case ScriptPubKeyType.SegwitP2SH:
kp = new KeyPath($"m/49'/{n.CoinType}/0'");
break;
default:
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle wallet type",
this);
continue;
}
extKey = extKey.Derive(kp);
if (first != extKey.Neuter())
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "key did not match", this);
continue;
}
}
privKeys.TryAdd(wallet.CryptoCode, extKey.GetWif(n.NBitcoinNetwork));
}
if (!ModelState.IsValid)
{
return View(vm);
}
return await GenerateLiquidScript(storeId, privKeys);
}
public class GenerateLiquidImportScripts
{
public class GenerateLiquidImportScriptWalletKeyVm
{
public string CryptoCode { get; set; }
public bool KeyPresent { get; set; }
public string ManualKey { get; set; }
}
public GenerateLiquidImportScriptWalletKeyVm[] Wallets { get; set; } =
Array.Empty<GenerateLiquidImportScriptWalletKeyVm>();
public Dictionary<string, string> Scripts { get; set; } = new Dictionary<string, string>();
}
}
}
namespace XX
{
public static class ModelStateExtensions
{
public static void AddModelError<TModel, TProperty>(this TModel source,
Expression<Func<TModel, TProperty>> ex,
string message,
ControllerBase controller)
{
var provider =
(ModelExpressionProvider) controller.HttpContext.RequestServices.GetService(
typeof(ModelExpressionProvider));
var key = provider.GetExpressionText(ex);
controller.ModelState.AddModelError(key, message);
}
}
}