Compare commits

...

33 Commits

Author SHA1 Message Date
137c3ef2ce Add changelog 2020-05-05 19:19:27 +09:00
87352f0b62 Make sure wallet support decimal fee, and allow user to select different fee rate based on expected confirmation time 2020-05-05 19:10:53 +09:00
2226884946 add qrcode margins (#1547)
addresses #1537 by adding a margin to the qrcodes in ShowLightningNodeInfo and WalletReceive (use same settings as Checkout)
2020-05-05 10:26:57 +02:00
9d2cd46464 fix tests and bump 2020-05-05 07:23:00 +09:00
f3b2b350ce Fix build 2020-05-05 07:06:32 +09:00
96c04481da bump NBXplorer 2020-05-05 06:55:19 +09:00
dad2642fa7 Make sure we match the user's sequence 2020-05-05 04:45:10 +09:00
59bdb943dd Reverting invoice row display so AM/PM is not cut off 2020-05-04 01:42:02 -05:00
67da6ee379 Decimal precision and filter valid transaction (#1538)
The liquid transactions list was showing all transactions to the wallet, even when it had nothing to do with the specific crypto code (e.g sending LBTC txs in USDT, LCAD in USDT, etc). This PR fixes that.

It also uses the previously introduced checkout decimal precision fix to the Wallets screen, specifically the balance amount on wallet llist and balance change on transaction list.
2020-05-04 01:04:34 +09:00
9c9c102e74 fix PadRight formatter 2020-05-03 09:32:10 +02:00
26241be6fa Ensure dropdown option doesn't overflow container (#1533)
fix #1526
2020-05-03 01:39:39 +09:00
2bb4dd5d01 Fix decimal points shown in Checkout UI based on currency ( always showed btc decimal precision before) (#1529)
* Fix decimal points shown in Checkout UI based on currency ( always showed btc decimal precision before)

* cleanup ShowMoney
2020-05-03 01:28:35 +09:00
5312bb1dee Merge pull request #1535 from Kukks/elementsbump
Bump elements and fix test
2020-05-02 20:47:22 +09:00
bf6f5aa335 Bump c-lightning 2020-05-02 20:33:55 +09:00
4a58763f98 Bump elements and fix test 2020-05-02 13:26:55 +02:00
b8202da7aa Fix tests 2020-05-02 00:59:36 +09:00
edfc82ac75 Merge pull request #1522 from Kukks/view-seed
Add View seed to wallet settings
2020-05-01 21:36:27 +09:00
b28fc85974 Fix: Do not returns HTML content if authentication to API fails 2020-05-01 21:33:42 +09:00
ab1b36bcdc add test for view seed 2020-05-01 13:34:11 +02:00
5443ac4688 Merge pull request #1531 from Kukks/api/store/prep
GreenField: Prep store for more data
2020-05-01 18:49:29 +09:00
d92d8ba0e4 Merge pull request #1524 from Kukks/txlist-labelfix
Add top Label filter + fix label link inconsistency
2020-05-01 18:47:48 +09:00
0f19d303eb Merge pull request #1527 from Kukks/app-inv-clean
Make App Inventory Updater run updates in order
2020-05-01 18:46:08 +09:00
1332f597e5 Merge pull request #1528 from Kukks/payjoin/ui-linkfix
Payjoin: Fix payjoin detection in checkout UI
2020-05-01 18:44:50 +09:00
de004074b7 Fix typo 2020-05-01 18:43:40 +09:00
29741f39ac Merge pull request #1523 from Kukks/sender-payjoin-label
Tag payjoin for sender too
2020-05-01 05:27:33 +09:00
33ea8984fc format 2020-04-30 16:44:27 +02:00
85517b0344 GreenField: Prep store for more data
refactored things around a bit to make it cleaner for when we add more properties to the store model
2020-04-30 16:43:16 +02:00
74c574255e Payjoin: Fix payjoin detection in checkout UI
sometimes, it would think there is a payjoin enabled invoice when really, it was just the address or asset id that has `pj` in it
2020-04-30 09:05:17 +02:00
70d4e98dff Make App Inventory Updater run updates in order
followed the same logic we used in the new auto labelling
2020-04-29 14:51:37 +02:00
f1900d30f2 clean format 2020-04-29 11:21:47 +02:00
463567cb07 Add top Label filter + fix label link inconsistency 2020-04-29 10:11:23 +02:00
53b0e675c3 Tag payjoin for sender too 2020-04-29 09:09:16 +02:00
d323bb35cc Add View seed to wallet settings 2020-04-29 08:28:13 +02:00
47 changed files with 539 additions and 298 deletions

View File

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.33" />
<PackageReference Include="NBitcoin" Version="5.0.35" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

View File

@ -1,6 +1,3 @@
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class ApiHealthData

View File

@ -7,6 +7,7 @@ namespace BTCPayServer.Client.Models
{
public string ApiKey { get; set; }
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}

View File

@ -6,33 +6,20 @@ namespace BTCPayServer.Client.Models
/// the id of the user
/// </summary>
public string Id { get; set; }
/// <summary>
/// the email AND username of the user
/// </summary>
public string Email { get; set; }
/// <summary>
/// Whether the user has verified their email
/// </summary>
public bool EmailConfirmed { get; set; }
/// <summary>
/// whether the user needed to verify their email on account creation
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
}
public class CreateApplicationUserRequest
{
/// <summary>
/// the email AND username of the new user
/// </summary>
public string Email { get; set; }
/// <summary>
/// password of the new user
/// </summary>
public string Password { get; set; }
/// <summary>
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
/// </summary>
public bool? IsAdministrator { get; set; }
}
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
@ -9,6 +6,7 @@ namespace BTCPayServer.Client.Models
public class CreateApiKeyRequest
{
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}

View File

@ -0,0 +1,20 @@
namespace BTCPayServer.Client.Models
{
public class CreateApplicationUserRequest
{
/// <summary>
/// the email AND username of the new user
/// </summary>
public string Email { get; set; }
/// <summary>
/// password of the new user
/// </summary>
public string Password { get; set; }
/// <summary>
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
/// </summary>
public bool? IsAdministrator { get; set; }
}
}

View File

@ -1,7 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class CreateStoreRequest
public class CreateStoreRequest : StoreBaseData
{
public string Name { get; set; }
}
}

View File

@ -1,11 +1,10 @@
namespace BTCPayServer.Client.Models
{
public class StoreData: StoreBaseData
public class StoreData : StoreBaseData
{
/// <summary>
/// the id of the store
/// </summary>
public string Id { get; set; }
}
}

View File

@ -1,6 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class UpdateStoreRequest: StoreBaseData
public class UpdateStoreRequest : StoreBaseData
{
}
}

