Пример #1
0
        public async Task <PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken)
        {
            Guard.NotNull(nameof(originalTx), originalTx);
            if (originalTx.IsAllFinalized())
            {
                throw new InvalidOperationException("The original PSBT should not be finalized.");
            }

            var sentBefore  = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
            var oldGlobalTx = originalTx.GetGlobalTransaction();

            if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
            {
                throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
            }

            var originalFee = originalTx.GetFee();
            var cloned      = originalTx.Clone();

            if (!cloned.TryFinalize(out var _))
            {
                return(null);
            }

            // We make sure we don't send unnecessary information to the receiver
            foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }

            foreach (var output in cloned.Outputs)
            {
                output.HDKeyPaths.Clear();
            }

            cloned.GlobalXPubs.Clear();

            var bpuResponse = await HttpClient.SendAsync(HttpMethod.Post, "",
                                                         new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken).ConfigureAwait(false);

            if (!bpuResponse.IsSuccessStatusCode)
            {
                var errorStr = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                try
                {
                    var error = JObject.Parse(errorStr);
                    throw new PayjoinReceiverException((int)bpuResponse.StatusCode,
                                                       error["errorCode"].Value <string>(),
                                                       error["message"].Value <string>());
                }
                catch (JsonReaderException)
                {
                    // will throw
                    bpuResponse.EnsureSuccessStatusCode();
                    throw;
                }
            }

            var hexOrBase64 = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

            var newPSBT = PSBT.Parse(hexOrBase64, originalTx.Network);

            // Checking that the PSBT of the receiver is clean
            if (newPSBT.GlobalXPubs.Any())
            {
                throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
            }

            if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
            }

            if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0)
            {
                throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
            }

            // Do not trust on inputs order because the payjoin server should shuffle them.
            foreach (var input in originalTx.Inputs)
            {
                var newInput = newPSBT.Inputs.FindIndexedInput(input.PrevOut);
                if (newInput is { })
Пример #2
0
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public BuildTransactionResult BuildTransaction(
            PaymentIntent payments,
            Func <FeeRate> feeRateFetcher,
            IEnumerable <TxoRef> allowedInputs = null,
            Func <LockTime> lockTimeSelector   = null)
        {
            payments = Guard.NotNull(nameof(payments), payments);
            lockTimeSelector ??= () => LockTime.Zero;

            long totalAmount = payments.TotalAmount.Satoshi;

            if (totalAmount < 0 || totalAmount > 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.Available();
            List <SmartCoin> allowedSmartCoinInputs = AllowUnconfirmed            // Inputs that can be used to build the transaction.
                                        ? availableCoinsView.ToList()
                                        : availableCoinsView.Confirmed().ToList();

            if (allowedInputs != 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.TransactionId == x.TransactionId && y.Index == 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.GetCoin()));

            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 = null;

            if (payments.TryGetCustomRequest(out DestinationRequest custChange))
            {
                var changeScript = custChange.Destination.ScriptPubKey;
                changeHdPubKey = KeyManager.GetKeyForScriptPubKey(changeScript);

                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).RandomElement();

                builder.SetChange(changeHdPubKey.P2wpkhScript);
            }

            builder.OptInRBF = new Random().NextDouble() < Constants.TransactionRBFSignalRate;

            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 != 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 totalSendAmount = totalSendAmountNoFee + fee;

            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 {totalOutgoingAmountNoFee:0.#}% of your transaction amount.{Environment.NewLine}"
                               + $"Sending:\t {totalSendAmount.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 twice the transaction 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.

            Transaction tx = null;

            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.SetLockTime(lockTimeSelector());
                builder.SignPSBT(psbt);
                psbt.Finalize();
                tx = psbt.ExtractTransaction();

                var checkResults = builder.Check(tx).ToList();
                if (!psbt.TryGetEstimatedFeeRate(out FeeRate actualFeeRate))
                {
                    throw new InvalidOperationException("Impossible to get the fee rate of the PSBT, this should never happen.");
                }

                // 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);
                }
            }

            if (KeyManager.MasterFingerprint is HDFingerprint fp)
            {
                foreach (var coin in spentCoins)
                {
                    var rootKeyPath = new RootedKeyPath(fp, coin.HdPubKey.FullKeyPath);
                    psbt.AddKeyPath(coin.HdPubKey.PubKey, rootKeyPath, coin.ScriptPubKey);
                }
            }

            var label = SmartLabel.Merge(payments.Requests.Select(x => x.Label).Concat(spentCoins.Select(x => x.Label)));
            var outerWalletOutputs = new List <SmartCoin>();
            var innerWalletOutputs = new List <SmartCoin>();

            for (var i = 0U; i < tx.Outputs.Count; i++)
            {
                TxOut output   = tx.Outputs[i];
                var   anonset  = tx.GetAnonymitySet(i) + spentCoins.Min(x => x.AnonymitySet) - 1;              // Minus 1, because count own only once.
                var   foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey);
                var   coin     = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Inputs.ToTxoRefs().ToArray(), Height.Unknown, tx.RBF, anonset, pubKey: foundKey);
                label = SmartLabel.Merge(label, coin.Label);                 // foundKey's label is already added to the coinlabel.

                if (foundKey is null)
                {
                    outerWalletOutputs.Add(coin);
                }
                else
                {
                    innerWalletOutputs.Add(coin);
                }
            }

            foreach (var coin in outerWalletOutputs.Concat(innerWalletOutputs))
            {
                var foundPaymentRequest = payments.Requests.FirstOrDefault(x => x.Destination.ScriptPubKey == coin.ScriptPubKey);

                // If change then we concatenate all the labels.
                if (foundPaymentRequest is null)                 // Then it's autochange.
                {
                    coin.Label = label;
                }
                else
                {
                    coin.Label = SmartLabel.Merge(coin.Label, foundPaymentRequest.Label);
                }

                var foundKey = KeyManager.GetKeyForScriptPubKey(coin.ScriptPubKey);
                foundKey?.SetLabel(coin.Label);                 // The foundkeylabel has already been added previously, so no need to concatenate.
            }

            Logger.LogInfo($"Transaction is successfully built: {tx.GetHash()}.");
            var sign = !KeyManager.IsWatchOnly;
            var spendsUnconfirmed = spentCoins.Any(c => !c.Confirmed);

            return(new BuildTransactionResult(new SmartTransaction(tx, Height.Unknown), psbt, spendsUnconfirmed, sign, fee, feePc, outerWalletOutputs, innerWalletOutputs, spentCoins));
        }
