Compare commits

..

13 Commits

Author SHA1 Message Date
aa72b814da bump 2018-07-27 18:04:57 +09:00
0dcda0f289 Fix: Inverse rule was not found in BTCPay with X_X 2018-07-27 18:04:41 +09:00
a2b039f983 Report errors of LND 2018-07-27 15:49:57 +09:00
7bfdf2d11d Order transactions in transaction list view 2018-07-27 12:03:56 +09:00
d2808cf662 bump 2018-07-27 01:35:07 +09:00
b68fcec692 Fix rate in the WalletSend 2018-07-27 01:17:43 +09:00
86644d38d7 Show rate error to the model in WalletSend 2018-07-27 00:32:09 +09:00
52f60b0457 Can show the transaction list in wallet menu 2018-07-27 00:08:07 +09:00
638b58ab48 remove debug u2f 2018-07-26 23:26:06 +09:00
1606f43609 Show the fiat price when sending coins 2018-07-26 23:23:28 +09:00
ad1307746c Add a "Wallet" menu 2018-07-26 22:32:50 +09:00
22307b8a2b Merge pull request #233 from danielalexiuc/master
Fix small typo on server settings page
2018-07-25 10:59:43 +09:00
f1c244bf6a Fix small typo 2018-07-25 11:44:26 +10:00
36 changed files with 915 additions and 394 deletions

View File

@ -104,7 +104,7 @@ namespace BTCPayServer.Tests
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
////////
// Make sure parenthesis are correctly calculated
@ -120,7 +120,7 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.BidAsk.Bid);
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
@ -128,7 +128,7 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.BidAsk.Bid);
////////
// Make sure kraken is not converted to CurrencyPair
@ -147,12 +147,12 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.Value.Value);
Assert.Equal(6000m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.Value.Value);
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();

View File

@ -521,14 +521,14 @@ namespace BTCPayServer.Tests
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
Assert.Equal(conn2.ToString(), conn.ToString());
@ -574,7 +574,7 @@ namespace BTCPayServer.Tests
user.GrantAccess();
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
@ -1592,7 +1592,7 @@ namespace BTCPayServer.Tests
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
Assert.NotNull(rateResult.Value);
Assert.NotNull(rateResult.BidAsk);
}
}
@ -1629,7 +1629,7 @@ namespace BTCPayServer.Tests
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value);
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid);
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.72</Version>
<Version>1.0.2.80</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -125,6 +125,21 @@
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewImports.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewStart.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>

View File

@ -209,7 +209,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = store.GetStoreBlob();
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
if (rate.BidAsk == null)
{
return null;
}
@ -217,7 +217,7 @@ namespace BTCPayServer.Controllers
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.Value.Value;
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
@ -244,9 +244,9 @@ namespace BTCPayServer.Controllers
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
if (limitValueRate.BidAsk != null)
{
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");

View File

@ -81,7 +81,7 @@ namespace BTCPayServer.Controllers
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
.Where(r => r.Value.HasValue)
.Select(r =>
new Rate()

View File

@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = GetStoreUrl(storeId);
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
@ -161,279 +161,5 @@ namespace BTCPayServer.Controllers
vm.Confirmation = true;
return View(vm);
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// getxpub
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
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");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
}
}

View File

@ -38,11 +38,9 @@ namespace BTCPayServer.Controllers
BTCPayRateProviderFactory _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
@ -56,7 +54,6 @@ namespace BTCPayServer.Controllers
IHostingEnvironment env)
{
_RateFactory = rateFactory;
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
@ -66,19 +63,16 @@ namespace BTCPayServer.Controllers
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
}
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
@ -94,21 +88,6 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")]
public IActionResult Wallet(string cryptoCode)
{
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(StoreData.Id);
model.CryptoCurrency = cryptoCode;
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
[HttpGet]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers()
@ -283,7 +262,7 @@ namespace BTCPayServer.Controllers
{
CurrencyPair = fetch.Key.ToString(),
Error = testResult.Errors.Count != 0,
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
: testResult.EvaluatedRule
});
}
@ -447,7 +426,8 @@ namespace BTCPayServer.Controllers
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
WalletId = new WalletId(store.Id, network.CryptoCode),
});
}

View File