View File

@ -23,6 +23,31 @@ namespace BTCPayServer
});
}
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
{
return new TransactionInformationSet()
{
Transactions =
transactionInformationSet.Transactions.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
};
}
return new GetTransactionsResponse()
{
Height = response.Height,
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
ReplacedTransactions = Filter(response.ReplacedTransactions),
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
};
}
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
//precision 0: 10 = 0.00000010

View File

@ -120,6 +120,11 @@ namespace BTCPayServer
{
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
}
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
return response;
}
}
public abstract class BTCPayNetworkBase

View File

@ -4,6 +4,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.9" />
<PackageReference Include="NBXplorer.Client" Version="3.0.11" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using System.Security.Claims;
namespace BTCPayServer.Data
{
@ -15,76 +11,39 @@ namespace BTCPayServer.Data
LowSpeed = 2,
LowMediumSpeed = 3
}
public class StoreData
{
public string Id
{
get;
set;
}
public string Id { get; set; }
public List<UserStore> UserStores { get; set; }
public List<UserStore> UserStores
{
get; set;
}
public List<AppData> Apps
{
get; set;
}
public List<PaymentRequestData> PaymentRequests
{
get; set;
}
public List<AppData> Apps { get; set; }
public List<PaymentRequestData> PaymentRequests { get; set; }
public List<InvoiceData> Invoices { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get; set;
}
public string DerivationStrategy { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies
{
get;
set;
}
public string DerivationStrategies { get; set; }
public string StoreName
{
get; set;
}
public string StoreName { get; set; }
public SpeedPolicy SpeedPolicy
{
get; set;
}
public SpeedPolicy SpeedPolicy { get; set; } = SpeedPolicy.MediumSpeed;
public string StoreWebsite
{
get; set;
}
public string StoreWebsite { get; set; }
public byte[] StoreCertificate
{
get; set;
}
public byte[] StoreCertificate { get; set; }
[NotMapped]
public string Role
{
get; set;
}
[NotMapped] public string Role { get; set; }
public byte[] StoreBlob { get; set; }
public byte[] StoreBlob
{
get;
set;
}
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
}

View File

@ -37,14 +37,13 @@ namespace BTCPayServer.Tests
{
tester.ActivateLBTC();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("USDT");
Assert.Single(Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets);
Assert.Equal(3, Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets.Count);
}
}

View File

@ -392,6 +392,7 @@ namespace BTCPayServer.Tests
lastInvoiceId = invoice.Id;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.OptInRBF = true;
txBuilder.AddCoins(coin);
txBuilder.Send(invoiceAddress, vector.Paid);
txBuilder.SendFees(vector.Fee);
@ -402,6 +403,10 @@ namespace BTCPayServer.Tests
if (vector.ExpectedError is null)
{
Assert.Contains(pj.Inputs, o => o.PrevOut == receiverCoin.Outpoint);
foreach (var input in pj.GetGlobalTransaction().Inputs)
{
Assert.Equal(Sequence.OptInRBF, input.Sequence);
}
if (!skipLockedCheck)
Assert.True(await payjoinRepository.TryUnlock(receiverCoin.Outpoint));
}
@ -777,13 +782,14 @@ namespace BTCPayServer.Tests
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
var txBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.OptInRBF = true;
var invoice7Coin6TxBuilder = txBuilder
.SetChange(senderChange)
.Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount)
.AddCoins(coin6.Coin)
.AddKeys(extKey.Derive(coin6.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.SetLockTime(0);
.SendEstimatedFees(new FeeRate(100m));
var invoice7Coin6Tx = invoice7Coin6TxBuilder
.BuildTransaction(true);

View File

@ -639,6 +639,13 @@ namespace BTCPayServer.Tests
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
s.AssertHappyMessage();
Assert.Equal(mnemonic.ToString(), s.Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text);
}
}
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)

View File

@ -607,6 +607,12 @@ namespace BTCPayServer.Tests
{
Assert.Equal("Object not found", ex.Errors.First());
}
var req = new HttpRequestMessage(HttpMethod.Get, "/invoices/Cy9jfK82eeEED1T3qhwF3Y");
req.Headers.TryAddWithoutValidation("Authorization", "Basic dGVzdA==");
req.Content = new StringContent("{}", Encoding.UTF8, "application/json");
var result = await tester.PayTester.HttpClient.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
Assert.Equal(0, result.Content.Headers.ContentLength.Value);
}
}

View File

@ -78,7 +78,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.24
image: nicolasdorier/nbxplorer:2.1.26
restart: unless-stopped
ports:
- "32838:32838"
@ -136,7 +136,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.8.0-dev
image: btcpayserver/lightning:v0.8.2-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -183,7 +183,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.8.0-dev
image: btcpayserver/lightning:v0.8.2-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -228,7 +228,7 @@ services:
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.18.1.1-1
image: btcpayserver/elements:0.18.1.7
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |

View File

@ -0,0 +1 @@
docker exec -ti btcpayserver_elementsd_liquid elements-cli -datadir="/data" $args

View File