Пример #3
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            DerivationSchemeSettings derivationSchemeSettings = null;

            try
            {
                psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
                derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(nameof(WalletPSBTReady), vm));
            }

            switch (command)
            {
            case "payjoin":
                string error = null;
                try
                {
                    var proposedPayjoin = await GetPayjoinProposedTX(vm.SigningContext.PayJoinEndpointUrl, psbt,
                                                                     derivationSchemeSettings, network, cancellationToken);

                    try
                    {
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath), new SigningOptions()
                        {
                            EnforceLowR = !(vm.SigningContext?.EnforceLowR is false)
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        proposedPayjoin.Finalize();
                        var hash = proposedPayjoin.ExtractTransaction().GetHash();
                        _EventAggregator.Publish(new UpdateTransactionLabel()
                        {
                            WalletId          = walletId,
                            TransactionLabels = new Dictionary <uint256, List <(string color, string label)> >()
                            {
                                {
                                    hash,
                                    new List <(string color, string label)>
                                    {
                                        UpdateTransactionLabel.PayjoinLabelTemplate()
                                    }
                                }
                            }
                        });
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Success,
                            AllowDismiss = false,
                            Html         = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
                        });
                        return(await WalletPSBTReady(walletId, vm, "broadcast"));
                    }
Пример #4
0
        public async Task <IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
                                                       WalletId walletId, SignWithSeedViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                return(View(viewModel));
            }
            var network = NetworkProvider.GetNetwork(walletId.CryptoCode);

            if (network == null)
            {
                throw new FormatException("Invalid value for crypto code");
            }

            ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);

            if (extKey == null)
            {
                ModelState.AddModelError(nameof(viewModel.SeedOrKey),
                                         "Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv");
            }

            var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork);

            if (!psbt.IsReadyToSign())
            {
                ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed");
            }

            if (!ModelState.IsValid)
            {
                return(View(viewModel));
            }

            ExtKey signingKey         = null;
            var    settings           = (await GetDerivationSchemeSettings(walletId));
            var    signingKeySettings = settings.GetSigningAccountKeySettings();

            if (signingKeySettings.RootFingerprint is null)
            {
                signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
            }

            RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();

            // The user gave the root key, let's try to rebase the PSBT, and derive the account private key
            if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
            {
                psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
                signingKey = extKey.Derive(rootedKeyPath.KeyPath);
            }
            // The user maybe gave the account key, let's try to sign with it
            else
            {
                signingKey = extKey;
            }
            var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath);

            if (balanceChange == Money.Zero)
            {
                ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings.");
                return(View(viewModel));
            }
            psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath);
            ModelState.Remove(nameof(viewModel.PSBT));
            return(await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString()));
        }
