Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
137c3ef2ce | |||
87352f0b62 | |||
2226884946 | |||
9d2cd46464 | |||
f3b2b350ce | |||
96c04481da | |||
dad2642fa7 | |||
59bdb943dd | |||
67da6ee379 | |||
9c9c102e74 | |||
26241be6fa | |||
2bb4dd5d01 | |||
5312bb1dee | |||
bf6f5aa335 | |||
4a58763f98 | |||
b8202da7aa | |||
edfc82ac75 | |||
b28fc85974 | |||
ab1b36bcdc | |||
5443ac4688 | |||
d92d8ba0e4 | |||
0f19d303eb | |||
1332f597e5 | |||
de004074b7 | |||
29741f39ac | |||
33ea8984fc | |||
85517b0344 | |||
74c574255e | |||
70d4e98dff | |||
f1900d30f2 | |||
463567cb07 | |||
53b0e675c3 | |||
d323bb35cc |
@ -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>
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ApiHealthData
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
20
BTCPayServer.Client/Models/CreateApplicationUserRequest.cs
Normal file
20
BTCPayServer.Client/Models/CreateApplicationUserRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateStoreRequest
|
||||
public class CreateStoreRequest : StoreBaseData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class UpdateStoreRequest: StoreBaseData
|
||||
public class UpdateStoreRequest : StoreBaseData
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: |
|
||||
|
1
BTCPayServer.Tests/docker-elements.ps1
Normal file
1
BTCPayServer.Tests/docker-elements.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_elementsd_liquid elements-cli -datadir="/data" $args
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"parallelizeTestCollections": false,
|
||||
"longRunningTestSeconds": 60,
|
||||
"diagnosticMessages": true
|
||||
"diagnosticMessages": true,
|
||||
"methodDisplay": "method"
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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)">«</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>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
@ -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 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.4.2</Version>
|
||||
<Version>1.0.4.4</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
37
Changelog.md
37
Changelog.md
@ -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
|
||||
|
Reference in New Issue
Block a user