Example #1
0
        public void CanBuildSegwitP2SHMultisigTransactionsWithPSBT()
        {
            using (var nodeBuilder = NodeBuilderEx.Create())
            {
                var rpc = nodeBuilder.CreateNode().CreateRPCClient();
                nodeBuilder.StartAll();
                rpc.Generate(102);

                // Build the keys and addresses
                var masterKeys = Enumerable.Range(0, 3).Select(_ => new ExtKey()).ToArray();
                var keys       = masterKeys.Select(mk => mk.Derive(0, false).PrivateKey).ToArray();
                var redeem     = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(keys.Length, keys.Select(k => k.PubKey).ToArray());
                var address    = redeem.WitHash.ScriptPubKey.Hash.ScriptPubKey.GetDestinationAddress(nodeBuilder.Network);

                var id         = rpc.SendToAddress(address, Money.Coins(1));
                var tx         = rpc.GetRawTransaction(id);
                var scriptCoin = tx.Outputs.AsCoins()
                                 .Where(o => o.ScriptPubKey == address.ScriptPubKey)
                                 .Select(o => o.ToScriptCoin(redeem))
                                 .Single();


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


                var builder           = Network.Main.CreateTransactionBuilder();
                var rate              = new FeeRate(Money.Satoshis(1), 1);
                var partiallySignedTx = builder
                                        .AddCoins(scriptCoin)
                                        .AddKeys(keys[0])
                                        .Send(destination, amount)
                                        .SubtractFees()
                                        .SetChange(new Key())
                                        .SendEstimatedFees(rate)
                                        .BuildPSBT(true);
                Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 1));

                partiallySignedTx = PSBT.Load(partiallySignedTx.ToBytes(), Network.Main);

                Network.Main.CreateTransactionBuilder()
                .AddKeys(keys[1], keys[2])
                .SignPSBT(partiallySignedTx);

                Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 3));
                partiallySignedTx.Finalize();
                Assert.DoesNotContain(partiallySignedTx.Inputs.Select(i => i.GetSignableCoin()), o => o is null);

                var signedTx = partiallySignedTx.ExtractTransaction();
                rpc.SendRawTransaction(signedTx);
                var errors = builder.Check(signedTx);
                Assert.Empty(errors);
            }
        }
        public TransactionBroadcasterViewModel() : base("Transaction Broadcaster")
        {
            Global = Locator.Current.GetService <Global>();

            ButtonText = "Broadcast Transaction";

            PasteCommand = ReactiveCommand.CreateFromTask(async() =>
            {
                if (!string.IsNullOrEmpty(TransactionString))
                {
                    return;
                }

                var textToPaste   = await Application.Current.Clipboard.GetTextAsync();
                TransactionString = textToPaste;
            });

            IObservable <bool> broadcastTransactionCanExecute = this
                                                                .WhenAny(x => x.TransactionString, (transactionString) => !string.IsNullOrWhiteSpace(transactionString.Value))
                                                                .ObserveOn(RxApp.MainThreadScheduler);

            BroadcastTransactionCommand = ReactiveCommand.CreateFromTask(
                async() => await OnDoTransactionBroadcastAsync(),
                broadcastTransactionCanExecute);

            ImportTransactionCommand = ReactiveCommand.CreateFromTask(
                async() =>
            {
                try
                {
                    var ofd = new OpenFileDialog
                    {
                        AllowMultiple = false,
                        Title         = "Import Transaction"
                    };

                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                    {
                        var initialDirectory = Path.Combine("/media", Environment.UserName);
                        if (!Directory.Exists(initialDirectory))
                        {
                            initialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
                        }
                        ofd.Directory = initialDirectory;
                    }
                    else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                    {
                        ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
                    }

                    var window   = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
                    var selected = await ofd.ShowAsync(window, fallBack: true);

                    if (selected != null && selected.Any())
                    {
                        var path                = selected.First();
                        var psbtBytes           = await File.ReadAllBytesAsync(path);
                        PSBT psbt               = null;
                        Transaction transaction = null;
                        try
                        {
                            psbt = PSBT.Load(psbtBytes, Global.Network);
                        }
                        catch
                        {
                            var text = await File.ReadAllTextAsync(path);
                            text     = text.Trim();
                            try
                            {
                                psbt = PSBT.Parse(text, Global.Network);
                            }
                            catch
                            {
                                transaction = Transaction.Parse(text, Global.Network);
                            }
                        }

                        if (psbt != null)
                        {
                            if (!psbt.IsAllFinalized())
                            {
                                psbt.Finalize();
                            }

                            TransactionString = psbt.ToBase64();
                        }
                        else
                        {
                            TransactionString = transaction.ToHex();
                        }
                    }
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex);
                    NotificationHelpers.Error(ex.ToUserFriendlyString());
                }
            },
                outputScheduler: RxApp.MainThreadScheduler);

            Observable
            .Merge(PasteCommand.ThrownExceptions)
            .Merge(BroadcastTransactionCommand.ThrownExceptions)
            .Merge(ImportTransactionCommand.ThrownExceptions)
            .ObserveOn(RxApp.TaskpoolScheduler)
            .Subscribe(ex =>
            {
                NotificationHelpers.Error(ex.ToUserFriendlyString());
                Logger.LogError(ex);
            });
        }