Пример #5
0
        public async Task <IActionResult> VaultBridgeConnection(string cryptoCode = null,
                                                                [ModelBinder(typeof(WalletIdModelBinder))]
                                                                WalletId walletId = null)
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }
            cryptoCode = cryptoCode ?? walletId.CryptoCode;
            bool versionChecked = false;

            using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
            {
                var cancellationToken = cts.Token;
                var network           = Networks.GetNetwork <BTCPayNetwork>(cryptoCode);
                if (network == null)
                {
                    return(NotFound());
                }
                var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();

                var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
                {
                    Transport = new HwiWebSocketTransport(websocket)
                };
                Hwi.HwiDeviceClient device      = null;
                HwiEnumerateEntry   deviceEntry = null;
                HDFingerprint?      fingerprint = null;
                string password        = null;
                var    websocketHelper = new WebSocketHelper(websocket);

                async Task <bool> RequireDeviceUnlocking()
                {
                    if (deviceEntry == null)
                    {
                        await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);

                        return(true);
                    }
                    if (deviceEntry.Code is HwiErrorCode.DeviceNotInitialized)
                    {
                        await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken);

                        return(true);
                    }
                    if (deviceEntry.Code is HwiErrorCode.DeviceNotReady)
                    {
                        if (IsTrezorT(deviceEntry))
                        {
                            await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken);

                            return(true);
                        }
                        else if (deviceEntry.NeedsPinSent is true)
                        {
                            await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);

                            return(true);
                        }
                        else if (deviceEntry.NeedsPassphraseSent is true && password is null)
                        {
                            await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken);

                            return(true);
                        }
                    }
                    return(false);
                }

                JObject o = null;
                try
                {
                    while (true)
                    {
                        var command = await websocketHelper.NextMessageAsync(cancellationToken);

                        switch (command)
                        {
                        case "set-passphrase":
                            device.Password = await websocketHelper.NextMessageAsync(cancellationToken);

                            password = device.Password;
                            break;

                        case "ask-sign":
                            if (await RequireDeviceUnlocking())
                            {
                                continue;
                            }
                            if (walletId == null)
                            {
                                await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken);

                                continue;
                            }
                            if (fingerprint is null)
                            {
                                fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
                            }
                            await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);

                            o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
                            var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings);

                            if (!authorization.Succeeded)
                            {
                                await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);

                                continue;
                            }
                            var psbt = PSBT.Parse(o["psbt"].Value <string>(), network.NBitcoinNetwork);
                            var derivationSettings = GetDerivationSchemeSettings(walletId);
                            derivationSettings.RebaseKeyPaths(psbt);
                            var signing = derivationSettings.GetSigningAccountKeySettings();
                            if (signing.GetRootedKeyPath()?.MasterFingerprint != fingerprint)
                            {
                                await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken);

                                continue;
                            }
                            var signableInputs = psbt.Inputs
                                                 .SelectMany(i => i.HDKeyPaths)
                                                 .Where(i => i.Value.MasterFingerprint == fingerprint)
                                                 .ToArray();
                            if (signableInputs.Length > 0)
                            {
                                var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath)).GetPublicKey();
                                if (actualPubKey != signableInputs[0].Key)
                                {
                                    await websocketHelper.Send("{ \"error\": \"wrong-keypath\"}", cancellationToken);

                                    continue;
                                }
                            }
                            try
                            {
                                psbt = await device.SignPSBTAsync(psbt, cancellationToken);
                            }
                            catch (Hwi.HwiException)
                            {
                                await websocketHelper.Send("{ \"error\": \"user-reject\"}", cancellationToken);

                                continue;
                            }
                            o = new JObject();
                            o.Add("psbt", psbt.ToBase64());
                            await websocketHelper.Send(o.ToString(), cancellationToken);

                            break;

                        case "display-address":
                            if (await RequireDeviceUnlocking())
                            {
                                continue;
                            }
                            var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
                            await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken);

                            await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);

                            break;

                        case "ask-pin":
                            if (device == null)
                            {
                                await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);

                                continue;
                            }
                            try
                            {
                                await device.PromptPinAsync(cancellationToken);
                            }
                            catch (HwiException ex) when(ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked)
                            {
                                await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken);

                                continue;
                            }
                            await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken);

                            var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture);
                            if (await device.SendPinAsync(pin, cancellationToken))
                            {
                                goto askdevice;
                            }
                            else
                            {
                                await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken);

                                continue;
                            }

                        case "ask-xpub":
                            if (await RequireDeviceUnlocking())
                            {
                                continue;
                            }
                            await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);

                            var     askedXpub     = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
                            var     addressType   = askedXpub["addressType"].Value <string>();
                            var     accountNumber = askedXpub["accountNumber"].Value <int>();
                            JObject result        = new JObject();
                            var     factory       = network.NBXplorerNetwork.DerivationStrategyFactory;
                            if (fingerprint is null)
                            {
                                fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
                            }
                            result["fingerprint"] = fingerprint.Value.ToString();

                            DerivationStrategyBase strategy = null;
                            KeyPath          keyPath        = null;
                            BitcoinExtPubKey xpub           = null;

                            if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy")
                            {
                                await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken);

                                continue;
                            }

                            if (!network.NBitcoinNetwork.Consensus.SupportTaproot && addressType == "taproot")
                            {
                                await websocketHelper.Send("{ \"error\": \"taproot-notsupported\"}", cancellationToken);

                                continue;
                            }
                            if (addressType == "taproot")
                            {
                                keyPath = new KeyPath("86'").Derive(network.CoinType).Derive(accountNumber, true);
                                xpub    = await device.GetXPubAsync(keyPath);

                                strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
                                {
                                    ScriptPubKeyType = ScriptPubKeyType.TaprootBIP86
                                });
                            }
                            else if (addressType == "segwit")
                            {
                                keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(accountNumber, true);
                                xpub    = await device.GetXPubAsync(keyPath);

                                strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
                                {
                                    ScriptPubKeyType = ScriptPubKeyType.Segwit
                                });
                            }
                            else if (addressType == "segwitWrapped")
                            {
                                keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(accountNumber, true);
                                xpub    = await device.GetXPubAsync(keyPath);

                                strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
                                {
                                    ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
                                });
                            }
                            else if (addressType == "legacy")
                            {
                                keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(accountNumber, true);
                                xpub    = await device.GetXPubAsync(keyPath);

                                strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
                                {
                                    ScriptPubKeyType = ScriptPubKeyType.Legacy
                                });
                            }
                            else
                            {
                                await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken);

                                continue;
                            }
                            result.Add(new JProperty("strategy", strategy.ToString()));
                            result.Add(new JProperty("accountKey", xpub.ToString()));
                            result.Add(new JProperty("keyPath", keyPath.ToString()));
                            await websocketHelper.Send(result.ToString(), cancellationToken);

                            break;

                        case "ask-passphrase":
                            if (command == "ask-passphrase")
                            {
                                if (deviceEntry == null)
                                {
                                    await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);

                                    continue;
                                }
                                // The make the trezor T ask for password
                                await device.GetXPubAsync(new KeyPath("44'"), cancellationToken);
                            }
                            goto askdevice;

                        case "ask-device":
