mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Refactor the detection of the payjoin payment
This commit is contained in:
@@ -130,21 +130,22 @@ namespace BTCPayServer.Tests
|
|||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||||
var payments = invoice.GetPayments().ToArray();
|
var payments = invoice.GetPayments();
|
||||||
var originalPayment = payments
|
Assert.Equal(2, payments.Count);
|
||||||
.Single(p =>
|
var originalPayment = payments[0];
|
||||||
p.GetCryptoPaymentData() is BitcoinLikePaymentData pd &&
|
var coinjoinPayment = payments[1];
|
||||||
pd.PayjoinInformation?.Type is PayjoinTransactionType.Original);
|
|
||||||
var coinjoinPayment = payments
|
|
||||||
.Single(p =>
|
|
||||||
p.GetCryptoPaymentData() is BitcoinLikePaymentData pd &&
|
|
||||||
pd.PayjoinInformation?.Type is PayjoinTransactionType.Coinjoin);
|
|
||||||
Assert.Equal(-1, ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
|
Assert.Equal(-1, ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||||
Assert.Equal(0, ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
|
Assert.Equal(0, ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||||
Assert.False(originalPayment.Accounted);
|
Assert.False(originalPayment.Accounted);
|
||||||
Assert.True(coinjoinPayment.Accounted);
|
Assert.True(coinjoinPayment.Accounted);
|
||||||
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
|
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
|
||||||
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
|
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
|
||||||
|
Assert.Equal(originalPayment.GetCryptoPaymentData()
|
||||||
|
.AssertType<BitcoinLikePaymentData>()
|
||||||
|
.Value,
|
||||||
|
coinjoinPayment.GetCryptoPaymentData()
|
||||||
|
.AssertType<BitcoinLikePaymentData>()
|
||||||
|
.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
@@ -211,10 +212,21 @@ namespace BTCPayServer.Tests
|
|||||||
return pj;
|
return pj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task LockNewReceiverCoin()
|
||||||
|
{
|
||||||
|
var coins = await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme);
|
||||||
|
foreach (var coin in coins.Where(c => c.OutPoint != receiverCoin.Outpoint))
|
||||||
|
{
|
||||||
|
await payjoinRepository.TryLock(coin.OutPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
|
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
|
||||||
"there is not enough to pay the additional payjoin input. (going below the min relay fee");
|
"there is not enough to pay the additional payjoin input. (going below the min relay fee" +
|
||||||
|
"However, the original tx has been broadcasted!");
|
||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), ExpectLocked: false, ExpectedError: "not-enough-money");
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), ExpectLocked: false, ExpectedError: "not-enough-money");
|
||||||
await RunVector();
|
await RunVector();
|
||||||
|
await LockNewReceiverCoin();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("We don't pay enough");
|
Logs.Tester.LogInformation("We don't pay enough");
|
||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), ExpectLocked: false, ExpectedError: "invoice-not-fully-paid");
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), ExpectLocked: false, ExpectedError: "invoice-not-fully-paid");
|
||||||
@@ -229,13 +241,8 @@ namespace BTCPayServer.Tests
|
|||||||
await payjoinRepository.TryLock(receiverCoin.Outpoint);
|
await payjoinRepository.TryLock(receiverCoin.Outpoint);
|
||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), ExpectLocked: true, ExpectedError: "out-of-utxos");
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), ExpectLocked: true, ExpectedError: "out-of-utxos");
|
||||||
await RunVector();
|
await RunVector();
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await LockNewReceiverCoin();
|
||||||
{
|
|
||||||
var coins = await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme);
|
|
||||||
Assert.Equal(2, coins.Length);
|
|
||||||
var newCoin = coins.First(c => (Money)c.Value == Money.Satoshis(500));
|
|
||||||
await payjoinRepository.TryLock(newCoin.OutPoint);
|
|
||||||
});
|
|
||||||
var originalSenderUser = senderUser;
|
var originalSenderUser = senderUser;
|
||||||
retry:
|
retry:
|
||||||
// Additional fee is 96 , minrelaytx is 294
|
// Additional fee is 96 , minrelaytx is 294
|
||||||
@@ -574,7 +581,8 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
|
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
|
||||||
Assert.Equal(InvoiceStatus.Paid, invoiceEntity.Status);
|
Assert.Equal(InvoiceStatus.Paid, invoiceEntity.Status);
|
||||||
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted && ((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation.Type is PayjoinTransactionType.Coinjoin);
|
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted &&
|
||||||
|
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
|
||||||
});
|
});
|
||||||
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
|
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
|
||||||
|
|
||||||
|
|||||||
@@ -301,9 +301,11 @@ namespace BTCPayServer.Tests
|
|||||||
var store = await storeRepository.FindStore(StoreId);
|
var store = await storeRepository.FindStore(StoreId);
|
||||||
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
|
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
|
||||||
.First();
|
.First();
|
||||||
|
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
|
||||||
if (expectedError is null)
|
if (expectedError is null)
|
||||||
{
|
{
|
||||||
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
|
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
|
||||||
|
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
|
||||||
Assert.NotNull(proposed);
|
Assert.NotNull(proposed);
|
||||||
return proposed;
|
return proposed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
|
|
||||||
public bool Replaced { get; set; }
|
public bool Replaced { get; set; }
|
||||||
public BitcoinLikePaymentData CryptoPaymentData { get; set; }
|
public BitcoinLikePaymentData CryptoPaymentData { get; set; }
|
||||||
|
public string AdditionalInformation { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OffChainPaymentViewModel
|
public class OffChainPaymentViewModel
|
||||||
|
|||||||
@@ -111,13 +111,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
|
|
||||||
public class PayjoinInformation
|
public class PayjoinInformation
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
public uint256 CoinjoinTransactionHash { get; set; }
|
||||||
public PayjoinTransactionType Type { get; set; }
|
public Money CoinjoinValue { get; set; }
|
||||||
public OutPoint[] ContributedOutPoints { get; set; }
|
public OutPoint[] ContributedOutPoints { get; set; }
|
||||||
}
|
}
|
||||||
public enum PayjoinTransactionType
|
|
||||||
{
|
|
||||||
Original,
|
|
||||||
Coinjoin
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
.ToArray(), true);
|
.ToArray(), true);
|
||||||
bool? originalPJBroadcasted = null;
|
bool? originalPJBroadcasted = null;
|
||||||
bool? originalPJBroadcastable = null;
|
bool? originalPJBroadcastable = null;
|
||||||
bool? cjPJBroadcasted = null;
|
bool cjPJBroadcasted = false;
|
||||||
OutPoint[] ourPJOutpoints = null;
|
PayjoinInformation payjoinInformation = null;
|
||||||
var paymentEntitiesByPrevOut = new Dictionary<OutPoint, PaymentEntity>();
|
var paymentEntitiesByPrevOut = new Dictionary<OutPoint, PaymentEntity>();
|
||||||
foreach (var payment in invoice.GetPayments(wallet.Network))
|
foreach (var payment in invoice.GetPayments(wallet.Network))
|
||||||
{
|
{
|
||||||
@@ -256,17 +256,9 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
}
|
}
|
||||||
if (paymentData.PayjoinInformation is PayjoinInformation pj)
|
if (paymentData.PayjoinInformation is PayjoinInformation pj)
|
||||||
{
|
{
|
||||||
ourPJOutpoints = pj.ContributedOutPoints;
|
payjoinInformation = pj;
|
||||||
switch (pj.Type)
|
originalPJBroadcasted = accounted && tx.Confirmations >= 0;
|
||||||
{
|
originalPJBroadcastable = accounted;
|
||||||
case PayjoinTransactionType.Original:
|
|
||||||
originalPJBroadcasted = accounted && tx.Confirmations >= 0;
|
|
||||||
originalPJBroadcastable = accounted;
|
|
||||||
break;
|
|
||||||
case PayjoinTransactionType.Coinjoin:
|
|
||||||
cjPJBroadcasted = accounted && tx.Confirmations >= 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// RPC might be unavailable, we can't check double spend so let's assume there is none
|
// RPC might be unavailable, we can't check double spend so let's assume there is none
|
||||||
@@ -287,6 +279,14 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
if (paymentEntitiesByPrevOut.TryGetValue(prevout, out var replaced) && !replaced.Accounted)
|
if (paymentEntitiesByPrevOut.TryGetValue(prevout, out var replaced) && !replaced.Accounted)
|
||||||
{
|
{
|
||||||
payment.NetworkFee = replaced.NetworkFee;
|
payment.NetworkFee = replaced.NetworkFee;
|
||||||
|
if (payjoinInformation is PayjoinInformation pj &&
|
||||||
|
pj.CoinjoinTransactionHash == tx.TransactionHash)
|
||||||
|
{
|
||||||
|
// This payment is a coinjoin, so the value of
|
||||||
|
// the payment output is different from the real value of the payment
|
||||||
|
paymentData.Value = pj.CoinjoinValue;
|
||||||
|
payment.SetCryptoPaymentData(paymentData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,9 +322,9 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
if (originalPJBroadcasted is true ||
|
if (originalPJBroadcasted is true ||
|
||||||
// If the original tx is not broadcastable anymore and nor does the coinjoin
|
// If the original tx is not broadcastable anymore and nor does the coinjoin
|
||||||
// reuse our outpoint for another PJ
|
// reuse our outpoint for another PJ
|
||||||
(originalPJBroadcastable is false && !(cjPJBroadcasted is true)))
|
(originalPJBroadcastable is false && !cjPJBroadcasted))
|
||||||
{
|
{
|
||||||
await _payJoinRepository.TryUnlock(ourPJOutpoints);
|
await _payJoinRepository.TryUnlock(payjoinInformation.ContributedOutPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
|
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
originalTx = psbt.ExtractTransaction();
|
originalTx = psbt.ExtractTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task BroadcastNow()
|
||||||
|
{
|
||||||
|
await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
|
||||||
|
}
|
||||||
|
|
||||||
if (originalTx.Inputs.Any(i => !(i.GetSigner() is WitKeyId)))
|
if (originalTx.Inputs.Any(i => !(i.GetSigner() is WitKeyId)))
|
||||||
return BadRequest(CreatePayjoinError(400, "not-using-p2wpkh", "Payjoin only support P2WPKH inputs"));
|
return BadRequest(CreatePayjoinError(400, "not-using-p2wpkh", "Payjoin only support P2WPKH inputs"));
|
||||||
if (psbt.CheckSanity() is var errors && errors.Count != 0)
|
if (psbt.CheckSanity() is var errors && errors.Count != 0)
|
||||||
@@ -160,6 +165,11 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
bool paidSomething = false;
|
bool paidSomething = false;
|
||||||
Money due = null;
|
Money due = null;
|
||||||
Dictionary<OutPoint, UTXO> selectedUTXOs = new Dictionary<OutPoint, UTXO>();
|
Dictionary<OutPoint, UTXO> selectedUTXOs = new Dictionary<OutPoint, UTXO>();
|
||||||
|
|
||||||
|
async Task UnlockUTXOs()
|
||||||
|
{
|
||||||
|
await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray());
|
||||||
|
}
|
||||||
PSBTOutput paymentOutput = null;
|
PSBTOutput paymentOutput = null;
|
||||||
BitcoinAddress paymentAddress = null;
|
BitcoinAddress paymentAddress = null;
|
||||||
InvoiceEntity invoice = null;
|
InvoiceEntity invoice = null;
|
||||||
@@ -231,34 +241,14 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
|
|
||||||
if (selectedUTXOs.Count == 0)
|
if (selectedUTXOs.Count == 0)
|
||||||
{
|
{
|
||||||
await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
|
await BroadcastNow();
|
||||||
return StatusCode(503,
|
return StatusCode(503,
|
||||||
CreatePayjoinError(503, "out-of-utxos",
|
CreatePayjoinError(503, "out-of-utxos",
|
||||||
"We do not have any UTXO available for making a payjoin for now"));
|
"We do not have any UTXO available for making a payjoin for now"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalPaymentValue = paymentOutput.Value;
|
var originalPaymentValue = paymentOutput.Value;
|
||||||
// Add the original transaction to the payment
|
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network);
|
||||||
var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
|
|
||||||
paymentOutput.Value,
|
|
||||||
new OutPoint(originalTx.GetHash(), paymentOutput.Index),
|
|
||||||
originalTx.RBF);
|
|
||||||
originalPaymentData.PayjoinInformation = new PayjoinInformation()
|
|
||||||
{
|
|
||||||
Type = PayjoinTransactionType.Original, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray()
|
|
||||||
};
|
|
||||||
originalPaymentData.ConfirmationCount = -1;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
var payment = await _invoiceRepository.AddPayment(invoice.Id, now, originalPaymentData, network, true);
|
|
||||||
if (payment is null)
|
|
||||||
{
|
|
||||||
return UnprocessableEntity(CreatePayjoinError(422, "already-paid",
|
|
||||||
$"The original transaction has already been accounted"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _broadcaster.Schedule(now + TimeSpan.FromMinutes(1.0), originalTx, network);
|
|
||||||
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx);
|
|
||||||
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) {Payment = payment});
|
|
||||||
|
|
||||||
//check if wallet of store is configured to be hot wallet
|
//check if wallet of store is configured to be hot wallet
|
||||||
var extKeyStr = await explorer.GetMetadataAsync<string>(
|
var extKeyStr = await explorer.GetMetadataAsync<string>(
|
||||||
@@ -267,6 +257,8 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
if (extKeyStr == null)
|
if (extKeyStr == null)
|
||||||
{
|
{
|
||||||
// This should not happen, as we check the existance of private key before creating invoice with payjoin
|
// This should not happen, as we check the existance of private key before creating invoice with payjoin
|
||||||
|
await UnlockUTXOs();
|
||||||
|
await BroadcastNow();
|
||||||
return StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"));
|
return StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +336,8 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
if (isSecondPass)
|
if (isSecondPass)
|
||||||
{
|
{
|
||||||
// This should not happen
|
// This should not happen
|
||||||
|
await UnlockUTXOs();
|
||||||
|
await BroadcastNow();
|
||||||
return StatusCode(500,
|
return StatusCode(500,
|
||||||
CreatePayjoinError(500, "unavailable",
|
CreatePayjoinError(500, "unavailable",
|
||||||
$"This service is unavailable for now (isSecondPass)"));
|
$"This service is unavailable for now (isSecondPass)"));
|
||||||
@@ -373,7 +367,8 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
|
var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
|
||||||
if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee)
|
if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee)
|
||||||
{
|
{
|
||||||
await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray());
|
await UnlockUTXOs();
|
||||||
|
await BroadcastNow();
|
||||||
return UnprocessableEntity(CreatePayjoinError(422, "not-enough-money",
|
return UnprocessableEntity(CreatePayjoinError(422, "not-enough-money",
|
||||||
"Not enough money is sent to pay for the additional payjoin inputs"));
|
"Not enough money is sent to pay for the additional payjoin inputs"));
|
||||||
}
|
}
|
||||||
@@ -392,20 +387,30 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness;
|
newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the coinjoin transaction to the payments
|
// Add the transaction to the payments with a confirmation of -1.
|
||||||
var coinjoinPaymentData = new BitcoinLikePaymentData(paymentAddress,
|
// This will make the invoice paid even if the user do not
|
||||||
originalPaymentValue - ourFeeContribution,
|
// broadcast the payjoin.
|
||||||
new OutPoint(newPsbt.GetGlobalTransaction().GetHash(), ourOutputIndex),
|
var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
|
||||||
|
paymentOutput.Value,
|
||||||
|
new OutPoint(originalTx.GetHash(), paymentOutput.Index),
|
||||||
originalTx.RBF);
|
originalTx.RBF);
|
||||||
coinjoinPaymentData.PayjoinInformation = new PayjoinInformation()
|
originalPaymentData.ConfirmationCount = -1;
|
||||||
|
originalPaymentData.PayjoinInformation = new PayjoinInformation()
|
||||||
{
|
{
|
||||||
Type = PayjoinTransactionType.Coinjoin,
|
CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(),
|
||||||
|
CoinjoinValue = originalPaymentValue - ourFeeContribution,
|
||||||
ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray()
|
ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray()
|
||||||
};
|
};
|
||||||
coinjoinPaymentData.ConfirmationCount = -1;
|
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true);
|
||||||
payment = await _invoiceRepository.AddPayment(invoice.Id, now, coinjoinPaymentData, network, false,
|
if (payment is null)
|
||||||
payment.NetworkFee);
|
{
|
||||||
// We do not publish an event on purpose, this would be confusing for the merchant.
|
await UnlockUTXOs();
|
||||||
|
await BroadcastNow();
|
||||||
|
return UnprocessableEntity(CreatePayjoinError(422, "already-paid",
|
||||||
|
$"The original transaction has already been accounted"));
|
||||||
|
}
|
||||||
|
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx);
|
||||||
|
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) {Payment = payment});
|
||||||
|
|
||||||
if (psbtFormat)
|
if (psbtFormat)
|
||||||
return Ok(newPsbt.ToBase64());
|
return Ok(newPsbt.ToBase64());
|
||||||
|
|||||||
@@ -687,7 +687,7 @@ retry:
|
|||||||
/// <param name="cryptoCode"></param>
|
/// <param name="cryptoCode"></param>
|
||||||
/// <param name="accounted"></param>
|
/// <param name="accounted"></param>
|
||||||
/// <returns>The PaymentEntity or null if already added</returns>
|
/// <returns>The PaymentEntity or null if already added</returns>
|
||||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false, decimal? networkFee = null)
|
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false)
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
@@ -705,7 +705,7 @@ retry:
|
|||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
ReceivedTime = date.UtcDateTime,
|
ReceivedTime = date.UtcDateTime,
|
||||||
Accounted = accounted,
|
Accounted = accounted,
|
||||||
NetworkFee = networkFee ?? paymentMethodDetails.GetNextNetworkFee(),
|
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||||
Network = network
|
Network = network
|
||||||
};
|
};
|
||||||
entity.SetCryptoPaymentData(paymentData);
|
entity.SetCryptoPaymentData(paymentData);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
|
PayjoinInformation payjoinIformation = null;
|
||||||
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == BitcoinPaymentType.Instance).Select(payment =>
|
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == BitcoinPaymentType.Instance).Select(payment =>
|
||||||
{
|
{
|
||||||
var m = new OnchainPaymentViewModel();
|
var m = new OnchainPaymentViewModel();
|
||||||
@@ -21,7 +22,16 @@
|
|||||||
{
|
{
|
||||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
if (onChainPaymentData?.PayjoinInformation is PayjoinInformation pj)
|
||||||
|
{
|
||||||
|
payjoinIformation = pj;
|
||||||
|
m.AdditionalInformation = "Original tranasaction";
|
||||||
|
}
|
||||||
|
if (payjoinIformation is PayjoinInformation &&
|
||||||
|
payjoinIformation.CoinjoinTransactionHash == onChainPaymentData?.Outpoint.Hash)
|
||||||
|
{
|
||||||
|
m.AdditionalInformation = "Payjoin transaction";
|
||||||
|
}
|
||||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||||
m.ReceivedTime = payment.ReceivedTime;
|
m.ReceivedTime = payment.ReceivedTime;
|
||||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
||||||
@@ -52,7 +62,7 @@
|
|||||||
<tr class="@(payment.Replaced ? "linethrough" : "")" >
|
<tr class="@(payment.Replaced ? "linethrough" : "")" >
|
||||||
<td>@payment.Crypto</td>
|
<td>@payment.Crypto</td>
|
||||||
<td>@payment.DepositAddress</td>
|
<td>@payment.DepositAddress</td>
|
||||||
<td class="payment-value">@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.CryptoPaymentData.PayjoinInformation?.Type is PayjoinTransactionType.Coinjoin? string.Empty : $"<br/>(Payjoin)")</td>
|
<td class="payment-value">@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.AdditionalInformation is string i ? $"<br/>({i})" : string.Empty)</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="wraptextAuto">
|
<div class="wraptextAuto">
|
||||||
<a href="@payment.TransactionLink" target="_blank">
|
<a href="@payment.TransactionLink" target="_blank">
|
||||||
|
|||||||
Reference in New Issue
Block a user