public void AddOrUpdate(SmartTransaction tx) { lock (Lock) { AddOrUpdateNoLock(tx); } }
public bool TryUpdate(SmartTransaction tx) { var hash = tx.GetHash(); lock (Lock) { // Do Contains first, because it's fast. if (ConfirmedStore.TryUpdate(tx)) { return(true); } else if (tx.Confirmed && MempoolStore.TryRemove(hash, out SmartTransaction originalTx)) { originalTx.TryUpdate(tx); ConfirmedStore.TryAddOrUpdate(originalTx); return(true); } else if (MempoolStore.TryUpdate(tx)) { return(true); } } return(false); }
public bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx) { lock (TransactionsLock) { return(Transactions.TryGetValue(hash, out sameStx)); } }
private bool TryUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryGetValue(hash, out var found)) { return(found.TryUpdate(tx)); } return(false); }
public virtual bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx) { lock (Lock) { if (MempoolStore.TryGetTransaction(hash, out sameStx)) { return(true); } return(ConfirmedStore.TryGetTransaction(hash, out sameStx)); } }
private async Task InitializeTransactionsNoLockAsync(CancellationToken cancel) { try { IoHelpers.EnsureFileExists(TransactionsFileManager.FilePath); cancel.ThrowIfCancellationRequested(); var allLines = await TransactionsFileManager.ReadAllLinesAsync(cancel).ConfigureAwait(false); var allTransactions = allLines .Select(x => SmartTransaction.FromLine(x, Network)) .OrderByBlockchain(); var added = false; var updated = false; lock (TransactionsLock) { foreach (var tx in allTransactions) { var(isAdded, isUpdated) = TryAddOrUpdateNoLockNoSerialization(tx); if (isAdded) { added = true; } if (isUpdated) { updated = true; } } } if (added || updated) { cancel.ThrowIfCancellationRequested(); // Another process worked into the file and appended the same transaction into it. // In this case we correct the file by serializing the unique set. await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false); } } catch (Exception ex) when(ex is not OperationCanceledException) { // We found a corrupted entry. Stop here. // Delete the currupted file. // Do not try to autocorrect, because the internal data structures are throwing events that may confuse the consumers of those events. Logger.LogError($"{TransactionsFileManager.FileNameWithoutExtension} file got corrupted. Deleting it..."); TransactionsFileManager.DeleteMe(); throw; } }
public bool TryUpdate(SmartTransaction tx) { bool ret; lock (TransactionsLock) { ret = TryUpdateNoLockNoSerialization(tx); } if (ret) { AbandonedTasks.AddAndClearCompleted(TryUpdateFileAsync(tx)); } return(ret); }
public bool TryRemove(uint256 hash, out SmartTransaction stx) { bool isRemoved; lock (TransactionsLock) { isRemoved = Transactions.Remove(hash, out stx); } if (isRemoved) { _ = TryRemoveFromFileAsync(hash); } return(isRemoved); }
public bool TryUpdate(SmartTransaction tx) { bool ret; lock (TransactionsLock) { ret = TryUpdateNoLockNoSerialization(tx); } if (ret) { _ = TryUpdateFileAsync(tx); } return(ret); }
public bool TryRemove(uint256 hash, out SmartTransaction stx) { bool isRemoved; lock (TransactionsLock) { isRemoved = Transactions.Remove(hash, out stx); } if (isRemoved) { AbandonedTasks.AddAndClearCompleted(TryRemoveFromFileAsync(hash)); } return(isRemoved); }
private (bool isAdded, bool isUpdated) TryAddOrUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryAdd(hash, tx)) { return(true, false); } else { if (Transactions[hash].TryUpdate(tx)) { return(false, true); } else { return(false, false); } } }
public (bool isAdded, bool isUpdated) TryAddOrUpdate(SmartTransaction tx) { (bool isAdded, bool isUpdated)ret; lock (TransactionsLock) { ret = TryAddOrUpdateNoLockNoSerialization(tx); } if (ret.isAdded) { _ = TryAppendToFileAsync(tx); } if (ret.isUpdated) { _ = TryUpdateFileAsync(tx); } return(ret); }
public (bool isAdded, bool isUpdated) TryAddOrUpdate(SmartTransaction tx) { (bool isAdded, bool isUpdated)ret; lock (TransactionsLock) { ret = TryAddOrUpdateNoLockNoSerialization(tx); } if (ret.isAdded) { AbandonedTasks.AddAndClearCompleted(TryAppendToFileAsync(tx)); } if (ret.isUpdated) { AbandonedTasks.AddAndClearCompleted(TryUpdateFileAsync(tx)); } return(ret); }
private void AddOrUpdateNoLock(SmartTransaction tx) { var hash = tx.GetHash(); if (tx.Confirmed) { if (MempoolStore.TryRemove(hash, out SmartTransaction found)) { found.TryUpdate(tx); ConfirmedStore.TryAddOrUpdate(found); } else { ConfirmedStore.TryAddOrUpdate(tx); } } else { if (!ConfirmedStore.TryUpdate(tx)) { MempoolStore.TryAddOrUpdate(tx); } } }
private async Task TryCommitToFileAsync(ITxStoreOperation operation) { try { if (operation is null || operation.IsEmpty) { return; } // Make sure that only one call can continue. lock (OperationsLock) { var isRunning = Operations.Any(); Operations.Add(operation); if (isRunning) { return; } } // Wait until the operation list calms down. IEnumerable <ITxStoreOperation> operationsToExecute; while (true) { var count = Operations.Count; await Task.Delay(100).ConfigureAwait(false); lock (OperationsLock) { if (count == Operations.Count) { // Merge operations. operationsToExecute = OperationMerger.Merge(Operations).ToList(); Operations.Clear(); break; } } } using (await TransactionsFileAsyncLock.LockAsync().ConfigureAwait(false)) { foreach (ITxStoreOperation op in operationsToExecute) { if (op is Append appendOperation) { var toAppends = appendOperation.Transactions; try { await TransactionsFileManager.AppendAllLinesAsync(toAppends.ToBlockchainOrderedLines()).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false); } } else if (op is Remove removeOperation) { var toRemoves = removeOperation.Transactions; string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false); var toSerialize = new List <string>(); foreach (var line in allLines) { var startsWith = false; foreach (var toRemoveString in toRemoves.Select(x => x.ToString())) { startsWith = startsWith || line.StartsWith(toRemoveString, StringComparison.Ordinal); } if (!startsWith) { toSerialize.Add(line); } } try { await TransactionsFileManager.WriteAllLinesAsync(toSerialize).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false); } } else if (op is Update updateOperation) { var toUpdates = updateOperation.Transactions; string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false); IEnumerable <SmartTransaction> allTransactions = allLines.Select(x => SmartTransaction.FromLine(x, Network)); var toSerialize = new List <SmartTransaction>(); foreach (SmartTransaction tx in allTransactions) { var txsToUpdateWith = toUpdates.Where(x => x == tx); foreach (var txToUpdateWith in txsToUpdateWith) { tx.TryUpdate(txToUpdateWith); } toSerialize.Add(tx); } try { await TransactionsFileManager.WriteAllLinesAsync(toSerialize.ToBlockchainOrderedLines()).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false); } } else { throw new NotSupportedException(); } } } } catch (Exception ex) { Logger.LogError(ex); } }
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> public BuildTransactionResult BuildTransaction( PaymentIntent payments, Func <FeeRate> feeRateFetcher, IEnumerable <OutPoint>?allowedInputs = null, Func <LockTime>?lockTimeSelector = null, IPayjoinClient?payjoinClient = null) { lockTimeSelector ??= () => LockTime.Zero; long totalAmount = payments.TotalAmount.Satoshi; if (totalAmount is < 0 or > Constants.MaximumNumberOfSatoshis) { throw new ArgumentOutOfRangeException($"{nameof(payments)}.{nameof(payments.TotalAmount)} sum cannot be smaller than 0 or greater than {Constants.MaximumNumberOfSatoshis}."); } // Get allowed coins to spend. var availableCoinsView = Coins.Unspent(); List <SmartCoin> allowedSmartCoinInputs = AllowUnconfirmed // Inputs that can be used to build the transaction. ? availableCoinsView.ToList() : availableCoinsView.Confirmed().ToList(); if (allowedInputs is not null) // If allowedInputs are specified then select the coins from them. { if (!allowedInputs.Any()) { throw new ArgumentException($"{nameof(allowedInputs)} is not null, but empty."); } allowedSmartCoinInputs = allowedSmartCoinInputs .Where(x => allowedInputs.Any(y => y.Hash == x.TransactionId && y.N == x.Index)) .ToList(); // Add those that have the same script, because common ownership is already exposed. // But only if the user didn't click the "max" button. In this case he'd send more money than what he'd think. if (payments.ChangeStrategy != ChangeStrategy.AllRemainingCustom) { var allScripts = allowedSmartCoinInputs.Select(x => x.ScriptPubKey).ToHashSet(); foreach (var coin in availableCoinsView.Where(x => !allowedSmartCoinInputs.Any(y => x.TransactionId == y.TransactionId && x.Index == y.Index))) { if (!(AllowUnconfirmed || coin.Confirmed)) { continue; } if (allScripts.Contains(coin.ScriptPubKey)) { allowedSmartCoinInputs.Add(coin); } } } } // Get and calculate fee Logger.LogInfo("Calculating dynamic transaction fee..."); TransactionBuilder builder = Network.CreateTransactionBuilder(); builder.SetCoinSelector(new SmartCoinSelector(allowedSmartCoinInputs)); builder.AddCoins(allowedSmartCoinInputs.Select(c => c.Coin)); builder.SetLockTime(lockTimeSelector()); foreach (var request in payments.Requests.Where(x => x.Amount.Type == MoneyRequestType.Value)) { var amountRequest = request.Amount; builder.Send(request.Destination, amountRequest.Amount); if (amountRequest.SubtractFee) { builder.SubtractFees(); } } HdPubKey?changeHdPubKey; if (payments.TryGetCustomRequest(out DestinationRequest? custChange)) { var changeScript = custChange.Destination.ScriptPubKey; KeyManager.TryGetKeyForScriptPubKey(changeScript, out HdPubKey? hdPubKey); changeHdPubKey = hdPubKey; var changeStrategy = payments.ChangeStrategy; if (changeStrategy == ChangeStrategy.Custom) { builder.SetChange(changeScript); } else if (changeStrategy == ChangeStrategy.AllRemainingCustom) { builder.SendAllRemaining(changeScript); } else { throw new NotSupportedException(payments.ChangeStrategy.ToString()); } } else { KeyManager.AssertCleanKeysIndexed(isInternal: true); KeyManager.AssertLockedInternalKeysIndexed(14); changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).First(); builder.SetChange(changeHdPubKey.P2wpkhScript); } builder.OptInRBF = true; FeeRate feeRate = feeRateFetcher(); builder.SendEstimatedFees(feeRate); var psbt = builder.BuildPSBT(false); var spentCoins = psbt.Inputs.Select(txin => allowedSmartCoinInputs.First(y => y.OutPoint == txin.PrevOut)).ToArray(); var realToSend = payments.Requests .Select(t => (label: t.Label, destination: t.Destination, amount: psbt.Outputs.FirstOrDefault(o => o.ScriptPubKey == t.Destination.ScriptPubKey)?.Value)) .Where(i => i.amount is not null); if (!psbt.TryGetFee(out var fee)) { throw new InvalidOperationException("Impossible to get the fees of the PSBT, this should never happen."); } Logger.LogInfo($"Fee: {fee.Satoshi} Satoshi."); var vSize = builder.EstimateSize(psbt.GetOriginalTransaction(), true); Logger.LogInfo($"Estimated tx size: {vSize} vBytes."); // Do some checks Money totalSendAmountNoFee = realToSend.Sum(x => x.amount); if (totalSendAmountNoFee == Money.Zero) { throw new InvalidOperationException("The amount after subtracting the fee is too small to be sent."); } Money totalOutgoingAmountNoFee; if (changeHdPubKey is null) { totalOutgoingAmountNoFee = totalSendAmountNoFee; } else { totalOutgoingAmountNoFee = realToSend.Where(x => !changeHdPubKey.ContainsScript(x.destination.ScriptPubKey)).Sum(x => x.amount); } decimal totalOutgoingAmountNoFeeDecimal = totalOutgoingAmountNoFee.ToDecimal(MoneyUnit.BTC); // Cannot divide by zero, so use the closest number we have to zero. decimal totalOutgoingAmountNoFeeDecimalDivisor = totalOutgoingAmountNoFeeDecimal == 0 ? decimal.MinValue : totalOutgoingAmountNoFeeDecimal; decimal feePc = 100 * fee.ToDecimal(MoneyUnit.BTC) / totalOutgoingAmountNoFeeDecimalDivisor; if (feePc > 1) { Logger.LogInfo($"The transaction fee is {feePc:0.#}% of the sent amount.{Environment.NewLine}" + $"Sending:\t {totalOutgoingAmountNoFee.ToString(fplus: false, trimExcessZero: true)} BTC.{Environment.NewLine}" + $"Fee:\t\t {fee.Satoshi} Satoshi."); } if (feePc > 100) { throw new InvalidOperationException($"The transaction fee is more than the sent amount: {feePc:0.#}%."); } if (spentCoins.Any(u => !u.Confirmed)) { Logger.LogInfo("Unconfirmed transaction is spent."); } // Build the transaction Logger.LogInfo("Signing transaction..."); // It must be watch only, too, because if we have the key and also hardware wallet, we do not care we can sign. psbt.AddKeyPaths(KeyManager); psbt.AddPrevTxs(TransactionStore); Transaction tx; if (KeyManager.IsWatchOnly) { tx = psbt.GetGlobalTransaction(); } else { IEnumerable <ExtKey> signingKeys = KeyManager.GetSecrets(Password, spentCoins.Select(x => x.ScriptPubKey).ToArray()); builder = builder.AddKeys(signingKeys.ToArray()); builder.SignPSBT(psbt); var isPayjoin = false; // Try to pay using payjoin if (payjoinClient is not null) { psbt = TryNegotiatePayjoin(payjoinClient, builder, psbt, changeHdPubKey); isPayjoin = true; psbt.AddKeyPaths(KeyManager); psbt.AddPrevTxs(TransactionStore); } psbt.Finalize(); tx = psbt.ExtractTransaction(); var checkResults = builder.Check(tx).ToList(); if (!psbt.TryGetEstimatedFeeRate(out var actualFeeRate)) { throw new InvalidOperationException("Impossible to get the fee rate of the PSBT, this should never happen."); } if (!isPayjoin) { // Manually check the feerate, because some inaccuracy is possible. var sb1 = feeRate.SatoshiPerByte; var sb2 = actualFeeRate.SatoshiPerByte; if (Math.Abs(sb1 - sb2) > 2) // 2s/b inaccuracy ok. { // So it'll generate a transactionpolicy error thrown below. checkResults.Add(new NotEnoughFundsPolicyError("Fees different than expected")); } } if (checkResults.Count > 0) { throw new InvalidTxException(tx, checkResults); } } var smartTransaction = new SmartTransaction(tx, Height.Unknown, label: SmartLabel.Merge(payments.Requests.Select(x => x.Label))); foreach (var coin in spentCoins) { smartTransaction.WalletInputs.Add(coin); } var label = SmartLabel.Merge(payments.Requests.Select(x => x.Label).Concat(smartTransaction.WalletInputs.Select(x => x.HdPubKey.Label))); for (var i = 0U; i < tx.Outputs.Count; i++) { TxOut output = tx.Outputs[i]; if (KeyManager.TryGetKeyForScriptPubKey(output.ScriptPubKey, out HdPubKey? foundKey)) { var smartCoin = new SmartCoin(smartTransaction, i, foundKey); label = SmartLabel.Merge(label, smartCoin.HdPubKey.Label); // foundKey's label is already added to the coinlabel. smartTransaction.WalletOutputs.Add(smartCoin); } } foreach (var coin in smartTransaction.WalletOutputs) { var foundPaymentRequest = payments.Requests.FirstOrDefault(x => x.Destination.ScriptPubKey == coin.ScriptPubKey); // If change then we concatenate all the labels. // The foundkeylabel has already been added previously, so no need to concatenate. if (foundPaymentRequest is null) // Then it's autochange. { coin.HdPubKey.SetLabel(label); } else { coin.HdPubKey.SetLabel(SmartLabel.Merge(coin.HdPubKey.Label, foundPaymentRequest.Label)); } } Logger.LogInfo($"Transaction is successfully built: {tx.GetHash()}."); var sign = !KeyManager.IsWatchOnly; return(new BuildTransactionResult(smartTransaction, psbt, sign, fee, feePc)); }
public bool Process(SmartTransaction tx) { if (!tx.Transaction.PossiblyP2WPKHInvolved()) { return(false); // We do not care about non-witness transactions for other than mempool cleanup. } uint256 txId = tx.GetHash(); var walletRelevant = false; if (tx.Confirmed) { foreach (var coin in Coins.AsAllCoinsView().CreatedBy(txId)) { coin.Height = tx.Height; walletRelevant = true; // relevant } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } } if (!tx.Transaction.IsCoinBase && !walletRelevant) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (SmartCoin coin in Coins.AsAllCoinsView()) { var spent = false; foreach (TxoRef spentOutput in coin.SpentOutputs) { foreach (TxIn txIn in tx.Transaction.Inputs) { if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N) // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null. { doubleSpends.Add(coin); spent = true; walletRelevant = true; break; } } if (spent) { break; } } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spanding transaction but a replacement transaction. var isReplacemenetTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed); if (isReplacemenetTx) { // Undo the replaced transaction by removing the coins it created (if other coin // spends it, remove that too and so on) and restoring those that it destroyed // ones. After undoing the replaced transaction it will process the replacement // transaction. var replacedTxId = doubleSpends.First().TransactionId; var(destroyed, restored) = Coins.Undo(replacedTxId); ReplaceTransactionReceived?.Invoke(this, new ReplaceTransactionReceivedEventArgs(tx, destroyed, restored)); tx.SetReplacement(); walletRelevant = true; } else { DoubleSpendReceived?.Invoke(this, new DoubleSpendReceivedEventArgs(tx, Enumerable.Empty <SmartCoin>())); return(false); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.Remove(doubleSpentCoin); } DoubleSpendReceived?.Invoke(this, new DoubleSpendReceivedEventArgs(tx, doubleSpends)); walletRelevant = true; } } } var isLikelyCoinJoinOutput = false; bool hasEqualOutputs = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default; if (hasEqualOutputs) { var receiveKeys = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript)); bool allReceivedInternal = receiveKeys.All(x => x.IsInternal); if (allReceivedInternal) { // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs. Money spentAmount = Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).TotalAmount(); Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value); bool receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m)); if (receivedAlmostAsMuchAsSpent) { isLikelyCoinJoinOutput = true; } } } List <SmartCoin> newCoins = new List <SmartCoin>(); List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey != default) { walletRelevant = true; if (output.Value <= DustThreshold) { continue; } foundKey.SetKeyState(KeyState.Used, KeyManager); spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList(); var anonset = tx.Transaction.GetAnonymitySet(i); if (spentOwnCoins.Count != 0) { anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1; // Minus 1, because do not count own. } SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. // If we did not have it. if (Coins.TryAdd(newCoin)) { newCoins.Add(newCoin); // Make sure there's always 21 clean keys generated and indexed. KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal); if (foundKey.IsInternal) { // Make sure there's always 14 internal locked keys generated and indexed. KeyManager.AssertLockedInternalKeysIndexed(14); } } else // If we had this coin already. { if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had. { SmartCoin oldCoin = Coins.AsAllCoinsView().GetByOutPoint(new OutPoint(txId, i)); if (oldCoin != null) // Just to be sure, it is a concurrent collection. { oldCoin.Height = newCoin.Height; } } } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.GetByOutPoint(input.PrevOut); if (foundCoin != null) { walletRelevant = true; var alreadyKnown = foundCoin.SpenderTransactionId == txId; foundCoin.SpenderTransactionId = txId; if (!alreadyKnown) { Coins.Spend(foundCoin); CoinSpent?.Invoke(this, foundCoin); } if (tx.Confirmed) { SpenderConfirmed?.Invoke(this, foundCoin); } } } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } foreach (var newCoin in newCoins) { CoinReceived?.Invoke(this, newCoin); } return(walletRelevant); }
public DoubleSpendReceivedEventArgs(SmartTransaction smartTransaction, IEnumerable <SmartCoin> remove) { SmartTransaction = smartTransaction; Remove = remove; }
public override bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx) { sameStx = null; return(false); }
public ReplaceTransactionReceivedEventArgs(SmartTransaction smartTransaction, IEnumerable <SmartCoin> destroyedCoins, IEnumerable <SmartCoin> restoredCoins) { SmartTransaction = smartTransaction; DestroyedCoins = destroyedCoins; RestoredCoins = restoredCoins; }