askdevice:
                            if (!versionChecked)
                            {
                                var version = await hwi.GetVersionAsync(cancellationToken);

                                if (version.Major < 2)
                                {
                                    await websocketHelper.Send("{ \"error\": \"vault-outdated\"}", cancellationToken);

                                    continue;
                                }
                                versionChecked = true;
                            }
                            password    = null;
                            deviceEntry = null;
                            device      = null;
                            var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList();
                            deviceEntry = entries.FirstOrDefault();
                            if (deviceEntry == null)
                            {
                                await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken);

                                continue;
                            }
                            device      = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
                            fingerprint = device.Fingerprint;
                            JObject json = new JObject();
                            json.Add("model", device.Model);
                            json.Add("fingerprint", device.Fingerprint?.ToString());
                            await websocketHelper.Send(json.ToString(), cancellationToken);

                            break;
                        }
                    }
                }
                catch (FormatException ex)
                {
                    JObject obj = new JObject();
                    obj.Add("error", "invalid-network");
                    obj.Add("details", ex.ToString());
                    try
                    {
                        await websocketHelper.Send(obj.ToString(), cancellationToken);
                    }
                    catch { }
                }
                catch (Exception ex)
                {
                    JObject obj = new JObject();
                    obj.Add("error", "unknown-error");
                    obj.Add("message", ex.Message);
                    obj.Add("details", ex.ToString());
                    try
                    {
                        await websocketHelper.Send(obj.ToString(), cancellationToken);
                    }
                    catch { }
                }
                finally
                {
                    await websocketHelper.DisposeAsync(cancellationToken);
                }
            }
            return(new EmptyResult());
        }
