//MultiValueDictionary<string,string> private async Task Listen(InvoiceEntity invoice, PaymentMethod paymentMethod, BTCPayNetwork network) { var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; if (lightningMethod == null) { return; } var lightningSupportedMethod = invoice.GetSupportedPaymentMethod <LightningSupportedPaymentMethod>(_NetworkProvider) .FirstOrDefault(c => c.CryptoCode == network.CryptoCode); if (lightningSupportedMethod == null) { return; } var charge = new ChargeClient(lightningSupportedMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork); var session = await charge.Listen(); while (true) { var notification = await session.NextEvent(); if (notification.Id == lightningMethod.InvoiceId && notification.PaymentRequest == lightningMethod.BOLT11) { if (notification.Status == "paid" && notification.PaidAt.HasValue) { await _InvoiceRepository.AddPayment(invoice.Id, notification.PaidAt.Value, new LightningLikePaymentData() { BOLT11 = notification.PaymentRequest, Amount = notification.MilliSatoshi }, network.CryptoCode, accounted : true); _Aggregator.Publish(new InvoiceEvent(invoice.Id, 1002, "invoice_receivedPayment")); break; } if (notification.Status == "expired") { break; } } } }
public async Task <IActionResult> Submit(string cryptoCode) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(BadRequest(CreatePayjoinError(400, "invalid-network", "Incorrect network"))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError(413, "payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError(411, "missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } Transaction originalTx = null; FeeRate originalFeeRate = null; bool psbtFormat = true; if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError(400, "invalid-format", "invalid transaction or psbt"))); } originalTx = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } else { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT should be finalized"))); } originalTx = psbt.ExtractTransaction(); } async Task BroadcastNow() { await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx); } var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (sendersInputType is null) { return(BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support segwit inputs (of the same type)"))); } if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError(400, "need-utxo-information", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(originalTx, true); if (!mempool.Success) { return(BadRequest(CreatePayjoinError(400, "invalid-transaction", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); async Task UnlockUTXOs() { await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray()); } PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"))); } if (sendersInputType != receiverInputsType) { return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin with the sender's inputs type"))); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { return(UnprocessableEntity(CreatePayjoinError(422, "already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray())) { return(BadRequest(CreatePayjoinError(400, "inputs-already-used", "Some of those inputs have already been used to make payjoin transaction"))); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in await SelectUTXO(network, utxos, output.Value, psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray())) { selectedUTXOs.Add(utxo.Outpoint, utxo); } originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError(400, "invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError(400, "invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { await BroadcastNow(); return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin for now"))); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // 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"))); } Money contributedAmount = Money.Zero; var newTx = originalTx.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; newTx.Inputs.Add(selectedUTXO.Outpoint); } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Probably receiving some spare change, let's add an output to make // it looks more like a normal transaction if (newTx.Outputs.Count == 1) { var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change); var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi; // Randomly round the amount to make the payment output look like a change output var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount)); while (roundMultiple > 1_000UL) { if (RandomUtils.GetUInt32() % 2 == 0) { roundMultiple = roundMultiple / 10; } else { randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple; break; } } var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey); if (fakeChange.IsDust(minRelayTxFee)) { randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee); fakeChange.Value = randomChangeAmount; } if (randomChangeAmount < contributedAmount) { ourNewOutput.Value -= fakeChange.Value; newTx.Outputs.Add(fakeChange); isOurOutput.Add(fakeChange); } } var rand = new Random(); Utils.Shuffle(newTx.Inputs, rand); Utils.Shuffle(newTx.Outputs, rand); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); txBuilder.AddCoins(psbt.Inputs.Select(i => i.GetSignableCoin())); txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) { if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++) { if (!isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; } } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee) { await UnlockUTXOs(); await BroadcastNow(); return(UnprocessableEntity(CreatePayjoinError(422, "not-enough-money", "Not enough money is sent to pay for the additional payjoin inputs"))); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.Sign(privateKey); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index), originalTx.RBF); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { 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 && HexEncoder.IsWellFormed(rawBody)) { return(Ok(newPsbt.ToHex())); } else if (psbtFormat) { return(Ok(newPsbt.ToBase64())); } else { return(Ok(newTx.ToHex())); } }
public async Task <IActionResult> Submit(string cryptoCode, long?maxadditionalfeecontribution, int?additionalfeeoutputindex, decimal minfeerate = -1.0m, bool disableoutputsubstitution = false, int v = 1) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(NotFound()); } if (v != 1) { return(BadRequest(new JObject { new JProperty("errorCode", "version-unsupported"), new JProperty("supported", new JArray(1)), new JProperty("message", "This version of payjoin is not supported.") })); } await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository); ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug) { ctx.Logs.Write($"Payjoin error: {debug}"); return(StatusCode(httpCode, CreatePayjoinError(err, debug))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError("payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError("missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } FeeRate originalFeeRate = null; bool psbtFormat = true; if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT should be finalized"))); } ctx.OriginalTransaction = psbt.ExtractTransaction(); } // BTCPay Server implementation support a transaction instead of PSBT else { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt"))); } ctx.OriginalTransaction = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } FeeRate senderMinFeeRate = minfeerate >= 0.0m ? new FeeRate(minfeerate) : null; Money allowedSenderFeeContribution = Money.Satoshis(maxadditionalfeecontribution is long t && t >= 0 ? t : 0); var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true); if (!mempool.Success) { ctx.DoNotBroadcast(); return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var enforcedLowR = ctx.OriginalTransaction.Inputs.All(IsLowR); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin")); } if (sendersInputType is ScriptPubKeyType t1 && t1 != receiverInputsType) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type")); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { ctx.DoNotBroadcast(); return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction")); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC), psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO) { selectedUTXOs.Add(utxo.Outpoint, utxo); } ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray(); originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError("invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError("invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin")); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed")); } Money contributedAmount = Money.Zero; var newTx = ctx.OriginalTransaction.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); TxOut feeOutput = additionalfeeoutputindex is int feeOutputIndex && maxadditionalfeecontribution is long v3 && v3 >= 0 && feeOutputIndex >= 0 && feeOutputIndex < newTx.Outputs.Count && !isOurOutput.Contains(newTx.Outputs[feeOutputIndex]) ? newTx.Outputs[feeOutputIndex] : null; var rand = new Random(); int senderInputCount = newTx.Inputs.Count; foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; 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 ?? new FeeRate(1.0m); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); var coins = psbt.Inputs.Select(i => i.GetSignableCoin()) .Concat(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))).ToArray(); txBuilder.AddCoins(coins); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) { if (disableoutputsubstitution) { break; } if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change if (feeOutput != null) { var outputContribution = Money.Min(additionalFee, feeOutput.Value); outputContribution = Money.Min(outputContribution, feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee)); outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution); feeOutput.Value -= outputContribution; additionalFee -= outputContribution; allowedSenderFeeContribution -= outputContribution; } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee)) { return(CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs")); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.Sign(privateKey, new SigningOptions() { EnforceLowR = enforcedLowR }); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index), ctx.OriginalTransaction.RBF); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The original transaction has already been accounted"))); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = new WalletId(invoice.StoreId, network.CryptoCode), TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => new KeyValuePair <uint256, List <(string color, string label)> >(utxo.Key, new List <(string color, string label)>() { UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id) })) .ToDictionary(pair => pair.Key, pair => pair.Value) });
private async Task Listen(BTCPayWallet wallet) { var network = wallet.Network; bool cleanup = false; try { if (_SessionsByCryptoCode.ContainsKey(network.CryptoCode)) { return; } var client = _ExplorerClients.GetExplorerClient(network); if (client == null) { return; } if (_Cts.IsCancellationRequested) { return; } var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false); if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session)) { await session.DisposeAsync(); return; } cleanup = true; using (session) { await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false); await session.ListenAllDerivationSchemesAsync(cancellation : _Cts.Token).ConfigureAwait(false); Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline..."); int paymentCount = await FindPaymentViaPolling(wallet, network); Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline"); Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})"); while (!_Cts.IsCancellationRequested) { var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false); switch (newEvent) { case NBXplorer.Models.NewBlockEvent evt: await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices()) .Select(invoiceId => UpdatePaymentStates(wallet, invoiceId)) .ToArray()); _Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode }); break; case NBXplorer.Models.NewTransactionEvent evt: wallet.InvalidateCache(evt.DerivationStrategy); foreach (var output in evt.Outputs) { foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins() .Where(o => o.ScriptPubKey == output.ScriptPubKey) .Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem))) { var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode); if (invoice != null) { var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF); var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); if (!alreadyExist) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode); await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy); } else { await UpdatePaymentStates(wallet, invoice.Id); } } } } break; default: Logs.PayServer.LogWarning("Received unknown message from NBXplorer"); break; } } } } catch when(_Cts.IsCancellationRequested) { }
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { if (evt is CatchUp) { DateTimeOffset start = DateTimeOffset.Now; await UpdateAnyPendingEthLikePaymentAndAddressWatchList(cancellationToken); TimeSpan diff = start - DateTimeOffset.Now; if (diff.TotalSeconds < 5) { _ = Task.Delay(TimeSpan.FromSeconds(5 - diff.TotalSeconds), cancellationToken).ContinueWith(task => { _eventAggregator.Publish(new CatchUp()); return(Task.CompletedTask); }, cancellationToken, TaskContinuationOptions.None, TaskScheduler.Current); } } if (evt is EthereumAddressBalanceFetched response) { if (response.ChainId != ChainId) { return; } var network = Networks.SingleOrDefault(payNetwork => payNetwork.CryptoCode.Equals(response.CryptoCode, StringComparison.InvariantCultureIgnoreCase)); if (network is null) { return; } var invoice = response.InvoiceEntity; if (invoice is null) { return; } var existingPayment = response.MatchedExistingPayment; if (existingPayment is null && response.Amount > 0) { //new payment var paymentData = new EthereumLikePaymentData() { Address = response.Address, CryptoCode = response.CryptoCode, Amount = response.Amount, Network = network, BlockNumber = response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber ? (long?)response.BlockParameter.BlockNumber.Value : (long?)null, ConfirmationCount = 0, AccountIndex = response.PaymentMethodDetails.Index, XPub = response.PaymentMethodDetails.XPub }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network, true); if (payment != null) { ReceivedPayment(invoice, payment); } } else if (existingPayment != null) { var cd = (EthereumLikePaymentData)existingPayment.GetCryptoPaymentData(); //existing payment amount was changed. Set to unaccounted and register as a new payment. if (response.Amount == 0 || response.Amount != cd.Amount) { existingPayment.Accounted = false; await _invoiceRepository.UpdatePayments(new List <PaymentEntity>() { existingPayment }); if (response.Amount > 0) { var paymentData = new EthereumLikePaymentData() { Address = response.Address, CryptoCode = response.CryptoCode, Amount = response.Amount, Network = network, BlockNumber = response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber ? (long?)response.BlockParameter.BlockNumber.Value : null, ConfirmationCount = response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber ? 1 : 0, AccountIndex = cd.AccountIndex, XPub = cd.XPub }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network, true); if (payment != null) { ReceivedPayment(invoice, payment); } } } else if (response.Amount == cd.Amount) { //transition from pending to 1 confirmed if (cd.BlockNumber is null && response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber) { cd.ConfirmationCount = 1; cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value; existingPayment.SetCryptoPaymentData(cd); await _invoiceRepository.UpdatePayments(new List <PaymentEntity>() { existingPayment }); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); } //increment confirm count else if (response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber) { if (response.BlockParameter.BlockNumber.Value > cd.BlockNumber.Value) { cd.ConfirmationCount = (long)(response.BlockParameter.BlockNumber.Value - cd.BlockNumber.Value); } else { cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value; cd.ConfirmationCount = 1; } existingPayment.SetCryptoPaymentData(cd); await _invoiceRepository.UpdatePayments(new List <PaymentEntity>() { existingPayment }); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); } } }
private async Task Listen(BTCPayWallet wallet) { var network = wallet.Network; bool cleanup = false; try { if (_SessionsByCryptoCode.ContainsKey(network.CryptoCode)) { return; } var client = _ExplorerClients.GetExplorerClient(network); if (client == null) { return; } if (_Cts.IsCancellationRequested) { return; } var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false); if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session)) { await session.DisposeAsync(); return; } cleanup = true; using (session) { await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false); await session.ListenAllDerivationSchemesAsync(cancellation : _Cts.Token).ConfigureAwait(false); Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline..."); int paymentCount = await FindPaymentViaPolling(wallet, network); Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline"); Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})"); while (!_Cts.IsCancellationRequested) { var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false); switch (newEvent) { case NBXplorer.Models.NewBlockEvent evt: await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices()) .Select(invoiceId => UpdatePaymentStates(wallet, invoiceId)) .ToArray()); _Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode }); break; case NBXplorer.Models.NewTransactionEvent evt: wallet.InvalidateCache(evt.DerivationStrategy); foreach (var output in network.GetValidOutputs(evt)) { var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice != null) { var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, output.Item1.KeyPath, output.Item1.ScriptPubKey); var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); var alreadyExist = invoice.GetAllBitcoinPaymentData().Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); if (!alreadyExist) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); if (payment != null) { await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); } } else { await UpdatePaymentStates(wallet, invoice.Id); } } } _Aggregator.Publish(new NewOnChainTransactionEvent() { CryptoCode = wallet.Network.CryptoCode, NewTransactionEvent = evt }); break; default: Logs.PayServer.LogWarning("Received unknown message from NBXplorer"); break; } } } } catch when(_Cts.IsCancellationRequested) { }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; //Fetch unknown payments var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); var getCoinsResponsesAsync = strategies .Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token)) .ToArray(); await Task.WhenAll(getCoinsResponsesAsync); var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray(); foreach (var response in getCoinsResponses) { response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray(); } var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault(); bool dirtyAddress = false; if (coins != null) { context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State); var alreadyAccounted = new HashSet <OutPoint>(invoice.Payments.Select(p => p.Outpoint)); foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false); invoice.Payments.Add(payment); context.Events.Add(new InvoicePaymentEvent(invoice.Id)); dirtyAddress = true; } } ////// var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key); var cryptoData = invoice.GetCryptoData(network); var cryptoDataAll = invoice.GetCryptoData(); var accounting = cryptoData.Calculate(); if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired")); invoice.Status = "expired"; } if (invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid")); invoice.Status = "paid"; invoice.ExceptionStatus = null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == "expired") { invoice.ExceptionStatus = "paidLate"; context.MarkDirty(); } } if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); if (dirtyAddress) { var address = await _Wallet.ReserveAddressAsync(coins.Strategy); Logs.PayServer.LogInformation("Generate new " + address); await _InvoiceRepository.NewAddress(invoice.Id, address, network); } } } if (invoice.Status == "paid") { var transactions = await GetPaymentsWithTransaction(network, invoice); if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF); } else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1); } else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed) { transactions = transactions.Where(t => t.Confirmations >= 6); } var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (totalConfirmed < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid")); invoice.Status = "invalid"; context.MarkDirty(); } else if (totalConfirmed >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } } if (invoice.Status == "confirmed") { var transactions = await GetPaymentsWithTransaction(network, invoice); transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) { context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete")); invoice.Status = "complete"; context.MarkDirty(); } } }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; } var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); foreach (Task <NetworkCoins> coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies)) { var coins = await coinsAsync; if (coins.TimestampedCoins.Length == 0) { continue; } bool dirtyAddress = false; if (coins.State != null) { context.ModifiedKnownStates.AddOrReplace(coins.Wallet.Network, coins.State); } var alreadyAccounted = new HashSet <OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint)); foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint))) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Wallet.Network.CryptoCode).ConfigureAwait(false); #pragma warning disable CS0618 invoice.Payments.Add(payment); #pragma warning restore CS0618 alreadyAccounted.Add(coin.Coin.Outpoint); context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment")); dirtyAddress = true; } if (dirtyAddress) { payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); } var network = coins.Wallet.Network; var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider); var accounting = cryptoData.Calculate(); if (invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = payments.Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull")); invoice.Status = "paid"; invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate") { invoice.ExceptionStatus = "paidLate"; context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration")); context.MarkDirty(); } } if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); if (dirtyAddress) { var address = await coins.Wallet.ReserveAddressAsync(coins.Strategy); Logs.PayServer.LogInformation("Generate new " + address); await _InvoiceRepository.NewAddress(invoice.Id, address, network); } } } if (invoice.Status == "paid") { IEnumerable <AccountedPaymentEntity> transactions = payments; if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF); } else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1); } else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed) { transactions = transactions.Where(t => t.Confirmations >= 6); } var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (totalConfirmed < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } else if (totalConfirmed >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } } if (invoice.Status == "confirmed") { IEnumerable <AccountedPaymentEntity> transactions = payments; transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; context.MarkDirty(); } } } }