@ -23,18 +23,15 @@ namespace BTCPayServer.Controllers
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private UserManager<ApplicationUser> _UserManager;
private BTCPayWalletProvider _WalletProvider;
public UserStoresController(
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository)
{
_Repo = storeRepository;
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
@ -85,17 +82,6 @@ namespace BTCPayServer.Controllers
{
StoresViewModel result = new StoresViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
@ -104,8 +90,7 @@ namespace BTCPayServer.Controllers
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
});
}
return View(result);
@ -128,22 +113,6 @@ namespace BTCPayServer.Controllers
});
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -0,0 +1,487 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public class WalletsController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
BTCPayRateProviderFactory _RateProvider;
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
BTCPayRateProviderFactory rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
_Repo = repo;
_RateProvider = rateProvider;
_NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
_explorerProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
Network: d.Network)))
.Where(_ => _.Wallet != null)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
foreach (var wallet in onChainWallets)
{
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
wallets.Wallets.Add(walletVm);
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
if (!wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key))
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
walletVm.StoreName = wallet.Store.StoreName;
walletVm.IsOwner = wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key);
}
return View(wallets);
}
[HttpGet]
[Route("{walletId}")]
public async Task<IActionResult> WalletTransactions(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateRules.GlobalMultiplier = 1.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
model.CryptoCurrency = walletId.CryptoCode;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
model.Fiat = currencyPair.Right;
}
else
{
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch(Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
return null;
try
{
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch(ArgumentException) { }
return null;
}
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
{
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _userManager.GetUserId(User);
}
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
{
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
}
[HttpGet]
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
public async Task<IActionResult> LedgerConnection(
string command,
// getinfo
string cryptoCode = null,
// getxpub
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
DerivationStrategyBase derivationScheme = null,
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
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");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _explorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(derivationScheme);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
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;
}
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
}

View File

@ -39,6 +39,8 @@ using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Hosting
{
@ -105,7 +107,11 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.Configure<MvcOptions>((o) => {
o.Filters.Add(new ContentSecurityPolicyCssThemeManager());
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
});
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using System.Reflection;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.ModelBinders
{
public class DerivationSchemeModelBinder : IModelBinder
{
public DerivationSchemeModelBinder()
{
}
#region IModelBinder Members
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(DerivationStrategyBase).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string key = val.FirstValue as string;
if (key == null)
{
return Task.CompletedTask;
}
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
var network = networkProvider.GetNetwork(cryptoCode ?? "BTC");
try
{
var data = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(key);
if (!bindingContext.ModelType.IsInstanceOfType(data))
{
throw new FormatException("Invalid destination type");
}
bindingContext.Result = ModelBindingResult.Success(data);
}
catch { throw new FormatException("Invalid derivation scheme"); }
return Task.CompletedTask;
}
#endregion
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.ModelBinders
{
public class WalletIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(WalletId).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string key = val.FirstValue as string;
if (key == null)
{
return Task.CompletedTask;
}
if(WalletId.TryParse(key, out var walletId))
{
bindingContext.Result = ModelBindingResult.Success(walletId);
}
return Task.CompletedTask;
}
}
}

View File

@ -18,6 +18,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
}
public StoreViewModel()

View File

@ -34,10 +34,6 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
public string[] Balances
{
get; set;
}
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class ListTransactionsViewModel
{
public class TransactionViewModel
{
public DateTimeOffset Timestamp { get; set; }
public string Id { get; set; }
public string Link { get; set; }
public bool Positive { get; set; }
public string Balance { get; set; }
}
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class ListWalletsViewModel
{
public class WalletViewModel
{
public string StoreName { get; set; }
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public string Balance { get; set; }
public bool IsOwner { get; set; }
public WalletId Id { get; set; }
}
public List<WalletViewModel> Wallets { get; set; } = new List<WalletViewModel>();
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletModel
{
@ -15,5 +15,9 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }
public string RateError { get; set; }
}
}

View File

@ -183,18 +183,24 @@ namespace BTCPayServer.Payments.Lightning.Lnd
NodeId = resp.Identity_pubkey
};
try
{
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
return nodeInfo;
return nodeInfo;
}
catch (SwaggerException ex) when (!string.IsNullOrEmpty(ex.Response))
{
throw new Exception("LND threw an error: " + ex.Response);
}
}
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))

View File

@ -138,6 +138,9 @@ namespace BTCPayServer.Rating
return _Ask;
}
}
public decimal Center => (Ask + Bid) / 2.0m;
public BidAsk Inverse()
{
return new BidAsk(1.0m / Ask, 1.0m / Bid);

View File

@ -504,7 +504,7 @@ namespace BTCPayServer.Rating
public bool Reevaluate()
{
_Value = null;
_BidAsk = null;
_EvaluatedNode = null;
_Evaluated = null;
Errors.Clear();
@ -524,7 +524,7 @@ namespace BTCPayServer.Rating
Errors.AddRange(calculate.Errors);
return false;
}
_Value = calculate.Values.Pop().Bid;
_BidAsk = calculate.Values.Pop();
_EvaluatedNode = result;
return true;
}
@ -563,12 +563,12 @@ namespace BTCPayServer.Rating
return expression.NormalizeWhitespace("", "\n").ToString();
}
decimal? _Value;
public decimal? Value
BidAsk _BidAsk;
public BidAsk BidAsk
{
get
{
return _Value;
return _BidAsk;
}
}
}

View File

@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Rates
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public decimal? Value { get; set; }
public BidAsk BidAsk { get; set; }
public bool Cached { get; internal set; }
}
@ -172,11 +172,11 @@ namespace BTCPayServer.Services.Rates
result.ExchangeExceptions.AddRange(query.Exceptions);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.Add(rule);
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.Value = rateRule.Value;
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);

