mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
298 lines
12 KiB
C#
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);
|
|
}
|
|
}
|
|
} |