Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private async Task<CreateSenderPsbtResult> CreateSenderPsbtAsync(SystemUri payme
paymentAmount = Money.Satoshis(checked((long)amountSats.Value)).ToDecimal(MoneyUnit.BTC);
using var _ = parsedUri.CheckPjSupported();
}
catch (PjParseException ex)
catch (UriParseException ex)
{
throw new InvalidOperationException($"Invalid BIP21 URI '{paymentUrl}': {ex.Message}", ex);
}
Expand Down Expand Up @@ -233,9 +233,9 @@ private async Task<PSBT> RequestProposalAsync(SystemUri paymentUrl, IReadOnlyLis
}
proposalPsbtBase64 = await PollForProposalAsync(current, senderPersister, ohttpRelayUrls, cancellationToken).ConfigureAwait(false);
}
catch (SenderPersistedException.ResponseException ex)
catch (SenderPersistedException ex) when (SenderResponseOf(ex) is { } senderResponse)
{
throw CreateSenderResponseFailure(ex);
throw CreateSenderResponseFailure(ex, senderResponse);
}

if (string.IsNullOrWhiteSpace(proposalPsbtBase64))
Expand Down Expand Up @@ -355,13 +355,23 @@ private async Task<PSBT> RequestProposalAsync(SystemUri paymentUrl, IReadOnlyLis
throw new InvalidOperationException($"All configured OHTTP relays failed for payer store '{_payer.StoreId}'. LastError='{lastError?.Message}'", lastError);
}

private InvalidOperationException CreateSenderResponseFailure(SenderPersistedException.ResponseException ex)
private InvalidOperationException CreateSenderResponseFailure(SenderPersistedException ex, global::Payjoin.ResponseException senderResponse)
{
return new InvalidOperationException(
$"Payjoin receiver rejected the proposal for payer store '{_payer.StoreId}': {FormatSenderResponseException(ex.v1)}",
$"Payjoin receiver rejected the proposal for payer store '{_payer.StoreId}': {FormatSenderResponseException(senderResponse)}",
ex);
}

private static global::Payjoin.ResponseException? SenderResponseOf(SenderPersistedException exception)
{
return exception switch
{
SenderPersistedException.Fatal { v1: SenderException.Response response } => response.v1,
SenderPersistedException.Transient { v1: SenderException.Response response } => response.v1,
_ => null
};
}

private static string FormatSenderResponseException(global::Payjoin.ResponseException responseException)
{
return responseException switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ private sealed class FixedPsbtProposal(string psbt) : IPayjoinProposal
public RequestResponse CreatePostRequest(string ohttpRelay) => throw new NotSupportedException();
public PayjoinProposalTransition ProcessResponse(byte[] body, ClientResponse ohttpContext) => throw new NotSupportedException();
public string Psbt() => psbt;
public PayjoinOutPoint[] UtxosToBeLocked() => throw new NotSupportedException();
public bool ProposalTxidIsStable() => throw new NotSupportedException();
}

private sealed class NoOpSessionProcessor : IPayjoinReceiverSessionProcessor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ private sealed class FixedPsbtProposal(string psbt) : IPayjoinProposal
public RequestResponse CreatePostRequest(string ohttpRelay) => throw new NotSupportedException();
public PayjoinProposalTransition ProcessResponse(byte[] body, ClientResponse ohttpContext) => throw new NotSupportedException();
public string Psbt() => psbt;
public PayjoinOutPoint[] UtxosToBeLocked() => throw new NotSupportedException();
public bool ProposalTxidIsStable() => throw new NotSupportedException();
}

private sealed class ThrowingProposal : IPayjoinProposal
Expand All @@ -179,7 +179,7 @@ private sealed class ThrowingProposal : IPayjoinProposal
public RequestResponse CreatePostRequest(string ohttpRelay) => throw new NotSupportedException();
public PayjoinProposalTransition ProcessResponse(byte[] body, ClientResponse ohttpContext) => throw new NotSupportedException();
public string Psbt() => throw new NotSupportedException();
public PayjoinOutPoint[] UtxosToBeLocked() => throw new NotSupportedException();
public bool ProposalTxidIsStable() => throw new NotSupportedException();
}

