public void CanHandleEmptyKeyPath()
        {
            var rKeyPath = RootedKeyPath.Parse("01234567");

            Assert.Equal(rKeyPath.KeyPath, KeyPath.Empty);
            Assert.Equal("", KeyPath.Empty.ToString());
            Assert.Equal(new byte[0], KeyPath.Empty.ToBytes());
            Assert.Equal("01234567", rKeyPath.ToStringWithEmptyKeyPathAware());
        }
示例#2
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
        {
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            PSBT psbt    = await vm.GetPSBT(network.NBitcoinNetwork);

            if (vm.InvalidPSBT || psbt is null)
            {
                if (vm.InvalidPSBT)
                {
                    vm.GlobalError = "Invalid PSBT";
                }
                return(View(nameof(WalletPSBT), vm));
            }
            DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings(walletId);

            if (derivationSchemeSettings == null)
            {
                return(NotFound());
            }

            await FetchTransactionDetails(derivationSchemeSettings, vm, network);

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

                    try
                    {
                        proposedPayjoin.Settings.SigningOptions = new SigningOptions
                        {
                            EnforceLowR = !(vm.SigningContext?.EnforceLowR is false)
                        };
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath));
                        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));
                    }
                }
                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(WalletPSBT), vm));

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

                    if (!broadcastResult.Success)
                    {
                        if (!string.IsNullOrEmpty(vm.SigningContext.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.SigningContext.PSBT         = vm.SigningContext.OriginalPSBT;
                            vm.SigningContext.OriginalPSBT = null;
                            return(await WalletPSBTReady(walletId, vm, "broadcast"));
                        }

                        vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
                        return(View(nameof(WalletPSBT), vm));
                    }
                    else
                    {
                        var wallet             = _walletProvider.GetWallet(network);
                        var derivationSettings = GetDerivationSchemeSettings(walletId);
                        wallet.InvalidateCache(derivationSettings.AccountDerivation);
                    }
                }
                catch (Exception ex)
                {
                    vm.GlobalError = "Error while broadcasting: " + ex.Message;
                    return(View(nameof(WalletPSBT), vm));
                }

                if (!TempData.HasStatusMessage())
                {
                    TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
                }
                var returnUrl = this.HttpContext.Request.Query["returnUrl"].FirstOrDefault();
                if (returnUrl is not null)
                {
                    return(Redirect(returnUrl));
                }
                return(RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() }));
            }

            case "analyze-psbt":
                return(RedirectToWalletPSBT(new WalletPSBTViewModel()
                {
                    PSBT = psbt.ToBase64()
                }));

            case "decode":
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);

                return(View("WalletPSBTDecoded", vm));

            default:
                vm.GlobalError = "Unknown command";
                return(View(nameof(WalletPSBT), vm));
            }
        }
示例#3
0
        private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
        {
            var psbtObject = PSBT.Parse(vm.SigningContext.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);
            }
        }
        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(ScriptPubKeyType.Legacy, Network.Main), 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), Network.Main);
            CanSerializeInJsonCore(new Key().PubKey.WitHash.ScriptPubKey.WitHash.GetAddress(Network.Main), 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 WitScript(OpcodeType.OP_0, OpcodeType.OP_0));
            CanSerializeInJsonCore(new LockTime(1));
            CanSerializeInJsonCore(new LockTime(130), out var str);
            Assert.Equal("130", str);
            var chain = CanSerializeInJsonCore(new ChainName("lol"), out str);

            Assert.Equal("Lol", chain.ToString());
            Assert.Equal("\"Lol\"", str);
            CanSerializeInJsonCore(new LockTime(DateTime.UtcNow));
            CanSerializeInJsonCore(new Sequence(1));
            CanSerializeInJsonCore(new Sequence?(1));
            CanSerializeInJsonCore(new Sequence?());
            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 feerate = Serializer.ToObject <FeeRate>("1");

            Assert.Equal(new FeeRate(Money.Satoshis(1.0m), 1), feerate);
            feerate = Serializer.ToObject <FeeRate>("1.0");
            Assert.Equal("1 Sat/B", feerate.ToString());
            Assert.Equal(new FeeRate(Money.Satoshis(1.0m), 1), feerate);
            feerate = Serializer.ToObject <FeeRate>("0.001");
            Assert.Equal("0.001 Sat/B", feerate.ToString());
            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);

            var key         = new Key(Encoders.Hex.DecodeData("ce71d1851c03cc6c0331020391113acbf6843b32065e53e4308984537630eee1"));
            var pubkey      = new PubKey(Encoders.Hex.DecodeData("02eae22800451728c177244a79be8ff22e92d08ec3a5cdd0b6d4b54fa7a90bb44c"));
            var internalKey = new TaprootInternalPubKey(Encoders.Hex.DecodeData("eae22800451728c177244a79be8ff22e92d08ec3a5cdd0b6d4b54fa7a90bb44c"));
            var outputKey   = new TaprootPubKey(Encoders.Hex.DecodeData("6bf657f19f5917eb6197ae123caf435611a1d35ba23a2d3394e579208d0f18d4"));

            CanSerializeInJsonCore(key, out str);
            CanSerializeInJsonCore(internalKey, out str);
            CanSerializeInJsonCore(outputKey, out str);
            CanSerializeInJsonCore(key.PubKey, out str);

            Assert.Throws <JsonObjectException>(() =>
            {
                Serializer.ToObject <OutPoint>("1");
            });
        }
