Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build & Test

on:
pull_request:
branches: [main, develop]
branches: [main, develop, release/**]

jobs:
build-and-test:
Expand All @@ -19,11 +19,14 @@ jobs:
with:
dotnet-version: '8.0.x'

- name: Clean
run: dotnet clean BTCPayServer.Plugins.Branta

- name: Restore dependencies
run: dotnet restore BTCPayServer.Plugins.Branta --locked-mode

- name: Build plugin
run: dotnet build BTCPayServer.Plugins.Branta --no-restore
run: dotnet build BTCPayServer.Plugins.Branta --no-restore -m:1

- name: Run Tests
run: dotnet test BTCPayServer.Plugins.Branta.Tests
run: dotnet test BTCPayServer.Plugins.Branta.Tests -m:1
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.20" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="NBitcoin" Version="8.0.13" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
Expand Down
55 changes: 55 additions & 0 deletions BTCPayServer.Plugins.Branta.Tests/Classes/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace BTCPayServer.Plugins.Branta.Tests.Classes;

public class TestHelper
{

public static string Decrypt(string encryptedValue, string secret)
{
byte[] keyData;
using (var sha256 = SHA256.Create())
{
keyData = sha256.ComputeHash(Encoding.UTF8.GetBytes(secret));
}

byte[] fullData = Convert.FromBase64String(encryptedValue);

byte[] iv = new byte[12];
Buffer.BlockCopy(fullData, 0, iv, 0, 12);

byte[] tag = new byte[16];
Buffer.BlockCopy(fullData, fullData.Length - 16, tag, 0, 16);

byte[] ciphertext = new byte[fullData.Length - 12 - 16];
Buffer.BlockCopy(fullData, 12, ciphertext, 0, ciphertext.Length);

byte[] plaintext = new byte[ciphertext.Length];

using (AesGcm aesGcm = new(keyData, 16))
{
aesGcm.Decrypt(iv, ciphertext, tag, plaintext);
}

return Encoding.UTF8.GetString(plaintext);
}

public static string GetSecret(string url)
{
return new Uri(url).Fragment.TrimStart('#').Substring("secret=".Length);
}

public static string? GetValueFromZeroKnowledgeUrl(string url)
{
var match = Regex.Match(new Uri(url).AbsolutePath, @"/zk-verify/(.+)$");

if (!match.Success)
return null;

var encodedValue = match.Groups[1].Value;
return HttpUtility.UrlDecode(encodedValue);
}
}
83 changes: 72 additions & 11 deletions BTCPayServer.Plugins.Branta.Tests/Services/BrantaServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using BTCPayServer.Plugins.Branta.Interfaces;
using BTCPayServer.Plugins.Branta.Models;
using BTCPayServer.Plugins.Branta.Services;
using BTCPayServer.Plugins.Branta.Tests.Classes;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Logging;
using Moq;
Expand All @@ -26,6 +27,7 @@ public class BrantaServiceTests

private const string ValidApiKey = "valid-api-key-123";
private const string OnChainAddress = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
private const string LightningAddress = "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs";

public BrantaServiceTests()
{
Expand Down Expand Up @@ -82,7 +84,7 @@ public async Task CreateInvoiceIfNotExists_CreatesInvoiceWhenBrantaDisabled()
var invoice = CreateInvoice();
var checkoutModel = CreateCheckoutModel(invoice);

var brantaSettings = GetSettings(invoice.StoreId, enabled: false);
SetSettings(invoice.StoreId, enabled: false);

var result = await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

Expand All @@ -107,7 +109,7 @@ public async Task CreateInvoiceIfNotExists_CreatesInvoiceWhenBrantaAPIKeyInvalid
var invoice = CreateInvoice();
var checkoutModel = CreateCheckoutModel(invoice);

var brantaSettings = GetSettings(invoice.StoreId, productionApiKey: null);
SetSettings(invoice.StoreId, productionApiKey: null);

var result = await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

Expand All @@ -130,7 +132,7 @@ public async Task CreateInvoiceIfNotExists_CreatesInvoice()
var invoice = CreateInvoice();
var checkoutModel = CreateCheckoutModel(invoice);

var brantaSettings = GetSettings(invoice.StoreId);
SetSettings(invoice.StoreId);

var result = await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

Expand All @@ -147,6 +149,61 @@ public async Task CreateInvoiceIfNotExists_CreatesInvoice()
Assert.Equal(InvoiceDataStatus.Success, resultInvoiceData.Status);
}

[Fact]
public async Task CreateInvoiceIfNotExists_CreatesZeroKnowledgeInvoice()
{
var invoice = CreateInvoice();
var checkoutModel = CreateCheckoutModel(invoice);

SetSettings(invoice.StoreId, enableZeroKnowledge: true);

var result = await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

var secret = TestHelper.GetSecret(result);
var value = TestHelper.GetValueFromZeroKnowledgeUrl(result);
Assert.NotNull(value);
var decryptedValue = TestHelper.Decrypt(value, secret);
Assert.Equal(OnChainAddress, decryptedValue);

_invoiceServiceMock.Verify(
x => x.AddAsync(It.IsAny<InvoiceData>()),
Times.Once
);

var resultInvoiceData = GetSavedInvoiceData();

Assert.NotNull(resultInvoiceData);
Assert.Null(resultInvoiceData.FailureReason);
Assert.Equal(InvoiceDataStatus.Success, resultInvoiceData.Status);
}

[Fact]
public async Task CreateInvoiceIfNotExists_DoesNotAddZeroKnowledgeParamsBolt11()
{
var invoice = CreateInvoice("BTC-Lightning");
var checkoutModel = CreateCheckoutModel(invoice);

SetSettings(invoice.StoreId, enableZeroKnowledge: true);

await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

Assert.DoesNotContain("?branta_id", checkoutModel.InvoiceBitcoinUrlQR);
Assert.DoesNotContain("&branta_secret", checkoutModel.InvoiceBitcoinUrlQR);
}

[Fact]
public async Task CreateInvoiceIfNotExists_ShouldNotSetZeroKnowledgeIfRequestUnsuccessful()
{
var invoice = CreateInvoice();
var checkoutModel = CreateCheckoutModel(invoice);

SetSettings(invoice.StoreId, enableZeroKnowledge: true, productionApiKey: "invalid-api-key");

await _brantaService.CreateInvoiceIfNotExistsAsync(checkoutModel);

Assert.DoesNotContain("branta_id", checkoutModel.InvoiceBitcoinUrlQR);
Assert.DoesNotContain("&branta_secret", checkoutModel.InvoiceBitcoinUrlQR);
}

private InvoiceData GetSavedInvoiceData()
{
Expand All @@ -155,23 +212,26 @@ private InvoiceData GetSavedInvoiceData()
.Arguments[0] as InvoiceData ?? throw new NullReferenceException();
}

private BrantaSettings GetSettings(string storeId, bool enabled = true, string? productionApiKey = ValidApiKey)
private void SetSettings(
string storeId,
bool enabled = true,
string? productionApiKey = ValidApiKey,
bool enableZeroKnowledge = false)
{
var brantaSettings = new BrantaSettings()
{
BrantaEnabled = enabled,
ProductionApiKey = productionApiKey,
PostDescriptionEnabled = true
PostDescriptionEnabled = true,
EnableZeroKnowledge = enableZeroKnowledge
};

_brantaSettingsServiceMock
.Setup(x => x.GetAsync(storeId))
.ReturnsAsync(brantaSettings);

return brantaSettings;
}

private InvoiceEntity CreateInvoice()
private InvoiceEntity CreateInvoice(string paymentMethodId = "BTC")
{
var invoice = new InvoiceEntity()
{
Expand All @@ -184,10 +244,10 @@ private InvoiceEntity CreateInvoice()
}
};

var btcPaymentMethodId = PaymentMethodId.Parse("BTC");
var btcPaymentMethodId = PaymentMethodId.Parse(paymentMethodId);
invoice.SetPaymentPrompt(btcPaymentMethodId, new PaymentPrompt()
{
Destination = OnChainAddress,
Destination = paymentMethodId.Contains("Lightning") ? LightningAddress : OnChainAddress,
PaymentMethodId = btcPaymentMethodId,
Currency = "BTC"
});
Expand All @@ -205,6 +265,7 @@ private static CheckoutModel CreateCheckoutModel(InvoiceEntity invoice)
{
StoreId = invoice.StoreId,
InvoiceId = invoice.Id,
InvoiceBitcoinUrlQR = $"bitcoin:{OnChainAddress}"
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace BTCPayServer.Plugins.Branta.Tests.Services;

public class InvoiceServiceTests
public class InvoiceServiceTests : IDisposable
{
private readonly BrantaDbContext _context;
private readonly InvoiceService _invoiceService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PropertyGroup>
<Product>Branta</Product>
<Description>Easily verify payments, checkouts, and invoices.</Description>
<Version>0.3.0</Version>
<Version>0.4.0</Version>
</PropertyGroup>

<!-- Plugin development properties -->
Expand Down
5 changes: 4 additions & 1 deletion BTCPayServer.Plugins.Branta/BrantaPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Branta.Classes;
using BTCPayServer.Plugins.Branta.Interfaces;
using BTCPayServer.Plugins.Branta.Services;
Expand All @@ -12,7 +13,7 @@ public class BrantaPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new IBTCPayServerPlugin.PluginDependency { Identifier = nameof(BTCPayServer), Condition = ">=2.0.0" }
new IBTCPayServerPlugin.PluginDependency { Identifier = nameof(BTCPayServer), Condition = ">=2.0.4" }
};

public override void Execute(IServiceCollection services)
Expand All @@ -34,6 +35,8 @@ public override void Execute(IServiceCollection services)
services.AddScoped<BrantaClient>();
services.AddScheduledTask<CleanUpInvoiceService>(TimeSpan.FromHours(24));

services.AddScoped<IGlobalCheckoutModelExtension, BrantaCheckoutModelExtension>();

services.AddUIExtension("checkout-payment-method", "Branta/VerifyWithBranta");
}
}
30 changes: 21 additions & 9 deletions BTCPayServer.Plugins.Branta/Classes/BrantaClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using BTCPayServer.Plugins.Branta.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
Expand All @@ -10,11 +12,17 @@ namespace BTCPayServer.Plugins.Branta.Classes;

public class BrantaClient(IHttpClientFactory httpClientFactory)
{
public static readonly string PaymentVersion = "v1";
public static readonly string PaymentVersion = "v2";

private JsonSerializerSettings _jsonSettings = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

public async Task PostPaymentAsync(PaymentRequest paymentRequest, BrantaSettings brantaSettings)
{
var json = JsonConvert.SerializeObject(paymentRequest);

var json = JsonConvert.SerializeObject(paymentRequest, _jsonSettings);
var content = new StringContent(json, Encoding.UTF8, "application/json");

using var request = new HttpRequestMessage(HttpMethod.Post, $"{PaymentVersion}/payments")
Expand All @@ -38,14 +46,18 @@ public async Task PostPaymentAsync(PaymentRequest paymentRequest, BrantaSettings

public class PaymentRequest
{
public Payment payment { get; set; }
public List<Destination> Destinations { get; set; }

public string Description { get; set; }

public string Ttl { get; set; }

public string BtcPayServerPluginVersion { get; set; }
}

public class Payment
public class Destination
{
public string description { get; set; }
public string payment { get; set; }
public string[] alt_payments { get; set; }
public string ttl { get; set; }
public string btcPayServerPluginVersion { get; set; }
public string Value { get; set; }

public bool Zk { get; set; }
}
7 changes: 7 additions & 0 deletions BTCPayServer.Plugins.Branta/Classes/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BTCPayServer.Plugins.Branta.Classes;

public class Constants
{
public const string PaymentId = "branta_id";
public const string ZeroKnowledgeSecret = "branta_secret";
}
Loading