Пример #6
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            DerivationSchemeSettings derivationSchemeSettings = null;

            try
            {
                psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
                derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(nameof(WalletPSBTReady), vm));
            }

            switch (command)
            {
            case "payjoin":
                string error = null;
                try
                {
                    var proposedPayjoin = await GetPayjoinProposedTX(new BitcoinUrlBuilder(vm.SigningContext.PayJoinBIP21, network.NBitcoinNetwork), psbt,
                                                                     derivationSchemeSettings, network, cancellationToken);

                    try
                    {
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath), new SigningOptions()
                        {
                            EnforceLowR = !(vm.SigningContext?.EnforceLowR is false)
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        proposedPayjoin.Finalize();
                        var hash = proposedPayjoin.ExtractTransaction().GetHash();
                        _EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Success,
                            AllowDismiss = false,
                            Html         = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
                        });
                        return(await WalletPSBTReady(walletId, vm, "broadcast"));
                    }
                    catch (Exception)
                    {
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Warning,
                            AllowDismiss = false,
                            Html         =
                                $"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
                                $"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
                                $"If you cancel or refuse to sign this transaction, the payment will proceed without payjoin"
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        return(ViewVault(walletId, vm.SigningContext));
                    }
                }
Пример #7
0
 public OnChainPaymentMethodDataPreview(string cryptoCode, string derivationScheme, string label, RootedKeyPath accountKeyPath)
 {
     Label            = label;
     AccountKeyPath   = accountKeyPath;
     CryptoCode       = cryptoCode;
     DerivationScheme = derivationScheme;
 }
Пример #8
0
 public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath)
 {
     Enabled          = enabled;
     Label            = label;
     AccountKeyPath   = accountKeyPath;
     CryptoCode       = cryptoCode;
     DerivationScheme = derivationScheme;
 }