@ -1,5 +1,6 @@
{
"parallelizeTestCollections": false,
"longRunningTestSeconds": 60,
"diagnosticMessages": true
"diagnosticMessages": true,
"methodDisplay": "method"
}

View File

@ -12,16 +12,20 @@ namespace BTCPayServer.Controllers
{
public IActionResult Handle(int? statusCode = null)
{
if (statusCode.HasValue)
if (Request.Headers.TryGetValue("Accept", out var v) && v.Any(v => v.Contains("text/html", StringComparison.OrdinalIgnoreCase)))
{
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
if (statusCode.HasValue)
{
var viewName = statusCode.ToString();
return View(viewName);
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
{
var viewName = statusCode.ToString();
return View(viewName);
}
}
return View(statusCode);
}
return View(statusCode);
return this.StatusCode(statusCode.Value);
}
}
}

View File

@ -9,7 +9,6 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Controllers.GreenField
{
@ -66,27 +65,36 @@ namespace BTCPayServer.Controllers.GreenField
[HttpPost("~/api/v1/stores")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<ActionResult<Client.Models.StoreData>> CreateStore(CreateStoreRequest request)
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
{
if (request?.Name is null)
return BadRequest(CreateValidationProblem(nameof(request.Name), "Name is missing"));
var store = await _storeRepository.CreateStore(_userManager.GetUserId(User), request.Name);
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var store = new Data.StoreData();
ToModel(request, store);
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
return Ok(FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}")]
public async Task<ActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
if (request?.Name is null)
return BadRequest(CreateValidationProblem(nameof(request.Name), "Name is missing"));
store.StoreName = request.Name;
ToModel(request, store);
await _storeRepository.UpdateStore(store);
return Ok(FromModel(store));
}
@ -100,11 +108,16 @@ namespace BTCPayServer.Controllers.GreenField
};
}
private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage)
private static void ToModel(StoreBaseData restModel,Data.StoreData model)
{
var modelState = new ModelStateDictionary();
modelState.AddModelError(propertyName, errorMessage);
return new ValidationProblemDetails(modelState);
model.StoreName = restModel.Name;
}
private IActionResult Validate(StoreBaseData request)
{
if (request?.Name is null)
ModelState.AddModelError(nameof(request.Name), "Name is missing");
return !ModelState.IsValid ? BadRequest(new ValidationProblemDetails(ModelState)) : null;
}
}
}

View File

@ -224,6 +224,9 @@ namespace BTCPayServer.Controllers
: (decimal?)null;
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
@ -236,8 +239,8 @@ namespace BTCPayServer.Controllers
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ShowMoney(divisibility),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
@ -253,7 +256,7 @@ namespace BTCPayServer.Controllers
StoreName = store.StoreName,
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
BtcPaid = accounting.Paid.ShowMoney(divisibility),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete

View File

@ -5,6 +5,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
@ -43,7 +44,11 @@ namespace BTCPayServer.Controllers
}
psbtRequest.FeePreference = new FeePreference();
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
if (sendModel.FeeSatoshiPerByte is decimal v &&
v > decimal.Zero)
{
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(v), 1);
}
if (sendModel.NoChange)
{
psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().Destination;
@ -329,6 +334,21 @@ namespace BTCPayServer.Controllers
vm.PSBT = proposedPayjoin.ToBase64();
vm.OriginalPSBT = psbt.ToBase64();
proposedPayjoin.Finalize();
var hash = proposedPayjoin.ExtractTransaction().GetHash();
_EventAggregator.Publish(new UpdateTransactionLabel()
{
WalletId = walletId,
TransactionLabels = new Dictionary<uint256, List<(string color, string label)>>()
{
{
hash,
new List<(string color, string label)>
{
UpdateTransactionLabel.PayjoinLabelTemplate()
}
}
}
});
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,

View File

@ -285,7 +285,7 @@ namespace BTCPayServer.Controllers
vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
vm.Balance = tx.BalanceChange.ToString();
vm.Balance = tx.BalanceChange.ShowMoney(wallet.Network);
vm.IsConfirmed = tx.Confirmations != 0;
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
@ -420,14 +420,35 @@ namespace BTCPayServer.Controllers
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var recommendedFees = new[] {
TimeSpan.FromMinutes(10.0),
TimeSpan.FromMinutes(60.0),
TimeSpan.FromHours(6.0),
TimeSpan.FromHours(24.0),
}
.Select(time => network.NBitcoinNetwork.Consensus.GetExpectedBlocksFor(time))
.Select(blockCount => feeProvider.GetFeeRateAsync((int)blockCount))
.ToArray();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey));
model.CurrentBalance = await balance;
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
model.RecommendedSatoshiPerByte = new decimal?[recommendedFees.Length];
for (int i = 0; i < model.RecommendedSatoshiPerByte.Length; i++)
{
decimal? feeRate = null;
try
{
feeRate = (await recommendedFees[i]).SatoshiPerByte;
}
catch
{
}
model.RecommendedSatoshiPerByte[i] = feeRate;
}
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte.Reverse().Where(r => r is decimal).FirstOrDefault();
model.SupportRBF = network.SupportRBF;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
@ -589,6 +610,25 @@ namespace BTCPayServer.Controllers
"You are sending more than what you own", this);
}
}
if (vm.FeeSatoshiPerByte is decimal fee)
{
if (fee < 0)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate should be above 0", this);
}
if (fee > 5_000m)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate is absurdly high", this);
}
if (_dashboard.Get(network.CryptoCode).Status?.BitcoinStatus?.MinRelayTxFee?.SatoshiPerByte is decimal minFee)
{
if (vm.FeeSatoshiPerByte < minFee)
vm.AddModelError(model => model.FeeSatoshiPerByte,
$"The fee rate is lower than the minimum relay fee ({vm.FeeSatoshiPerByte} < {minFee})", this);
}
}
if (!ModelState.IsValid)
return View(vm);
@ -996,7 +1036,8 @@ namespace BTCPayServer.Controllers
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString(CultureInfo.InvariantCulture);
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ShowMoney(wallet.Network
.Divisibility);
}
catch
{
@ -1137,7 +1178,10 @@ namespace BTCPayServer.Controllers
Label = derivationSchemeSettings.Label,
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal,
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString()
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString(),
NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey))
};
vm.AccountKeys = derivationSchemeSettings.AccountKeySettings
.Select(e => new WalletSettingsAccountKeyViewModel()
@ -1152,8 +1196,9 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/settings")]
[HttpPost]
public async Task<IActionResult> WalletSettings(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSettingsViewModel vm, string command = "save", CancellationToken cancellationToken = default)
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSettingsViewModel vm, string command = "save",
CancellationToken cancellationToken = default)
{
if (!ModelState.IsValid)
return View(vm);
@ -1164,14 +1209,21 @@ namespace BTCPayServer.Controllers
if (command == "save")
{
derivationScheme.Label = vm.Label;
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey)
? null
: new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
{
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
derivationScheme.AccountKeySettings[i].AccountKeyPath =
string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath)
? null
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
derivationScheme.AccountKeySettings[i].RootFingerprint =
string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
}
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
store.SetSupportedPaymentMethod(derivationScheme);
await Repository.UpdateStore(store);
@ -1180,15 +1232,41 @@ namespace BTCPayServer.Controllers
}
else if (command == "prune")
{
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet is already pruned";
}
else
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
TempData[WellKnownTempData.SuccessMessage] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
}
return RedirectToAction(nameof(WalletSettings));
}
else if (command == "view-seed" && await CanUseHotWallet())
{
var seed = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.Mnemonic, cancellationToken);
if (string.IsNullOrEmpty(seed))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error, Message = "The seed was not found"
});
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"Please store your seed securely! <br/><code class=\"alert-link\">{seed}</code>"
});
}
return RedirectToAction(nameof(WalletSettings));
}
else
@ -1196,8 +1274,6 @@ namespace BTCPayServer.Controllers
return NotFound();
}
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{

View File

@ -36,5 +36,27 @@ namespace BTCPayServer
return decimal.Parse(amt, CultureInfo.InvariantCulture);
}
public static string ShowMoney(this IMoney money, BTCPayNetwork network)
{
return money.GetValue(network).ShowMoney(network.Divisibility);
}
public static string ShowMoney(this Money money, int? divisibility)
{
return !divisibility.HasValue
? money.ToString()
: money.ToDecimal(MoneyUnit.BTC).ShowMoney(divisibility.Value);
}
public static string ShowMoney(this decimal d, int divisibility)
{
return d.ToString(GetDecimalFormat(divisibility), CultureInfo.InvariantCulture);
}
private static string GetDecimalFormat(int divisibility)
{
var res = $"0{(divisibility > 0 ? "." : string.Empty)}";
return res.PadRight(divisibility + res.Length, '0');
}
}
}