View File

@ -151,6 +151,11 @@ namespace BTCPayServer.Services.Wallets
return await completionSource.Task;
}
public Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Client.GetTransactionsAsync(derivationStrategyBase, null, false);
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();

View File

@ -17,7 +17,7 @@
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.<br /></span>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection information here.<br /></span>
</p>
</div>

View File

@ -61,6 +61,7 @@
}
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>

View File

@ -82,7 +82,7 @@
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
<a asp-action="WalletTransactions" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>

View File

@ -27,7 +27,6 @@
<tr>
<th>Name</th>
<th>Website</th>
<th>On-Chain balances</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
@ -42,16 +41,6 @@
<a href="@store.WebSite">@store.WebSite</a>
}
</td>
<td>
@for (int i = 0; i < store.Balances.Length; i++)
{
<span>@store.Balances[i]</span>
if (i != store.Balances.Length - 1)
{
<br />
}
}
</td>
<td style="text-align:right">
@if (store.IsOwner)
{

View File

@ -0,0 +1,56 @@
@model BTCPayServer.Models.WalletViewModels.ListWalletsViewModel
@{
ViewData["Title"] = "Wallets";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create and manage wallets.</p>
</div>
</div>
<div class="row">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Store Name</th>
<th>Crypto Code</th>
<th>Balance</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var wallet in Model.Wallets)
{
<tr>
@if(wallet.IsOwner)
{
<td><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
}
else
{
<td>@wallet.StoreName</td>
}
<td>@wallet.CryptoCode</td>
<td>@wallet.Balance</td>
<td style="text-align:right">
<a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</section>

View File

@ -2,6 +2,7 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
<h4>@ViewData["Title"]</h4>
@ -40,7 +41,14 @@
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<div class="input-group">
<input id="amount-textbox" name="Amount" class="form-control" type="text" onkeyup='updateFiatValue();' />
<div class="input-group-prepend">
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
</div>
</div>
@*<span class="text-muted" id="fiatValue"></span>*@
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
@ -62,11 +70,11 @@
</form>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
@section Scripts
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

View File

@ -0,0 +1,54 @@
@model ListTransactionsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Transactions);
}
<style type="text/css">
.smMaxWidth {
max-width: 200px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 400px;
}
}
</style>
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Timestamp</th>
<th>Transaction Id</th>
<th style="text-align:right">Balance</th>
</tr>
</thead>
<tbody>
@foreach(var transaction in Model.Transactions)
{
<tr>
<td>@transaction.Timestamp.ToBrowserDate()</td>
<td class="smMaxWidth text-truncate">
<a href="@transaction.Link" target="_blank">
@transaction.Id
</a>
</td>
@if(transaction.Positive)
{
<td style="text-align:right; color:green;">@transaction.Balance</td>
}
else
{
<td style="text-align:right; color:red;">@transaction.Balance</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public enum WalletsNavPages
{
Send,
Transactions
}
}

View File

@ -0,0 +1,7 @@
@inject SignInManager<ApplicationUser> SignInManager
<div class="nav flex-column nav-pills">
<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>
</div>

View File

@ -0,0 +1,2 @@
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Models.WalletViewModels

View File

@ -0,0 +1,3 @@
@{
ViewBag.MainTitle = "Manage wallet";
}

40
BTCPayServer/WalletId.cs Normal file
View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class WalletId
{
static readonly Regex _WalletStoreRegex = new Regex("^S-([a-zA-Z0-9]{30,60})-([a-zA-Z]{2,5})$");
public static bool TryParse(string str, out WalletId walletId)
{
walletId = null;
WalletId w = new WalletId();
var match = _WalletStoreRegex.Match(str);
if (!match.Success)
return false;
w.StoreId = match.Groups[1].Value;
w.CryptoCode = match.Groups[2].Value.ToUpperInvariant();
walletId = w;
return true;
}
public WalletId()
{
}
public WalletId(string storeId, string cryptoCode)
{
StoreId = storeId;
CryptoCode = cryptoCode;
}
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public override string ToString()
{
return $"S-{StoreId}-{CryptoCode.ToUpperInvariant()}";
}
}
}

View File

@ -1,6 +1,6 @@
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel);
var cryptoSelector = $("#CryptoCurrency");
function GetSelectedCryptoCode() {

View File

@ -1,6 +1,17 @@
$(function () {

function updateFiatValue() {
if (srvModel.rate !== null) {
var fiatValue = $("#fiatValue");
fiatValue.css("display", "inline");
var amountValue = parseFloat($("#amount-textbox").val());
if (!isNaN(amountValue)) {
fiatValue.text("= " + (srvModel.rate * amountValue).toFixed(srvModel.divisibility) + " " + srvModel.fiat);
}
}
}
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl);
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();