Пример #9
0
        public void CanSerializeInJson()
        {
            Key k = new Key();

            CanSerializeInJsonCore(DateTimeOffset.UtcNow);
            CanSerializeInJsonCore(new byte[] { 1, 2, 3 });
            CanSerializeInJsonCore(k);
            CanSerializeInJsonCore(Money.Coins(5.0m));
            CanSerializeInJsonCore(k.PubKey.GetAddress(Network.Main));
            CanSerializeInJsonCore(new KeyPath("1/2"));
            CanSerializeInJsonCore(RootedKeyPath.Parse("7b09d780/0'/0'/2'"));
            CanSerializeInJsonCore(Network.Main);
            CanSerializeInJsonCore(new uint256(RandomUtils.GetBytes(32)));
            CanSerializeInJsonCore(new uint160(RandomUtils.GetBytes(20)));
            CanSerializeInJsonCore(new AssetId(k.PubKey));
            CanSerializeInJsonCore(k.PubKey.ScriptPubKey);
            CanSerializeInJsonCore(new Key().PubKey.WitHash.GetAddress(Network.Main));
            CanSerializeInJsonCore(new Key().PubKey.WitHash.ScriptPubKey.GetWitScriptAddress(Network.Main));
            var sig = k.Sign(new uint256(RandomUtils.GetBytes(32)));

            CanSerializeInJsonCore(sig);
            CanSerializeInJsonCore(new TransactionSignature(sig, SigHash.All));
            CanSerializeInJsonCore(k.PubKey.Hash);
            CanSerializeInJsonCore(k.PubKey.ScriptPubKey.Hash);
            CanSerializeInJsonCore(k.PubKey.WitHash);
            CanSerializeInJsonCore(k);
            CanSerializeInJsonCore(k.PubKey);
            CanSerializeInJsonCore(new WitScript(new Script(Op.GetPushOp(sig.ToDER()), Op.GetPushOp(sig.ToDER()))));
            CanSerializeInJsonCore(new LockTime(1));
            CanSerializeInJsonCore(new LockTime(130), out var str);
            Assert.Equal("130", str);
            CanSerializeInJsonCore(new LockTime(DateTime.UtcNow));
            CanSerializeInJsonCore(new FeeRate(Money.Satoshis(1), 1000));
            CanSerializeInJsonCore(new FeeRate(Money.Satoshis(1000), 1000));
            CanSerializeInJsonCore(new FeeRate(0.5m));
            CanSerializeInJsonCore(new HDFingerprint(0x0a), out str);
            Assert.Equal("\"0a000000\"", str);
            var print  = Serializer.ToObject <HDFingerprint>("\"0a000000\"");
            var print2 = Serializer.ToObject <HDFingerprint>("10");

            Assert.Equal(print, print2);

            var printn  = Serializer.ToObject <HDFingerprint?>("\"0a000000\"");
            var print2n = Serializer.ToObject <HDFingerprint?>("10");

            Assert.Equal(printn, print2n);
            Assert.Null(Serializer.ToObject <HDFingerprint?>(""));

            var psbt     = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", Network.Main);
            var psbtJson = Serializer.ToString(psbt, Network.Main);
            var psbt2    = Serializer.ToObject <PSBT>(psbtJson, Network.Main);

            Assert.Equal(psbt, psbt2);


            var expectedOutpoint = OutPoint.Parse("44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000");
            var actualOutpoint   = Serializer.ToObject <OutPoint>("\"44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000\"", Network.Main);

            Assert.Equal(expectedOutpoint, actualOutpoint);
            actualOutpoint = Serializer.ToObject <OutPoint>("\"7c02d7d6923ab5e9bbdadf7cf6873a5454ae5aa86d15308ed8d68840a79cf644-1\"", Network.Main);
            Assert.Equal(expectedOutpoint, actualOutpoint);

            CanSerializeInJsonCore(expectedOutpoint, out str);
            Assert.Equal("\"44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000\"", str);
        }
        public void CanSignPSBTWithRootAndAccountKey()
        {
            using (var nodeBuilder = NodeBuilderEx.Create())
            {
                var rpc = nodeBuilder.CreateNode().CreateRPCClient();
                nodeBuilder.StartAll();
                rpc.Generate(102);
                uint hardenedFlag = 0U;
retry:
                var masterKey = new ExtKey();
                var accountKeyPath = new RootedKeyPath(masterKey, new KeyPath("49'/0'/0'"));
                var accountKey     = masterKey.Derive(accountKeyPath);
                var addresses      = Enumerable.Range(0, 5)
                                     .Select(i =>
                {
                    var addressPath     = new KeyPath(new uint[] { 0U | hardenedFlag, (uint)i | hardenedFlag });
                    var fullAddressPath = accountKeyPath.Derive(addressPath);
                    var address         = accountKey.Derive(addressPath).GetPublicKey().WitHash.GetAddress(nodeBuilder.Network);
                    return(new
                    {
                        FullAddressPath = fullAddressPath,
                        AddressPath = addressPath,
                        Address = address
                    });
                }).ToList();

                var changeAddress = addresses.Last();
                addresses = addresses.Take(addresses.Count - 1).ToList();


                // Fund the addresses
                var coins = addresses.Select(async kra =>
                {
                    var id = await rpc.SendToAddressAsync(kra.Address, Money.Coins(1));
                    var tx = await rpc.GetRawTransactionAsync(id);
                    return(tx.Outputs.AsCoins().Where(o => o.ScriptPubKey == kra.Address.ScriptPubKey).Single());
                }).Select(t => t.Result).ToArray();


                var destination = new Key().ScriptPubKey;
                var amount      = new Money(1, MoneyUnit.BTC);

                var builder = Network.Main.CreateTransactionBuilder();

                var fee = new Money(100_000L);
                var partiallySignedTx = builder
                                        .AddCoins(coins)
                                        .Send(destination, amount)
                                        .SetChange(changeAddress.Address)
                                        .SendFees(fee)
                                        .BuildPSBT(false);
                partiallySignedTx.AddKeyPath(masterKey, addresses.Concat(new[] { changeAddress }).Select(a => a.FullAddressPath.KeyPath).ToArray());
                var expectedBalance = -amount - fee;
                var actualBalance   = partiallySignedTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, accountKeyPath);
                Assert.Equal(expectedBalance, actualBalance);

                actualBalance = partiallySignedTx.GetBalance(ScriptPubKeyType.Segwit, masterKey);
                Assert.Equal(expectedBalance, actualBalance);
                Assert.Equal(Money.Zero, partiallySignedTx.GetBalance(ScriptPubKeyType.Legacy, masterKey));
                // You can sign with accountKey and keypath
                var memento = partiallySignedTx.Clone();

                partiallySignedTx.SignAll(ScriptPubKeyType.Segwit, accountKey, accountKeyPath);
                Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 1));
                partiallySignedTx.Finalize();

                var partiallySignedTx2 = memento;

                // Or you can sign with the masterKey
                partiallySignedTx2.SignAll(ScriptPubKeyType.Segwit, masterKey);
                Assert.True(partiallySignedTx2.Inputs.All(i => i.PartialSigs.Count == 1));
                partiallySignedTx2.Finalize();

                Assert.Equal(partiallySignedTx, partiallySignedTx2);

                var signedTx = partiallySignedTx.ExtractTransaction();
                rpc.SendRawTransaction(signedTx);
                var errors = builder.Check(signedTx);
                Assert.Empty(errors);

                if (hardenedFlag == 0)
                {
                    hardenedFlag = 0x80000000;
                    goto retry;
                }
            }
        }