View File

@ -11,22 +11,81 @@ namespace BTCPayServer.HostedServices
{
public class AppInventoryUpdaterHostedService : EventHostedServiceBase
{
private readonly AppService _AppService;
private readonly EventAggregator _eventAggregator;
private readonly AppService _appService;
protected override void SubscribeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<UpdateAppInventory>();
}
public AppInventoryUpdaterHostedService(EventAggregator eventAggregator, AppService appService) : base(
eventAggregator)
{
_AppService = appService;
_eventAggregator = eventAggregator;
_appService = appService;
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent)
if (evt is UpdateAppInventory updateAppInventory)
{
//get all apps that were tagged that have manageable inventory that has an item that matches the item code in the invoice
var apps = (await _appService.GetApps(updateAppInventory.AppId)).Select(data =>
{
switch (Enum.Parse<AppType>(data.AppType))
{
case AppType.PointOfSale:
var possettings = data.GetSettings<AppsController.PointOfSaleSettings>();
return (Data: data, Settings: (object)possettings,
Items: _appService.Parse(possettings.Template, possettings.Currency));
case AppType.Crowdfund:
var cfsettings = data.GetSettings<CrowdfundSettings>();
return (Data: data, Settings: (object)cfsettings,
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
default:
return (null, null, null);
}
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
item.Inventory.HasValue &&
updateAppInventory.Items.ContainsKey(item.Id)));
foreach (var valueTuple in apps)
{
foreach (var item1 in valueTuple.Items.Where(item =>
updateAppInventory.Items.ContainsKey(item.Id)))
{
if (updateAppInventory.Deduct)
{
item1.Inventory -= updateAppInventory.Items[item1.Id];
}
else
{
item1.Inventory += updateAppInventory.Items[item1.Id];
}
}
switch (Enum.Parse<AppType>(valueTuple.Data.AppType))
{
case AppType.PointOfSale:
((AppsController.PointOfSaleSettings)valueTuple.Settings).Template =
_appService.SerializeTemplate(valueTuple.Items);
break;
case AppType.Crowdfund:
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
_appService.SerializeTemplate(valueTuple.Items);
break;
default:
throw new InvalidOperationException();
}
valueTuple.Data.SetSettings(valueTuple.Settings);
await _appService.UpdateOrCreateApp(valueTuple.Data);
}
}else if (evt is InvoiceEvent invoiceEvent)
{
Dictionary<string, int> cartItems = null;
bool deduct;
@ -44,7 +103,7 @@ namespace BTCPayServer.HostedServices
return;
}
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) ||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.PosData, out cartItems)))
{
@ -55,80 +114,28 @@ namespace BTCPayServer.HostedServices
return;
}
//get all apps that were tagged that have manageable inventory that has an item that matches the item code in the invoice
var apps = (await _AppService.GetApps(appIds)).Select(data =>
var items = cartItems ?? new Dictionary<string, int>();
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode))
{
switch (Enum.Parse<AppType>(data.AppType))
{
case AppType.PointOfSale:
var possettings = data.GetSettings<AppsController.PointOfSaleSettings>();
return (Data: data, Settings: (object)possettings,
Items: _AppService.Parse(possettings.Template, possettings.Currency));
case AppType.Crowdfund:
var cfsettings = data.GetSettings<CrowdfundSettings>();
return (Data: data, Settings: (object)cfsettings,
Items: _AppService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
default:
return (null, null, null);
}
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
item.Inventory.HasValue &&
((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) &&
item.Id == invoiceEvent.Invoice.ProductInformation.ItemCode) ||
(cartItems != null && cartItems.ContainsKey(item.Id)))));
foreach (var valueTuple in apps)
{
foreach (var item1 in valueTuple.Items.Where(item =>
((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) &&
item.Id == invoiceEvent.Invoice.ProductInformation.ItemCode) ||
(cartItems != null && cartItems.ContainsKey(item.Id)))))
{
if (cartItems != null && cartItems.ContainsKey(item1.Id))
{
if (deduct)
{
item1.Inventory -= cartItems[item1.Id];
}
else
{
item1.Inventory += cartItems[item1.Id];
}
}
else if (!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) &&
item1.Id == invoiceEvent.Invoice.ProductInformation.ItemCode)
{
if (deduct)
{
item1.Inventory--;
}
else
{
item1.Inventory++;
}
}
}
switch (Enum.Parse<AppType>(valueTuple.Data.AppType))
{
case AppType.PointOfSale:
((AppsController.PointOfSaleSettings)valueTuple.Settings).Template =
_AppService.SerializeTemplate(valueTuple.Items);
break;
case AppType.Crowdfund:
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
_AppService.SerializeTemplate(valueTuple.Items);
break;
default:
throw new InvalidOperationException();
}
valueTuple.Data.SetSettings(valueTuple.Settings);
await _AppService.UpdateOrCreateApp(valueTuple.Data);
items.TryAdd(invoiceEvent.Invoice.ProductInformation.ItemCode, 1);
}
_eventAggregator.Publish(new UpdateAppInventory()
{
Deduct = deduct,
Items = items,
AppId = appIds
});
}
}
}
public class UpdateAppInventory
{
public string[] AppId { get; set; }
public Dictionary<string, int> Items { get; set; }
public bool Deduct { get; set; }
}
}
}

