public void CorrectAmountText(string amount, bool expectedResult, string expectedCorrection) { var result = BitcoinInput.TryCorrectAmount(amount, out var correction); Assert.Equal(expectedCorrection, correction); Assert.Equal(expectedResult, result); }
/// <summary> /// Reads a single transaction off the blockchain in sequential order /// /// thanks /// https://en.bitcoin.it/wiki/Protocol_specification#block /// http://james.lab6.com/2012/01/12/bitcoin-285-bytes-that-changed-the-world/ /// https://code.google.com/p/blockchain/source/browse/trunk/BlockChain.h /// </summary> public Boolean Read() { var newBlock = new BitcoinBlock { MagicBytes = Dequeue(4) }; if (BitConverter.ToUInt32(newBlock.MagicBytes, 0) != 3652501241) { throw new Exception("Invalid magic number at the start of this block"); } newBlock.BlockSize = BitConverter.ToUInt32(Dequeue(4), 0); newBlock.BlockFormatVersion = BitConverter.ToUInt32(Dequeue(4), 0); newBlock.PreviousBlockHash = ReverseArray(Dequeue(32)); newBlock.MerkleRoot = ReverseArray(Dequeue(32)); newBlock.TimeStamp = Helper.UnixToDateTime(BitConverter.ToUInt32(Dequeue(4), 0)); newBlock.Bits = BitConverter.ToUInt32(Dequeue(4), 0); newBlock.Nonce = BitConverter.ToUInt32(Dequeue(4), 0); newBlock.TransactionCount = ReadVariableLengthInteger(Dequeue(1)); for (var t = 0; t < newBlock.TransactionCount; t++) { var newTransaction = new BitcoinTransaction(); newTransaction.TransactionVersionNumber = BitConverter.ToUInt32(Dequeue(4), 0); newTransaction.InputCount = ReadVariableLengthInteger(Dequeue(1)); for (var i = 0; i < newTransaction.InputCount; i++) { var newInput = new BitcoinInput(); newInput.InputTransactionHash = Dequeue(32); newInput.InputTransactionIndex = BitConverter.ToUInt32(Dequeue(4), 0); newInput.ResponseScriptLength = ReadVariableLengthInteger(Dequeue(1)); newInput.ResponseScript = Dequeue((int)newInput.ResponseScriptLength); newInput.SequenceNumber = BitConverter.ToUInt32(Dequeue(4), 0); newTransaction.Inputs.Add(newInput); } newTransaction.OutputCount = ReadVariableLengthInteger(Dequeue(1)); for (var o = 0; o < newTransaction.OutputCount; o++) { var newOutput = new BitcoinOutput(); newOutput.OutputValue = BitConverter.ToUInt64(Dequeue(8), 0); newOutput.ChallengeScriptLength = ReadVariableLengthInteger(Dequeue(1)); newOutput.ChallengeScript = Dequeue((int)newOutput.ChallengeScriptLength); newOutput.EcdsaPublickey = ExtractPublicKey(newOutput.ChallengeScript); newOutput.BitcoinAddress = ComputeBitcoinAddress(newOutput.EcdsaPublickey); // todo: expensive operation, should it be a feature flag? newTransaction.Outputs.Add(newOutput); } newTransaction.LockTime = BitConverter.ToUInt32(Dequeue(4), 0); newBlock.Transactions.Add(newTransaction); } CurrentBlock = newBlock; return(true); }
protected SendControlViewModel(Wallet wallet, string title) : base(title) { Global = Locator.Current.GetService <Global>(); Wallet = wallet; LabelSuggestion = new SuggestLabelViewModel(); _buildTransactionButtonText = DoButtonText; this.ValidateProperty(x => x.Address, ValidateAddress); this.ValidateProperty(x => x.CustomChangeAddress, ValidateCustomChangeAddress); this.ValidateProperty(x => x.Password, ValidatePassword); this.ValidateProperty(x => x.UserFeeText, ValidateUserFeeText); ResetUi(); CoinList = new CoinListViewModel(Wallet, Global.Config, Global.UiConfig, displayCommonOwnershipWarning: true); Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => SetFeesAndTexts()); _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y) .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler); SetFeeTargetLimits(); FeeTarget = Global.UiConfig.FeeTarget; FeeDisplayFormat = (FeeDisplayFormat)(Enum.ToObject(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat) ?? FeeDisplayFormat.SatoshiPerByte); SetFeesAndTexts(); this.WhenAnyValue(x => x.AmountText) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { if (Money.TryParse(x.TrimStart('~', ' '), out Money amountBtc)) { SetAmountWatermark(amountBtc); } else { SetAmountWatermark(Money.Zero); } SetFees(); }); AmountKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) => { if (IsMax) { SetFeesAndTexts(); } else if (BitcoinInput.TryCorrectAmount(AmountText, out var betterAmount)) { AmountText = betterAmount; } }); this.WhenAnyValue(x => x.IsBusy, x => x.IsHardwareBusy) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => BuildTransactionButtonText = IsHardwareBusy ? WaitingForHardwareWalletButtonTextString : IsBusy ? DoingButtonText : DoButtonText); Observable .Merge(this.WhenAnyValue(x => x.FeeTarget).Select(_ => true)) .Merge(this.WhenAnyValue(x => x.IsEstimateAvailable).Select(_ => true)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { IsSliderFeeUsed = IsEstimateAvailable; SetFeesAndTexts(); }); this.WhenAnyValue(x => x.IsSliderFeeUsed) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(enabled => FeeControlOpacity = enabled ? 1 : 0.5); // Give the control the disabled feeling. Real Disable it not a solution as we have to detect if the slider is moved. MaxCommand = ReactiveCommand.Create(() => IsMax = !IsMax, outputScheduler: RxApp.MainThreadScheduler); this.WhenAnyValue(x => x.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { if (IsMax) { SetFeesAndTexts(); LabelToolTip = "Spending whole coins does not generate change, thus labeling is unnecessary."; } else { AmountText = "0.0"; LabelToolTip = "Who can link this transaction to you? E.g.: \"Max, BitPay\""; } }); // Triggering the detection of same address values. this.WhenAnyValue(x => x.Address) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomChangeAddress))); this.WhenAnyValue(x => x.CustomChangeAddress) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Address))); this.WhenAnyValue(x => x.IsCustomChangeAddressVisible) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(Address)); this.RaisePropertyChanged(nameof(CustomChangeAddress)); }); FeeRateCommand = ReactiveCommand.Create(ChangeFeeRateDisplay, outputScheduler: RxApp.MainThreadScheduler); OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) => OnAddressPaste(url)); BuildTransactionCommand = ReactiveCommand.CreateFromTask(async() => { try { IsBusy = true; MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BuildingTransaction); var label = new SmartLabel(LabelSuggestion.Label); LabelSuggestion.Label = label; if (!IsMax && label.IsEmpty) { NotificationHelpers.Warning("Label is required.", ""); return; } var selectedCoinViewModels = CoinList.Coins.Where(cvm => cvm.IsSelected); var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList(); if (!selectedCoinReferences.Any()) { NotificationHelpers.Warning("No coins are selected to spend.", ""); return; } BitcoinAddress address; try { address = BitcoinAddress.Create(Address, Global.Network); } catch (FormatException) { NotificationHelpers.Warning("Invalid address.", ""); return; } var requests = new List <DestinationRequest>(); if (IsCustomChangeAddressVisible && !string.IsNullOrWhiteSpace(CustomChangeAddress)) { try { var customChangeAddress = BitcoinAddress.Create(CustomChangeAddress, Global.Network); if (customChangeAddress == address) { NotificationHelpers.Warning("The active address and the change address cannot be the same.", ""); return; } requests.Add(new DestinationRequest(customChangeAddress, MoneyRequest.CreateChange(subtractFee: true), label)); } catch (FormatException) { NotificationHelpers.Warning("Invalid custom change address.", ""); return; } } MoneyRequest moneyRequest; if (IsMax) { moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true); } else { if (!Money.TryParse(AmountText, out Money amount) || amount == Money.Zero) { NotificationHelpers.Warning("Invalid amount."); return; } if (amount == selectedCoinViewModels.Sum(x => x.Amount)) { NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", ""); return; } moneyRequest = MoneyRequest.Create(amount, subtractFee: false); } if (FeeRate is null || FeeRate.SatoshiPerByte < 1) { NotificationHelpers.Warning("Invalid fee rate.", ""); return; } var feeStrategy = FeeStrategy.CreateFromFeeRate(FeeRate); var activeDestinationRequest = new DestinationRequest(address, moneyRequest, label); requests.Add(activeDestinationRequest); var intent = new PaymentIntent(requests); try { MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.DequeuingSelectedCoins); OutPoint[] toDequeue = selectedCoinViewModels.Where(x => x.CoinJoinInProgress).Select(x => x.Model.OutPoint).ToArray(); if (toDequeue is { } && toDequeue.Any()) { await Wallet.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding); } } catch { NotificationHelpers.Error("Cannot spend mixing coins.", ""); return; } finally { MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.DequeuingSelectedCoins); } if (!Wallet.KeyManager.IsWatchOnly) { try { PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out var compatiblityPasswordUsed); // We could use TryPassword but we need the exception. if (compatiblityPasswordUsed is { }) { Password = compatiblityPasswordUsed; // Overwrite the password for BuildTransaction function. NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } } catch (SecurityException ex) { NotificationHelpers.Error(ex.Message, ""); return; } catch (Exception ex) { Logger.LogError(ex); NotificationHelpers.Error(ex.ToUserFriendlyString()); return; } }
public void ParseSatoshiFeeText(bool isValid, string feeText) { Assert.Equal(isValid, BitcoinInput.TryParseSatoshiFeeText(feeText, out var _)); }