Example #3
0
        public void CanBuildSegwitP2SHMultisigTransactionsWithPSBT()
        {
            using (var nodeBuilder = NodeBuilderEx.Create())
            {
                var rpc = nodeBuilder.CreateNode().CreateRPCClient();
                nodeBuilder.StartAll();
                rpc.Generate(102);

                // Build the keys and addresses
                var masterKeys         = Enumerable.Range(0, 3).Select(_ => new ExtKey()).ToArray();
                var keyRedeemAddresses = Enumerable.Range(0, 4)
                                         .Select(i => masterKeys.Select(m => m.Derive(i, false)).ToArray())
                                         .Select(keys =>
                                                 (
                                                     Keys: keys.Select(k => k.PrivateKey).ToArray(),
                                                     Redeem: PayToMultiSigTemplate.Instance.GenerateScriptPubKey(keys.Length, keys.Select(k => k.PrivateKey.PubKey).ToArray()))
                                                 ).Select(_ =>
                                                          (
                                                              Keys: _.Keys,
                                                              Redeem: _.Redeem,
                                                              Address: _.Redeem.WitHash.ScriptPubKey.Hash.ScriptPubKey.GetDestinationAddress(nodeBuilder.Network)
                                                          ));


                // Fund the addresses
                var scriptCoins = keyRedeemAddresses.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)
                           .Select(c => c.ToScriptCoin(kra.Redeem)).Single());
                }).Select(t => t.Result).ToArray();


                var destination   = new Key().ScriptPubKey;
                var amount        = new Money(1, MoneyUnit.BTC);
                var redeemScripts = keyRedeemAddresses.Select(kra => kra.Redeem).ToArray();
                var privateKeys   = keyRedeemAddresses.SelectMany(kra => kra.Keys).ToArray();


                var builder           = Network.Main.CreateTransactionBuilder();
                var rate              = new FeeRate(Money.Satoshis(1), 1);
                var partiallySignedTx = builder
                                        .AddCoins(scriptCoins)
                                        .AddKeys(privateKeys[0])
                                        .Send(destination, amount)
                                        .SubtractFees()
                                        .SetChange(new Key().ScriptPubKey)
                                        .SendEstimatedFees(rate)
                                        .BuildPSBT(true);

                Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 1));

                partiallySignedTx = PSBT.Load(partiallySignedTx.ToBytes(), Network.Main);

                Network.Main.CreateTransactionBuilder()
                .AddKeys(privateKeys[1], privateKeys[2])
                .SignPSBT(partiallySignedTx);

                Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 3));
                partiallySignedTx.Finalize();

                var signedTx = partiallySignedTx.ExtractTransaction();
                rpc.SendRawTransaction(signedTx);
                var errors = builder.Check(signedTx);
                Assert.Empty(errors);
            }
        }