Пример #11
0
        public async Task <Key> GetKey(RootedKeyPath keyPath)
        {
            var keyset = await OpenKeyset(keyPath.MasterFingerprint);

            return(keyset.GetKey(keyPath));
        }
Пример #12
0
 public abstract Task <PSBT> SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken);
Пример #13
0
        private async Task FetchTransactionDetails(WalletId walletId, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
        {
            var           psbtObject     = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
            IHDKey        signingKey     = null;
            RootedKeyPath signingKeyPath = null;

            try
            {
                signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }
            try
            {
                signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }

            try
            {
                signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath);
            }
            catch { }

            var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId);

            if (signingKey == null || signingKeyPath == null)
            {
                var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
                if (signingKey == null)
                {
                    signingKey    = signingKeySettings.AccountKey;
                    vm.SigningKey = signingKey.ToString();
                }
                if (vm.SigningKeyPath == null)
                {
                    signingKeyPath    = signingKeySettings.GetRootedKeyPath();
                    vm.SigningKeyPath = signingKeyPath?.ToString();
                }
            }

            if (psbtObject.IsAllFinalized())
            {
                vm.CanCalculateBalance = false;
            }
            else
            {
                var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath);
                vm.BalanceChange       = ValueToString(balanceChange, network);
                vm.CanCalculateBalance = true;
                vm.Positive            = balanceChange >= Money.Zero;
            }

            foreach (var output in psbtObject.Outputs)
            {
                var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
                vm.Destinations.Add(dest);
                var mine           = output.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
                var balanceChange2 = output.Value;
                if (!mine)
                {
                    balanceChange2 = -balanceChange2;
                }
                dest.Balance     = ValueToString(balanceChange2, network);
                dest.Positive    = balanceChange2 >= Money.Zero;
                dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
            }

            if (psbtObject.TryGetFee(out var fee))
            {
                vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel()
                {
                    Positive    = false,
                    Balance     = ValueToString(-fee, network),
                    Destination = "Mining fees"
                });
            }
            if (psbtObject.TryGetEstimatedFeeRate(out var feeRate))
            {
                vm.FeeRate = feeRate.ToString();
            }

            if (!psbtObject.IsAllFinalized() && !psbtObject.TryFinalize(out var errors))
            {
                vm.SetErrors(errors);
            }
        }
Пример #14
0
        private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
        {
            var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);

            if (!psbtObject.IsAllFinalized())
            {
                psbtObject = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbtObject) ?? psbtObject;
            }
            IHDKey        signingKey     = null;
            RootedKeyPath signingKeyPath = null;

            try
            {
                signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }
            try
            {
                signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }

            try
            {
                signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath);
            }
            catch { }

            if (signingKey == null || signingKeyPath == null)
            {
                var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
                if (signingKey == null)
                {
                    signingKey    = signingKeySettings.AccountKey;
                    vm.SigningKey = signingKey.ToString();
                }
                if (vm.SigningKeyPath == null)
                {
                    signingKeyPath    = signingKeySettings.GetRootedKeyPath();
                    vm.SigningKeyPath = signingKeyPath?.ToString();
                }
            }

            if (psbtObject.IsAllFinalized())
            {
                vm.CanCalculateBalance = false;
            }
            else
            {
                var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath);
                vm.BalanceChange       = ValueToString(balanceChange, network);
                vm.CanCalculateBalance = true;
                vm.Positive            = balanceChange >= Money.Zero;
            }
            vm.Inputs = new List <WalletPSBTReadyViewModel.InputViewModel>();
            foreach (var input in psbtObject.Inputs)
            {
                var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
                vm.Inputs.Add(inputVm);
                var mine           = input.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
                var balanceChange2 = input.GetTxOut()?.Value ?? Money.Zero;
                if (mine)
                {
                    balanceChange2 = -balanceChange2;
                }
                inputVm.BalanceChange = ValueToString(balanceChange2, network);
                inputVm.Positive      = balanceChange2 >= Money.Zero;
                inputVm.Index         = (int)input.Index;
            }
            vm.Destinations = new List <WalletPSBTReadyViewModel.DestinationViewModel>();
            foreach (var output in psbtObject.Outputs)
            {
                var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
                vm.Destinations.Add(dest);
                var mine           = output.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
                var balanceChange2 = output.Value;
                if (!mine)
                {
                    balanceChange2 = -balanceChange2;
                }
                dest.Balance     = ValueToString(balanceChange2, network);
                dest.Positive    = balanceChange2 >= Money.Zero;
                dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
            }

            if (psbtObject.TryGetFee(out var fee))
            {
                vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel()
                {
                    Positive    = false,
                    Balance     = ValueToString(-fee, network),
                    Destination = "Mining fees"
                });
            }
            if (psbtObject.TryGetEstimatedFeeRate(out var feeRate))
            {
                vm.FeeRate = feeRate.ToString();
            }

            var sanityErrors = psbtObject.CheckSanity();

            if (sanityErrors.Count != 0)
            {
                vm.SetErrors(sanityErrors);
            }
            else if (!psbtObject.IsAllFinalized() && !psbtObject.TryFinalize(out var errors))
            {
                vm.SetErrors(errors);
            }
        }