View File

@ -26,12 +26,18 @@ namespace BTCPayServer.Models.WalletViewModels
public string CryptoCode { get; set; }
public int RecommendedSatoshiPerByte { get; set; }
public string[] RecommendedSatoshiLabels = new string[]
{
"10 minutes",
"1 hour",
"6 hours",
"1 day"
};
public decimal?[] RecommendedSatoshiPerByte { get; set; }
[Range(1, int.MaxValue)]
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
public decimal? FeeSatoshiPerByte { get; set; }
[Display(Name = "Make sure no change UTXO is created")]
public bool NoChange { get; set; }

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -16,6 +16,7 @@ namespace BTCPayServer.Models.WalletViewModels
public bool IsMultiSig => AccountKeys.Count > 1;
public List<WalletSettingsAccountKeyViewModel> AccountKeys { get; set; } = new List<WalletSettingsAccountKeyViewModel>();
public bool NBXSeedAvailable { get; set; }
}
public class WalletSettingsAccountKeyViewModel

View File

@ -36,7 +36,7 @@ namespace BTCPayServer.Payments.Bitcoin
public Data.NetworkFeeMode NetworkFeeMode { get; set; }
FeeRate _NetworkFeeRate;
[JsonConverter(typeof(FeeRateJsonConverter))]
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
public FeeRate NetworkFeeRate
{
get

View File

@ -331,10 +331,13 @@ namespace BTCPayServer.Payments.PayJoin
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
isOurOutput.Add(ourNewOutput);
var rand = new Random();
int senderInputCount = newTx.Inputs.Count;
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
{
contributedAmount += (Money)selectedUTXO.Value;
newTx.Inputs.Add(selectedUTXO.Outpoint);
var newInput = newTx.Inputs.Add(selectedUTXO.Outpoint);
newInput.Sequence = newTx.Inputs[rand.Next(0, senderInputCount)].Sequence;
}
ourNewOutput.Value += contributedAmount;
var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
@ -376,7 +379,6 @@ namespace BTCPayServer.Payments.PayJoin
}
}
var rand = new Random();
Utils.Shuffle(newTx.Inputs, rand);
Utils.Shuffle(newTx.Outputs, rand);

View File