示例#5
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));
                    }
                }
        public async Task <IActionResult> VaultBridgeConnection(string cryptoCode = null,
                                                                [ModelBinder(typeof(WalletIdModelBinder))]
                                                                WalletId walletId = null)
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }
            cryptoCode = cryptoCode ?? walletId.CryptoCode;
            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.Key);

                            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 (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:
                            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.ToString());
                            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());
        }
示例#7
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));
            }
        }
示例#8
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);
            }
        }
示例#9
0
        public async Task CanBuildTaprootSingleSigTransactions()
        {
            using (var nodeBuilder = NodeBuilderEx.Create())
            {
                var rpc = nodeBuilder.CreateNode().CreateRPCClient();
                nodeBuilder.StartAll();
                rpc.Generate(102);
                var                change             = new Key();
                var                rootKey            = new ExtKey();
                var                accountKeyPath     = new KeyPath("86'/0'/0'");
                var                accountRootKeyPath = new RootedKeyPath(rootKey.GetPublicKey().GetHDFingerPrint(), accountKeyPath);
                var                accountKey         = rootKey.Derive(accountKeyPath);
                var                key         = accountKey.Derive(new KeyPath("0/0")).PrivateKey;
                var                address     = key.PubKey.GetAddress(ScriptPubKeyType.TaprootBIP86, nodeBuilder.Network);
                var                destination = new Key();
                var                amount      = new Money(1, MoneyUnit.BTC);
                uint256            id          = null;
                Transaction        tx          = null;
                ICoin              coin        = null;
                TransactionBuilder builder     = null;
                var                rate        = new FeeRate(Money.Satoshis(1), 1);

                async Task RefreshCoin()
                {
                    id = await rpc.SendToAddressAsync(address, Money.Coins(1));

                    tx = await rpc.GetRawTransactionAsync(id);

                    coin    = tx.Outputs.AsCoins().Where(o => o.ScriptPubKey == address.ScriptPubKey).Single();
                    builder = Network.Main.CreateTransactionBuilder(0);
                }

                await RefreshCoin();

                var signedTx = builder
                               .AddCoins(coin)
                               .AddKeys(key)
                               .Send(destination, amount)
                               .SubtractFees()
                               .SetChange(change)
                               .SendEstimatedFees(rate)
                               .BuildTransaction(true);
                rpc.SendRawTransaction(signedTx);

                await RefreshCoin();

                // Let's try again, but this time with PSBT
                var psbt = builder
                           .AddCoins(coin)
                           .Send(destination, amount)
                           .SubtractFees()
                           .SetChange(change)
                           .SendEstimatedFees(rate)
                           .BuildPSBT(false);

                var tk = key.PubKey.GetTaprootFullPubKey();
                psbt.Inputs[0].HDTaprootKeyPaths.Add(tk.OutputKey, new TaprootKeyPath(accountRootKeyPath.Derive(KeyPath.Parse("0/0"))));
                psbt.SignAll(ScriptPubKeyType.TaprootBIP86, accountKey, accountRootKeyPath);

                // Check if we can roundtrip
                psbt = CanRoundtripPSBT(psbt);

                psbt.Finalize();
                rpc.SendRawTransaction(psbt.ExtractTransaction());

                // Let's try again, but this time with BuildPSBT(true)
                await RefreshCoin();

                psbt = builder
                       .AddCoins(coin)
                       .AddKeys(key)
                       .Send(destination, amount)
                       .SubtractFees()
                       .SetChange(change)
                       .SendEstimatedFees(rate)
                       .BuildPSBT(true);
                psbt.Finalize();
                rpc.SendRawTransaction(psbt.ExtractTransaction());

                // Let's try again, this time with a merkle root
                var merkleRoot = RandomUtils.GetUInt256();
                address = key.PubKey.GetTaprootFullPubKey(merkleRoot).GetAddress(nodeBuilder.Network);

                await RefreshCoin();

                psbt = builder
                       .AddCoins(coin)
                       .AddKeys(key.CreateTaprootKeyPair(merkleRoot))
                       .Send(destination, amount)
                       .SubtractFees()
                       .SetChange(change)
                       .SendEstimatedFees(rate)
                       .BuildPSBT(true);
                Assert.NotNull(psbt.Inputs[0].TaprootMerkleRoot);
                Assert.NotNull(psbt.Inputs[0].TaprootInternalKey);
                Assert.NotNull(psbt.Inputs[0].TaprootKeySignature);
                psbt = CanRoundtripPSBT(psbt);
                psbt.Finalize();
                rpc.SendRawTransaction(psbt.ExtractTransaction());

                // Can we sign the PSBT separately?
                await RefreshCoin();

                psbt = builder
                       .AddCoins(coin)
                       .Send(destination, amount)
                       .SubtractFees()
                       .SetChange(change)
                       .SendEstimatedFees(rate)
                       .BuildPSBT(false);

                var taprootKeyPair = key.CreateTaprootKeyPair(merkleRoot);
                psbt.Inputs[0].Sign(taprootKeyPair);
                Assert.NotNull(psbt.Inputs[0].TaprootMerkleRoot);
                Assert.NotNull(psbt.Inputs[0].TaprootInternalKey);
                Assert.NotNull(psbt.Inputs[0].TaprootKeySignature);

                // This line is useless, we just use it to test the PSBT roundtrip
                psbt.Inputs[0].HDTaprootKeyPaths.Add(taprootKeyPair.PubKey,
                                                     new TaprootKeyPath(RootedKeyPath.Parse("12345678/86'/0'/0'/0/0"),
                                                                        new uint256[] { RandomUtils.GetUInt256() }));
                psbt = CanRoundtripPSBT(psbt);
                psbt.Finalize();
                rpc.SendRawTransaction(psbt.ExtractTransaction());

                // Can we sign the transaction separately?
                await RefreshCoin();

                var coin1 = coin;
                await RefreshCoin();

                var coin2 = coin;
                builder  = Network.Main.CreateTransactionBuilder(0);
                signedTx = builder
                           .AddCoins(coin1, coin2)
                           .Send(destination, amount)
                           .SubtractFees()
                           .SetChange(change)
                           .SendEstimatedFees(rate)
                           .BuildTransaction(false);
                var unsignedTx = signedTx.Clone();
                builder = Network.Main.CreateTransactionBuilder(0);
                builder.AddKeys(key.CreateTaprootKeyPair(merkleRoot));
                builder.AddCoins(coin1);
                var ex = Assert.Throws <InvalidOperationException>(() => builder.SignTransactionInPlace(signedTx));
                Assert.Contains("taproot", ex.Message);
                builder.AddCoin(coin2);
                builder.SignTransactionInPlace(signedTx);
                Assert.True(!WitScript.IsNullOrEmpty(signedTx.Inputs.FindIndexedInput(coin2.Outpoint).WitScript));
                // Another solution is to set the precomputed transaction data.
                signedTx = unsignedTx;
                builder  = Network.Main.CreateTransactionBuilder(0);
                builder.AddKeys(key.CreateTaprootKeyPair(merkleRoot));
                builder.AddCoins(coin2);
                builder.SetSigningOptions(new SigningOptions()
                {
                    PrecomputedTransactionData = signedTx.PrecomputeTransactionData(new ICoin[] { coin1, coin2 })
                });
                builder.SignTransactionInPlace(signedTx);
                Assert.True(!WitScript.IsNullOrEmpty(signedTx.Inputs.FindIndexedInput(coin2.Outpoint).WitScript));


                // Let's check if we estimate precisely the size of a taproot transaction.
                await RefreshCoin();

                signedTx = builder
                           .AddCoins(coin)
                           .AddKeys(key.CreateTaprootKeyPair(merkleRoot))
                           .Send(destination, amount)
                           .SubtractFees()
                           .SetChange(change)
                           .SendEstimatedFees(rate)
                           .BuildTransaction(false);
                var actualvsize = builder.EstimateSize(signedTx, true);
                builder.SignTransactionInPlace(signedTx);
                var expectedvsize = signedTx.GetVirtualSize();
                // The estimator can't assume the sighash to be default
                // for all inputs, so we likely overestimate 1 bytes per input
                Assert.Equal(expectedvsize, actualvsize - 1);
            }
        }
示例#10
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"));
                    }