private sealed class UnusedRelayRequestSender : IPayjoinReceiverRelayRequestSender
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public async Task<ActionResult<RunTestPaymentResponse>> RunTestPayment([FromBody
paymentAmount = Money.Satoshis(checked((long)amountSats.Value)).ToDecimal(MoneyUnit.BTC);
using var _ = parsedUri.CheckPjSupported();
}
catch (PjParseException ex)
catch (UriParseException ex)
{
return RunTestPaymentFailure($"Invalid BIP21 URI: {ex.Message}");
}
Expand Down
1 change: 1 addition & 0 deletions BTCPayServer.Plugins.Payjoin/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public override void Execute(IServiceCollection applicationBuilder)
provider.GetRequiredService<BTCPayNetworkProvider>(),
provider.GetRequiredService<PayjoinReceiverSessionStore>(),
provider.GetRequiredService<PayjoinMailroomManager>(),
provider.GetRequiredService<PayjoinOhttpKeysProvider>(),
provider.GetRequiredService<PayjoinAvailabilityService>(),
provider.GetRequiredService<PayjoinSessionBuildLock>(),
provider.GetRequiredService<IPayjoinAccountingBridgeService>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private static bool IsPayjoinEnabled(string paymentUrl)
using var _ = parsedUri.CheckPjSupported();
return true;
}
catch (PjParseException)
catch (UriParseException)
{
return false;
}
Expand Down
13 changes: 11 additions & 2 deletions BTCPayServer.Plugins.Payjoin/Services/PayjoinOhttpKeysProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ namespace BTCPayServer.Plugins.Payjoin.Services;

public sealed class PayjoinOhttpKeysProvider
{
// TODO: Consider making this configurable if 12 hours is not a good duration for caching OHTTP keys.
private static readonly TimeSpan OhttpKeysCacheDuration = TimeSpan.FromHours(12);
// Directory OHTTP keys can rotate; a shorter lifetime bounds how long stale keys are served, and
// Invalidate lets a failed session build drop them immediately instead of waiting out the window.
// TODO: Consider making this configurable if one hour is not a good duration for caching OHTTP keys.
private static readonly TimeSpan OhttpKeysCacheDuration = TimeSpan.FromHours(1);

// TODO: Consider making this configurable if 10 seconds is not a good timeout for fetching OHTTP keys.
private static readonly TimeSpan OhttpKeysFetchTimeout = TimeSpan.FromSeconds(10);
Expand Down Expand Up @@ -121,6 +123,13 @@ internal async Task<PayjoinOhttpKeysFetchResult> FetchKeysAsync(
}
}

// Stale directory OHTTP keys are one way session construction fails; dropping the cached
// keys costs at most one refetch and lets the next attempt start from fresh material.
internal void Invalidate(string storeId, SystemUri ohttpRelayUrl, string directoryUrl)
{
_memoryCache.Remove(CreateCacheKey(storeId, ohttpRelayUrl, directoryUrl));
}