@ -43,12 +43,13 @@ namespace BTCPayServer.Services.Labels
throw new ArgumentNullException(nameof(value));
if (color == null)
throw new ArgumentNullException(nameof(color));
if (value.StartsWith("{"))
if (value.StartsWith("{", StringComparison.InvariantCultureIgnoreCase))
{
var jObj = JObject.Parse(value);
if (jObj.ContainsKey("value"))
{
var id = jObj.ContainsKey("id") ? jObj["id"].Value<string>() : string.Empty;
var idInLabel = string.IsNullOrEmpty(id) ? string.Empty : $"({id})";
switch (jObj["value"].Value<string>())
{
case "invoice":
@ -57,7 +58,7 @@ namespace BTCPayServer.Services.Labels
RawValue = value,
Value = "invoice",
Color = color,
Tooltip = $"Received through an invoice ({id})",
Tooltip = $"Received through an invoice {idInLabel}",
Link = string.IsNullOrEmpty(id)
? null
: _linkGenerator.InvoiceLink(id, request.Scheme, request.Host, request.PathBase)
@ -68,7 +69,7 @@ namespace BTCPayServer.Services.Labels
RawValue = value,
Value = "payjoin-exposed",
Color = color,
Tooltip = $"This utxo was exposed through a payjoin proposal for an invoice ({id})",
Tooltip = $"This utxo was exposed through a payjoin proposal for an invoice {idInLabel}",
Link = string.IsNullOrEmpty(id)
? null
: _linkGenerator.InvoiceLink(id, request.Scheme, request.Host, request.PathBase)

View File

@ -158,33 +158,36 @@ namespace BTCPayServer.Services.Stores
}
}
public async Task<StoreData> CreateStore(string ownerId, string name)
public async Task CreateStore(string ownerId, StoreData storeData)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("name should not be empty", nameof(name));
if (!string.IsNullOrEmpty(storeData.Id))
throw new ArgumentException("id should be empty", nameof(storeData.StoreName));
if (string.IsNullOrEmpty(storeData.StoreName))
throw new ArgumentException("name should not be empty", nameof(storeData.StoreName));
if (ownerId == null)
throw new ArgumentNullException(nameof(ownerId));
using (var ctx = _ContextFactory.CreateContext())
{
StoreData store = new StoreData
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
StoreName = name,
SpeedPolicy = SpeedPolicy.MediumSpeed
};
storeData.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var userStore = new UserStore
{
StoreDataId = store.Id,
StoreDataId = storeData.Id,
ApplicationUserId = ownerId,
Role = "Owner"
Role = StoreRoles.Owner
};
ctx.Add(store);
ctx.Add(storeData);
ctx.Add(userStore);
await ctx.SaveChangesAsync().ConfigureAwait(false);
return store;
await ctx.SaveChangesAsync();
}
}
public async Task<StoreData> CreateStore(string ownerId, string name)
{
var store = new StoreData() {StoreName = name};
await CreateStore(ownerId,store);
return store;
}
public async Task RemoveStore(string storeId, string userId)
{
using (var ctx = _ContextFactory.CreateContext())

View File

@ -200,9 +200,9 @@ namespace BTCPayServer.Services.Wallets
return await completionSource.Task;
}
public Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
public async Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Client.GetTransactionsAsync(derivationStrategyBase);
return _Network.FilterValidTransactions(await _Client.GetTransactionsAsync(derivationStrategyBase));
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)

View File