Example #4
0
        public async Task CanPlayWithPSBT()
        {
            using (var tester = ServerTester.Create())
            {
                tester.Start();
                var user = tester.NewAccount();
                user.GrantAccess();
                user.RegisterDerivationScheme("BTC");
                var invoice = user.BitPay.CreateInvoice(new Invoice()
                {
                    Price             = 10,
                    Currency          = "USD",
                    PosData           = "posData",
                    OrderId           = "orderId",
                    ItemDesc          = "Some \", description",
                    FullNotifications = true
                }, Facade.Merchant);
                var cashCow        = tester.ExplorerNode;
                var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
                cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
                TestUtils.Eventually(() =>
                {
                    invoice = user.BitPay.GetInvoice(invoice.Id);
                    Assert.Equal("paid", invoice.Status);
                });

                var walletController = tester.PayTester.GetController <WalletsController>(user.UserId);
                var walletId         = new WalletId(user.StoreId, "BTC");
                var sendDestination  = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
                var sendModel        = new WalletSendModel()
                {
                    Destination       = sendDestination,
                    Amount            = 0.1m,
                    FeeSatoshiPerByte = 1,
                    CurrentBalance    = 1.5m
                };
                var vmLedger = await walletController.WalletSend(walletId, sendModel, command : "ledger").AssertViewModelAsync <WalletSendLedgerModel>();

                PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmLedger.SuccessPath);
                Assert.NotNull(vmLedger.WebsocketPath);

                var redirectedPSBT = (string)Assert.IsType <RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
                var vmPSBT         = walletController.WalletPSBT(walletId, new WalletPSBTViewModel()
                {
                    PSBT = redirectedPSBT
                }).AssertViewModel <WalletPSBTViewModel>();
                var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmPSBT.Decoded);

                var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
                PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);

                await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync <WalletSendLedgerModel>();

                var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync <WalletPSBTViewModel>();

                Assert.NotEmpty(vmPSBT2.Errors);
                Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
                Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);

                var signedPSBT = unsignedPSBT.Clone();
                signedPSBT.SignAll(user.ExtKey);
                vmPSBT.PSBT = signedPSBT.ToBase64();
                var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.Equal(2, psbtReady.Destinations.Count);
                Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
                Assert.Contains(psbtReady.Destinations, d => d.Positive);
                var redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);

                vmPSBT.PSBT = unsignedPSBT.ToBase64();
                var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync <WalletPSBTCombineViewModel>();

                Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
                combineVM.PSBT = signedPSBT.ToBase64();
                vmPSBT         = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync <WalletPSBTViewModel>();

                var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                // Can use uploaded file?
                combineVM.PSBT             = null;
                combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
                vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync <WalletPSBTViewModel>();

                signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel <WalletPSBTReadyViewModel>();
                Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
                redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
                Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
                redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
            }
        }
Example #5
0
        public async Task CanPlayWithPSBT()
        {
            using (var tester = ServerTester.Create())
            {
                await tester.StartAsync();

                var user = tester.NewAccount();
                user.GrantAccess();
                user.RegisterDerivationScheme("BTC");
                var invoice = user.BitPay.CreateInvoice(new Invoice()
                {
                    Price             = 10,
                    Currency          = "USD",
                    PosData           = "posData",
                    OrderId           = "orderId",
                    ItemDesc          = "Some \", description",
                    FullNotifications = true
                }, Facade.Merchant);
                var cashCow        = tester.ExplorerNode;
                var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
                cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
                TestUtils.Eventually(() =>
                {
                    invoice = user.BitPay.GetInvoice(invoice.Id);
                    Assert.Equal("paid", invoice.Status);
                });

                var walletController = user.GetController <WalletsController>();
                var walletId         = new WalletId(user.StoreId, "BTC");
                var sendDestination  = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
                var sendModel        = new WalletSendModel()
                {
                    Outputs = new List <WalletSendModel.TransactionOutput>()
                    {
                        new WalletSendModel.TransactionOutput()
                        {
                            DestinationAddress = sendDestination,
                            Amount             = 0.1m,
                        }
                    },
                    FeeSatoshiPerByte = 1,
                    CurrentBalance    = 1.5m
                };

                string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
                var    vmPSBT         = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel()
                {
                    PSBT = redirectedPSBT
                }).AssertViewModelAsync <WalletPSBTViewModel>();

                var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmPSBT.Decoded);

                var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
                PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);

                var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
                {
                    SigningContext = new SigningContextModel()
                    {
                        PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
                    }
                }).AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
                Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);

                var signedPSBT = unsignedPSBT.Clone();
                signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
                vmPSBT.PSBT = signedPSBT.ToBase64();
                var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
                {
                    SigningContext = new SigningContextModel
                    {
                        PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
                    }
                }).AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
                Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
                Assert.Contains(psbtReady.Destinations, d => d.Positive);

                vmPSBT.PSBT = unsignedPSBT.ToBase64();
                var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync <WalletPSBTCombineViewModel>();

                Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
                combineVM.PSBT = signedPSBT.ToBase64();
                var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));

                var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                // Can use uploaded file?
                combineVM.PSBT             = null;
                combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
                psbt        = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
                signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
                {
                    SigningContext = new SigningContextModel(signedPSBT)
                })).AssertViewModel <WalletPSBTReadyViewModel>();
                Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
                psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
                Assert.Equal(signedPSBT.ToBase64(), psbt);
                var redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);

                //test base64 psbt file
                Assert.False(string.IsNullOrEmpty(Assert.IsType <WalletPSBTViewModel>(
                                                      Assert.IsType <ViewResult>(
                                                          await walletController.WalletPSBT(walletId,
                                                                                            new WalletPSBTViewModel()
                {
                    UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
                })).Model).PSBT));
            }
        }