internal static string CreateCacheKey(string storeId, SystemUri ohttpRelayUrl, string directoryUrl)
{
return $"PayjoinOhttpKeys_{storeId}_{ohttpRelayUrl.AbsoluteUri}_{directoryUrl}";
Expand Down
21 changes: 18 additions & 3 deletions BTCPayServer.Plugins.Payjoin/Services/PayjoinUriSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class PayjoinUriSessionService
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PayjoinReceiverSessionStore _receiverSessionStore;
private readonly PayjoinMailroomManager _mailroomManager;
private readonly PayjoinOhttpKeysProvider _ohttpKeysProvider;
private readonly PayjoinAvailabilityService _availabilityService;
private readonly PayjoinSessionBuildLock _sessionBuildLock;
private readonly IPayjoinAccountingBridgeService _accountingBridgeService;
Expand All @@ -44,6 +45,7 @@ internal PayjoinUriSessionService(
BTCPayNetworkProvider networkProvider,
PayjoinReceiverSessionStore receiverSessionStore,
PayjoinMailroomManager mailroomManager,
PayjoinOhttpKeysProvider ohttpKeysProvider,
PayjoinAvailabilityService availabilityService,
PayjoinSessionBuildLock sessionBuildLock,
IPayjoinAccountingBridgeService accountingBridgeService,
Expand All @@ -52,6 +54,7 @@ internal PayjoinUriSessionService(
_networkProvider = networkProvider;
_receiverSessionStore = receiverSessionStore;
_mailroomManager = mailroomManager;
_ohttpKeysProvider = ohttpKeysProvider;
_availabilityService = availabilityService;
_sessionBuildLock = sessionBuildLock;
_accountingBridgeService = accountingBridgeService;
Expand Down Expand Up @@ -129,19 +132,31 @@ public async Task<string> BuildAsync(

if (session is null)
{
var selectedRelay = await _mailroomManager.SelectBootstrapRouteAsync(
var bootstrapRoute = await _mailroomManager.SelectBootstrapRouteAsync(
storeSettings,
storeId,
invoiceId,
cancellationToken).ConfigureAwait(false);

if (selectedRelay is null)
if (bootstrapRoute is null)
{
return LogUnexpectedFallbackAndReturnBip21(bip21, invoiceId, "OHTTP keys are unavailable from all configured relays");
}

var bootstrapPersister = new CapturingReceiverSessionPersister();
InitializeSession(destination, due, selectedRelay.DirectoryUrl.AbsoluteUri, selectedRelay.OhttpKeys, monitoringExpiresAt, bootstrapPersister);
try
{
InitializeSession(destination, due, bootstrapRoute.DirectoryUrl.AbsoluteUri, bootstrapRoute.OhttpKeys, monitoringExpiresAt, bootstrapPersister);
}
catch (UniffiException)
{
// Stale directory OHTTP keys are one way session construction fails; dropping the
// cached keys costs at most one refetch and lets the next attempt start from fresh
// material. Replayed sessions never reach here, so their failures keep the cache.
_ohttpKeysProvider.Invalidate(storeId, bootstrapRoute.RelayUrl, bootstrapRoute.DirectoryUrl.AbsoluteUri);
throw;
}

session = _receiverSessionStore.CreateSession(
invoiceId,
destination,
Expand Down
16 changes: 13 additions & 3 deletions BTCPayServer.Plugins.Payjoin/Services/RunTestPaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,13 @@ private async Task<PSBT> RequestProposalAsync(
current.Dispose();
}
}
catch (BuildSenderException ex)
catch (SenderInputException ex)
{
throw new RunTestPaymentExecutionException($"Sender build failed: {ex.Message}", ex);
}
catch (SenderPersistedException.ResponseException ex)
catch (SenderPersistedException ex) when (SenderResponseOf(ex) is { } senderResponse)
{
throw new RunTestPaymentExecutionException($"Sender rejected by receiver: {FormatSenderResponseException(ex.v1)}", ex);
throw new RunTestPaymentExecutionException($"Sender rejected by receiver: {FormatSenderResponseException(senderResponse)}", ex);
}
catch (HttpRequestException ex)
{
Expand Down Expand Up @@ -442,6 +442,16 @@ private static IReadOnlyList<SystemUri> GetConfiguredRelayUrls(RunTestPaymentCon
throw new RunTestPaymentExecutionException("No OHTTP relays configured for test payment.");
}

private static global::Payjoin.ResponseException? SenderResponseOf(SenderPersistedException exception)
{
return exception switch
{
SenderPersistedException.Fatal { v1: SenderException.Response response } => response.v1,
SenderPersistedException.Transient { v1: SenderException.Response response } => response.v1,
_ => null
};
}

private static string FormatSenderResponseException(global::Payjoin.ResponseException responseException)
{
return responseException switch
Expand Down
2 changes: 1 addition & 1 deletion rust-payjoin
Submodule rust-payjoin updated 100 files
Loading