@ -204,7 +204,7 @@
@foreach (var invoice in Model.Invoices)
{
<tr>
<td style="width:10em;">
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
@ -225,7 +225,7 @@
{
<div id="pavpill_@invoice.InvoiceId">
<span class="dropdown-toggle dropdown-toggle-split pavpill pavpil-@invoice.Status.ToString().ToLower()"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.StatusString
</span>
<div class="dropdown-menu pull-right">
@ -272,27 +272,32 @@
</td>
</tr>
<tr id="invoice_@invoice.InvoiceId" style="display:none;">
<td style="border-top:0"></td>
<td style="border-top:0" colspan="6" class="pt-3 pb-5">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
</div>
</td>
</tr>
}
</tbody>
</table>
<nav aria-label="..." class="w-100">
@if (Model.Total != 0) {
@if (Model.Total != 0)
{
<ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@ListInvoicesPage(-1, Model.Count)">&laquo;</a>
</li>
<li class="page-item disabled">
@if (Model.Total <= Model.Count) {
@if (Model.Total <= Model.Count)
{
<span class="page-link">
1@Model.Invoices.Count
</span>
} else {
}
else
{
<span class="page-link">
@(Model.Skip + 1)@(Model.Skip + Model.Invoices.Count), Total: @Model.Total
</span>

View File

@ -132,7 +132,7 @@
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">

View File

@ -228,7 +228,7 @@
},
computed: {
hasPayjoin: function(){
return this.srvModel.invoiceBitcoinUrl.indexOf('@PayjoinClient.BIP21EndpointKey') === -1;
return this.srvModel.invoiceBitcoinUrl.indexOf('@PayjoinClient.BIP21EndpointKey=') === -1;
},
coinswitchAmountDue: function() {
return this.srvModel.coinSwitchAmountMarkupPercentage

View File

@ -19,7 +19,7 @@
<div class="form-group">
<h5>Scripting</h5>
<p>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</p>
<p>We are retrieving the rate of each exchange either directly, via <a href="https://www.coingecko.com/" target="_blank">CoinGecko (free)</a>.</p>
<p>We are retrieving the rate of each exchange either directly, or via <a href="https://www.coingecko.com/" target="_blank">CoinGecko (free)</a>.</p>
<div class="accordion" id="accordion-info">
<div class="card">
<div class="card-header" id="direct-header">

View File

@ -43,7 +43,7 @@
<div class="only-for-js card-body m-sm-0 p-sm-0" id="app">
<div class="qr-container mb-4">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-address">

View File

@ -25,7 +25,10 @@
<input type="hidden" asp-for="Fiat" />
<input type="hidden" asp-for="Rate" />
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
@for (int i = 0; i < Model.RecommendedSatoshiPerByte.Length; i++)
{
<input type="hidden" asp-for="RecommendedSatoshiPerByte[i]" />
}
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" name="BIP21" id="BIP21" />
<ul class="text-danger">
@ -38,7 +41,7 @@
}
}
</ul>
@if (Model.InputSelection)
{
<div class="form-group hide-when-js">
@ -50,10 +53,10 @@
}
</select>
</div>
<partial name="CoinSelection"/>
<partial name="CoinSelection" />
<br>
}
@if (Model.Outputs.Count == 1)
{
<div class="form-group">
@ -130,10 +133,16 @@
<div class="form-group">
<label asp-for="FeeSatoshiPerByte"></label>
<input asp-for="FeeSatoshiPerByte" type="number" step="any" class="form-control" />
<span asp-validation-for="FeeSatoshiPerByte" class="text-danger"></span>
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info">
The recommended value is
<button type="button" id="crypto-fee-link" class="btn btn-link p-0 align-baseline">@Model.RecommendedSatoshiPerByte</button> satoshi per byte.
Target confirmation in:
@for (int i = 0; i < Model.RecommendedSatoshiPerByte.Length; i++)
{
if (Model.RecommendedSatoshiPerByte[i] is null)
continue;
<span><button type="button" class="btn btn-link p-0 align-baseline crypto-fee-link" data-feerate="@Model.RecommendedSatoshiPerByte[i]">@Model.RecommendedSatoshiLabels[i]</button>.</span>
}
</p>
</div>
@if (Model.Outputs.Count == 1)
@ -146,7 +155,7 @@
</div>
</div>
}
<div class="card">
<button id="advancedSettings" class="btn btn-light collapsed" type="button" data-toggle="collapse" data-target="#accordian-advanced" aria-expanded="false" aria-controls="accordian-advanced">
Advanced settings
@ -174,7 +183,7 @@
{
<div class="form-group">
<label asp-for="PayJoinEndpointUrl" class="control-label"></label>
<input asp-for="PayJoinEndpointUrl" class="form-control"/>
<input asp-for="PayJoinEndpointUrl" class="form-control" />
<span asp-validation-for="PayJoinEndpointUrl" class="text-danger"></span>
</div>
}
@ -193,7 +202,8 @@
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
@if (Model.CryptoCode == "BTC") {
@if (Model.CryptoCode == "BTC")
{
<button name="command" type="submit" class="dropdown-item" value="vault">... the vault (preview)</button>
}
@if (Model.NBXSeedAvailable)
@ -202,7 +212,7 @@
}
</div>
</div>
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary" >Add another destination</button>
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination</button>
<button type="button" id="bip21parse" class="ml-1 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="ml-1 btn btn-secondary only-for-js" data-toggle="modal" data-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button>
</div>

View File

@ -71,11 +71,15 @@
<div class="form-group d-flex mt-2">
<button name="command" type="submit" class="btn btn-primary" value="save">Save</button>
<div class="dropdown">
<button class="ml-1 btn btn-secondary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="ml-1 btn btn-secondary dropdown-toggle" type="button" id="SettingsMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Other actions...
</button>
<div class="dropdown-menu" aria-labelledby="SendMenu">
<button name="command" type="submit" class="dropdown-item" value="prune">Prune old transactions from history</button>
@if (Model.NBXSeedAvailable)
{
<button name="command" type="submit" class="dropdown-item" value="view-seed">View seed</button>
}
</div>
</div>
</div>

View File

@ -61,6 +61,22 @@
If some transactions appear in BTCPay Server, but are missing on Electrum or another wallet, <a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-wallet#missing-payments-in-my-software-or-hardware-wallet">follow those instructions</a>.
</div>
</div>
@if (Model.Labels.Any())
{
<div class="row mt-4">
<div class="col-md-12">
<div class="d-flex flex-row card card-body p-2">
<span class="mr-2">Filter by label:</span>
@foreach (var label in Model.Labels)
{
<a asp-route-labelFilter="@label.Value" class="badge mr-2 my-1 position-relative text-white d-block"
style="background-color: @label.Color;"><span class="text-white">@label.Value</span></a>
}
</div>
</div>
</div>
}
<div class="row">
<div class="col-md-12">
<table class="table table-sm table-responsive-lg">
@ -72,10 +88,10 @@
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th style="text-align:left">Label</th>
<th class="text-left">Label</th>
<th>Transaction Id</th>
<th style="text-align:right">Balance</th>
<th style="text-align:right"></th>
<th class="text-right">Balance</th>
<th class="text-right"></th>
</tr>
</thead>
<tbody>
@ -87,27 +103,19 @@
@transaction.Timestamp.ToBrowserDate()
</span>
</td>
<td style="text-align:left">
<td class="text-left">
@foreach (var label in transaction.Labels)
{
<div
class="badge transactionLabel"
style="
background-color: @label.Color;
color: white;
display: block;
padding-right: 16px;
position: relative;
"
class="badge transactionLabel position-relative text-white d-block"
style="background-color: @label.Color; padding-right: 16px;"
data-toggle="tooltip"
title="@label.Tooltip">
<a asp-route-labelFilter="@label.Value" class="text-white">@label.Value</a>
@if (!string.IsNullOrEmpty(label.Link))
{
<a href="@label.Link" target="_blank"><span class="text-white">@label.Value</span> <span class="fa fa-info-circle"></span></a>
}
else
{
<a asp-route-labelFilter="@label.Value" class="text-white">@label.Value</a>
<a href="@label.Link" target="_blank"> <span class="fa fa-info-circle"></span></a>
}
<form
asp-route-walletId="@this.Context.GetRouteValue("walletId")"
@ -132,14 +140,14 @@
</td>
@if (transaction.Positive)
{
<td style="text-align:right; color:green;">@transaction.Balance</td>
<td style="text-align:right; color:green;" class="text-right">@transaction.Balance</td>
}
else
{
<td style="text-align:right; color:red;">@transaction.Balance</td>
<td style="text-align:right; color:red;" class="text-right">@transaction.Balance</td>
}
<td style="text-align:right;">
<div class="dropdown" style="display:inline-block;">
<td class="text-right">
<div class="dropdown d-inline-block" >
<span class="fa fa-tags" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu">
<form asp-action="ModifyTransaction" method="post"
@ -169,7 +177,7 @@
</form>
</div>
</div>
<div class="dropdown" style="display:inline-block;">
<div class="dropdown d-inline-block">
@if (string.IsNullOrEmpty(transaction.Comment))
{
<span class="fa fa-comment" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>

View File

@ -27,8 +27,8 @@ function updateFiatValueWithCurrentElement() {
$(function () {
$(".output-amount").on("input", updateFiatValueWithCurrentElement).each(updateFiatValueWithCurrentElement);
$("#crypto-fee-link").on("click", function (elem) {
var val = $(this).text();
$(".crypto-fee-link").on("click", function (elem) {
var val = $(this).attr("data-feerate").valueOf();
$("#FeeSatoshiPerByte").val(val);
return false;
});

View File

@ -4,19 +4,19 @@
"currentLanguage": "Português (Brasil)",
"lang": "Idioma",
"Awaiting Payment...": "Aguardando pagamento...",
"Pay with": "Pague com",
"Pay with": "Pagar com",
"Contact and Refund Email": "E-mail de contato para reembolso",
"Contact_Body": "Por favor, informe um e-mail abaixo. Nós entraremos em contato caso algum problema ocorra com o seu pagamento.",
"Contact_Body": "Preencha seu e-mail abaixo. Nós entraremos em contato caso ocorra algum problema com o pagamento.",
"Your email": "Seu e-mail",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor, informe um e-mail válido",
"Please enter a valid email address": "Insira um e-mail válido",
"Order Amount": "Valor do pedido",
"Network Cost": "Taxa da rede",
"Already Paid": "Já foi pago",
"Due": "Devido",
"Scan": "Escanear",
"Copy": "Copiar",
"Conversion": "Conversão",
"Conversion": "Converter",
"Open in wallet": "Abrir na carteira",
"CompletePay_Body": "Para completar seu pagamento, por favor envie {{btcDue}} {{cryptoCode}} para o endereço abaixo.",
"Amount": "Quantia",
@ -27,19 +27,19 @@
"ConversionTab_CalculateAmount_Error": "Tentar novamente",
"ConversionTab_LoadCurrencies_Error": "Tentar novamente",
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Por favor selecione a moeda da qual pretende converter",
"ConversionTab_CurrencyList_Select_Option": "Selecione a moeda para converter",
"Invoice expiring soon...": "A fatura está expirando...",
"Invoice expired": "Fatura expirada",
"What happened?": "O que aconteceu?",
"InvoiceExpired_Body_1": "Essa fatura expirou. Uma fatura é válida por apenas {{maxTimeMinutes}} minutos. \nVocê pode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.",
"InvoiceExpired_Body_1": "Esta fatura expirou. Uma fatura é válida por apenas {{maxTimeMinutes}} minutos. \nVocê pode voltar para {{storeName}} se quiser tentar enviar o pagamento novamente.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento e ele ainda não foi aceito pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
"InvoiceExpired_Body_3": "Se o recebermos mais tarde, vamos processar o pedido ou entrar em contato para combinarmos uma devolução...",
"InvoiceExpired_Body_3": "Se recebermos mais tarde, vamos processar o pedido ou entrar em contato para combinar o reembolso...",
"Invoice ID": "Nº da Fatura",
"Order ID": "Nº do Pedido",
"Return to StoreName": "Voltar para {{storeName}}",
"This invoice has been paid": "Essa fatura foi paga",
"This invoice has been archived": "Essa fatura foi arquivada",
"Archived_Body": "Por favor, entre em contato com o estabelecimento para informações e suporte",
"This invoice has been paid": "Esta fatura foi paga",
"This invoice has been archived": "Esta fatura foi arquivada",
"Archived_Body": "Entre em contato com o estabelecimento para informações e suporte",
"BOLT 11 Invoice": "Fatura BOLT 11",
"Node Info": "Informação do nó",
"txCount": "{{count}} transação",
@ -47,6 +47,6 @@
"Pay with CoinSwitch": "Pagar com CoinSwitch",
"Pay with Changelly": "Pagar com Changelly",
"Close": "Fechar",
"NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Por favor, envie outra transação para cobrir o valor devido.",
"NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Envie outra transação para cobrir o valor devido.",
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
}

View File

@ -1,5 +1,6 @@
.prettydropdown {
position: relative;
max-width: 100%; /* Make sure it's never larger than parent container */
min-width: 72px; /* 70px + borders */
display: inline-block;
}
@ -21,6 +22,7 @@
font: normal 18px Calibri, sans-serif;
list-style-type: none;
margin: 0;
max-width: 100%; /* Make sure it's never larger than parent container */
padding: 0;
text-align: left;
-webkit-user-select: none; /* Chrome all / Safari all */
@ -69,6 +71,13 @@
line-height: 46px; /* 48px - borders */
margin: 0;
padding-left: 0.8rem;
/* Make sure text doesn't overflow */
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 2rem;
}
.prettydropdown.loading > ul > li {

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.0.4.2</Version>
<Version>1.0.4.4</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,42 @@
# Changelog
## 1.0.4.4:
### New Feature
* Allow user to select different fee rate based on expected confirmation time (@NicolasDorier)
### Bug fixes
* Fix QR Code on dark theme by adding some white margin around it (@chewsta)
* Make sure wallet support decimal fee ... again. (@NicolasDorier)
## 1.0.4.3:
### New features
* If you use a hot wallet, you can retrieve the seed in wallet settings / Other actions / View seed (@MrKukks)
* Add top Label filter (@MrKukks)
* As a sender, payjoin transaction are tagged in the wallet (@MrKukks)
### Bug fixes
* The wallet now discourage fee sniping (increase privacy by mimicking wallets like bitcoin core) (@NicolasDorier)
* Payjoin receiver fix: The receiver's inputs sequence must be the same as the sender's inputs' sequence (@NicolasDorier, reported by @waxwing)
* The wallet do not round fee rate to the nearest integer. (@NicolasDorier)
* Invoice row should not cut off the "AM/PM" part of the date (@r0ckstardev)
* Ensure dropdown in checkout page does not overflow (@ubolator)
* Fix decimal points shown in Checkout UI based on currency ( always showed btc decimal precision before) (@MrKukks #1529)
* fix label link inconsistency (@MrKukks)
* Fix payjoin detection in checkout UI (@MrKukks)
### Altcoins
* For liquid, fix decimal precision issue in the wallet (@MrKukks)
* For liquid, the transactions in a wallet of a specific asset should only show transactions specific to this asset (@MrKukks)
### Language
* Update portuguese strings (@BitcoinHeiro)
## 1.0.4.2
### New feature and improvements