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