Пример #15
0
 public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath) :
     base(cryptoCode, derivationScheme, label, accountKeyPath)
 {
     Enabled = enabled;
 }
Пример #16
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath, vm.OriginalPSBT, vm.PayJoinEndpointUrl));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            DerivationSchemeSettings derivationSchemeSettings = null;

            try
            {
                psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
                derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(nameof(WalletPSBTReady), vm));
            }

            switch (command)
            {
            case "payjoin":
                string error = null;
                try
                {
                    var proposedPayjoin = await GetPayjoinProposedTX(vm.PayJoinEndpointUrl, psbt,
                                                                     derivationSchemeSettings, network, cancellationToken);

                    try
                    {
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath));
                        vm.PSBT         = proposedPayjoin.ToBase64();
                        vm.OriginalPSBT = psbt.ToBase64();
                        proposedPayjoin.Finalize();
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Success,
                            AllowDismiss = false,
                            Html         = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
                        });
                        return(await WalletPSBTReady(walletId, vm, "broadcast"));
                    }
                    catch (Exception)
                    {
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Warning,
                            AllowDismiss = false,
                            Html         =
                                $"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
                                $"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
                                $"If you cancel refuse to sign this transaction, the payment will proceed without payjoin"
                        });
                        return(ViewVault(walletId, proposedPayjoin, vm.PayJoinEndpointUrl, psbt));
                    }
                }
                catch (PayjoinReceiverException ex)
                {
                    error = $"The payjoin receiver could not complete the payjoin: {ex.Message}";
                }
                catch (PayjoinSenderException ex)
                {
                    error = $"We rejected the receiver's payjoin proposal: {ex.Message}";
                }
                catch (Exception ex)
                {
                    error = $"Unexpected payjoin error: {ex.Message}";
                }

                //we possibly exposed the tx to the receiver, so we need to broadcast straight away
                psbt.Finalize();
                TempData.SetStatusMessageModel(new StatusMessageModel()
                {
                    Severity     = StatusMessageModel.StatusSeverity.Warning,
                    AllowDismiss = false,
                    Html         = $"The payjoin transaction could not be created.<br/>" +
                                   $"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" +
                                   $"{error}"
                });
                return(await WalletPSBTReady(walletId, vm, "broadcast"));

            case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
                vm.SetErrors(errors);
                return(View(nameof(WalletPSBTReady), vm));

            case "broadcast":
            {
                var transaction = psbt.ExtractTransaction();
                try
                {
                    var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                    if (!broadcastResult.Success)
                    {
                        if (!string.IsNullOrEmpty(vm.OriginalPSBT))
                        {
                            TempData.SetStatusMessageModel(new StatusMessageModel()
                                {
                                    Severity     = StatusMessageModel.StatusSeverity.Warning,
                                    AllowDismiss = false,
                                    Html         = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast."
                                });
                            vm.PSBT         = vm.OriginalPSBT;
                            vm.OriginalPSBT = null;
                            return(await WalletPSBTReady(walletId, vm, "broadcast"));
                        }

                        vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
                        return(View(nameof(WalletPSBTReady), vm));
                    }
                }
                catch (Exception ex)
                {
                    vm.GlobalError = "Error while broadcasting: " + ex.Message;
                    return(View(nameof(WalletPSBTReady), vm));
                }

                if (!TempData.HasStatusMessage())
                {
                    TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
                }
                return(RedirectToWalletTransaction(walletId, transaction));
            }

            case "analyze-psbt":
                return(RedirectToWalletPSBT(psbt));

            default:
                vm.GlobalError = "Unknown command";
                return(View(nameof(WalletPSBTReady), vm));
            }
        }
Пример #17
0
 public OnChainPaymentMethodDataWithSensitiveData(string cryptoCode, string derivationScheme, bool enabled,
                                                  string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic) : base(cryptoCode, derivationScheme, enabled,
                                                                                                                        label, accountKeyPath)
 {
     Mnemonic = mnemonic;
 }