public async Task <WalletProcessPSBTResponse> WalletProcessPSBTAsync(PSBT psbt, bool sign = true, SigHash sighashType = SigHash.All, bool bip32derivs = false) { if (psbt == null) { throw new ArgumentNullException(nameof(psbt)); } var response = await SendCommandAsync(RPCOperations.walletprocesspsbt, psbt.ToBase64(), sign, SigHashToString(sighashType), bip32derivs).ConfigureAwait(false); var result = (JObject)response.Result; var psbt2 = PSBT.Parse(result.Property("psbt").Value.Value <string>(), Network.Main); var complete = result.Property("complete").Value.Value <bool>(); return(new WalletProcessPSBTResponse(psbt2, complete)); }
PSBT GetAmbientPSBT(Network network, bool peek) { if (network == null) { throw new ArgumentNullException(nameof(network)); } if ((peek ? TempData.Peek("AmbientPSBT") : TempData["AmbientPSBT"]) is string str) { try { return(PSBT.Parse(str, network)); } catch { } } return(null); }
public static async Task <PSBT> UTXOUpdatePSBT(this RPCClient rpcClient, PSBT psbt) { if (psbt == null) { throw new ArgumentNullException(nameof(psbt)); } var response = await rpcClient.SendCommandAsync("utxoupdatepsbt", new object[] { psbt.ToBase64() }); response.ThrowIfError(); if (response.Error == null && response.Result is JValue rpcResult && rpcResult.Value is string psbtStr) { return(PSBT.Parse(psbtStr, psbt.Network)); } throw new Exception("This should never happen"); }
public static async Task <PSBT> SignTxAsync(HardwareWalletInfo hardwareWalletInfo, PSBT psbt) { var psbtString = psbt.ToBase64(); var networkString = Network == Network.Main ? "" : "--testnet"; JToken jtok = await SendCommandAsync($"{networkString} --device-type \"{hardwareWalletInfo.Type.ToString().ToLowerInvariant()}\" --device-path \"{hardwareWalletInfo.Path}\" signtx {psbtString}", isMutexPriority : true); JObject json = jtok as JObject; var signedPsbtString = json.Value <string>("psbt"); var signedPsbt = PSBT.Parse(signedPsbtString, Network); if (!signedPsbt.IsAllFinalized()) { signedPsbt.Finalize(); } return(signedPsbt); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { return(null); } reader.AssertJsonType(JsonToken.String); try { return(PSBT.Parse((string)reader.Value, Network)); } catch (EndOfStreamException) { } catch (FormatException) { } throw new JsonObjectException("Invalid PSBT", reader); }
public static PSBT ParsePsbt(string json, Network network) { // HWI does not support regtest, so the parsing would fail here. if (network == Network.RegTest) { network = Network.TestNet; } if (JsonHelpers.TryParseJToken(json, out JToken token)) { var psbtString = token["psbt"]?.ToString()?.Trim() ?? null; var psbt = PSBT.Parse(psbtString, network); return(psbt); } else { throw new FormatException($"Could not parse PSBT: {json}."); } }
public static AcceptorTest Open(string folder, Network network) { var settings = network == Network.RegTest ? Settings : TestnetSettings; AcceptorTest t = new AcceptorTest(); var fundingOverride = Path.Combine(folder, "FundingOverride.hex"); if (File.Exists(fundingOverride)) { t.FundingOverride = Transaction.Parse(File.ReadAllText(fundingOverride), network); } t.Offer = Parse <Offer>(Path.Combine(folder, "Offer.json"), settings); t.Sign = Parse <Sign>(Path.Combine(folder, "Sign.json"), settings); var attestation = Path.Combine(folder, "OracleAttestation.hex"); if (File.Exists(attestation)) { t.OracleAttestation = new Key(Encoders.Hex.DecodeData(File.ReadAllText(attestation))); } t.Builder = new DLCTransactionBuilder(false, null, null, null, network); t.FundingTemplate = PSBT.Parse(File.ReadAllText(Path.Combine(folder, "FundingTemplate.psbt")), network); return(t); }
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 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 <WalletCreateFundedPSBTResponse> WalletCreateFundedPSBTAsync( TxIn[] inputs, Tuple <Dictionary <BitcoinAddress, Money>, Dictionary <string, string> > outputs, LockTime locktime = default(LockTime), FundRawTransactionOptions options = null, bool bip32derivs = false ) { var values = new object[] { }; if (inputs == null) { inputs = new TxIn[] {} } ; if (outputs == null) { throw new ArgumentNullException(nameof(outputs)); } var rpcInputs = inputs.Select(i => i.ToRPCInputs()).ToArray(); var outputToSend = new JObject { }; if (outputs.Item1 != null) { foreach (var kv in outputs.Item1) { outputToSend.Add(kv.Key.ToString(), kv.Value.ToUnit(MoneyUnit.BTC)); } } if (outputs.Item2 != null) { foreach (var kv in outputs.Item2) { outputToSend.Add(kv.Key, kv.Value); } } JObject jOptions; if (options != null) { jOptions = FundRawTransactionOptionsToJson(options); } else { jOptions = (JObject)""; } RPCResponse response = await SendCommandAsync( "walletcreatefundedpsbt", rpcInputs, outputToSend, locktime.Value, jOptions, bip32derivs).ConfigureAwait(false); var result = (JObject)response.Result; var psbt = PSBT.Parse(result.Property("psbt").Value.Value <string>(), Network.Main); var fee = Money.Coins(result.Property("fee").Value.Value <decimal>()); var changePos = result.Property("changepos").Value.Value <int>(); var tmp = changePos == -1 ? (int?)null : (int?)changePos; return(new WalletCreateFundedPSBTResponse { PSBT = psbt, Fee = fee, ChangePos = tmp }); }
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; int? pin = null; var websocketHelper = new WebSocketHelper(websocket); async Task <bool> RequireDeviceUnlocking() { if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); return(true); } if (deviceEntry.NeedsPinSent is true && pin is null) { await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); return(true); } if (deviceEntry.NeedsPassphraseSent is true && password == 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; } 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 "ask-pin": if (device == null) { await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); continue; } await device.PromptPinAsync(cancellationToken); await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken); pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture); if (await device.SendPinAsync(pin.Value, cancellationToken)) { await websocketHelper.Send("{ \"info\": \"the pin is correct\"}", cancellationToken); } else { await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken); continue; } break; case "ask-xpubs": if (await RequireDeviceUnlocking()) { continue; } JObject result = new JObject(); var factory = network.NBXplorerNetwork.DerivationStrategyFactory; var keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(0, true); BitcoinExtPubKey xpub = await device.GetXPubAsync(keyPath); if (fingerprint is null) { fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint; } result["fingerprint"] = fingerprint.Value.ToString(); var strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.Segwit }); AddDerivationSchemeToJson("segwit", result, keyPath, xpub, strategy); keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(0, true); xpub = await device.GetXPubAsync(keyPath); strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH }); AddDerivationSchemeToJson("segwitWrapped", result, keyPath, xpub, strategy); keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(0, true); xpub = await device.GetXPubAsync(keyPath); strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.Legacy }); AddDerivationSchemeToJson("legacy", result, keyPath, xpub, strategy); await websocketHelper.Send(result.ToString(), cancellationToken); break; case "ask-device": password = null; pin = 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 (Exception ex) { JObject obj = new JObject(); obj.Add("error", "unknown-error"); obj.Add("details", ex.ToString()); try { await websocketHelper.Send(obj.ToString(), cancellationToken); } catch { } } finally { await websocketHelper.DisposeAsync(cancellationToken); } } return(new EmptyResult()); }
public async Task <IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, SignWithSeedViewModel viewModel) { if (!ModelState.IsValid) { return(View(viewModel)); } var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork); if (extKey == null) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv"); } var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork); if (!psbt.IsReadyToSign()) { ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed"); } if (!ModelState.IsValid) { return(View(viewModel)); } ExtKey signingKey = null; var settings = GetDerivationSchemeSettings(walletId); var signingKeySettings = settings.GetSigningAccountKeySettings(); if (signingKeySettings.RootFingerprint is null) { signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint(); } RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath(); // The user gave the root key, let's try to rebase the PSBT, and derive the account private key if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) { psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath); signingKey = extKey.Derive(rootedKeyPath.KeyPath); } // The user maybe gave the account key, let's try to sign with it else { signingKey = extKey; } var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath); if (balanceChange == Money.Zero) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed is unable to sign this transaction. Either the seed is incorrect, or the account path has not been properly configured in the Wallet Settings."); return(View(viewModel)); } psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath); ModelState.Remove(nameof(viewModel.PSBT)); return(await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString())); }
public async Task <IActionResult> WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTReadyViewModel vm, string command = null) { if (command == null) { return(await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath)); } PSBT psbt = null; var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); try { psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) { return(NotFound()); } await FetchTransactionDetails(derivationSchemeSettings, vm, network); } catch { vm.GlobalError = "Invalid PSBT"; return(View(vm)); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.SetErrors(errors); return(View(vm)); } var transaction = psbt.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; return(View(vm)); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; return(View(vm)); } return(RedirectToWalletTransaction(walletId, transaction)); } else if (command == "analyze-psbt") { return(RedirectToWalletPSBT(walletId, psbt)); } else { vm.GlobalError = "Unknown command"; return(View(vm)); } }
public async Task <IActionResult> LedgerConnection( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, string command, // getinfo // getxpub int account = 0, // sendtoaddress string psbt = null, string hintChange = null ) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); var derivationSettings = GetDerivationSchemeSettings(walletId, storeData); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); using (var normalOperationTimeout = new CancellationTokenSource()) using (var signTimeout = new CancellationTokenSource()) { normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30)); var hw = new LedgerHardwareWalletService(webSocket); var model = new WalletSendLedgerModel(); object result = null; try { if (command == "test") { result = await hw.Test(normalOperationTimeout.Token); } if (command == "sendtoaddress") { if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) { throw new Exception($"{network.CryptoCode}: not started or fully synched"); } var accountKey = derivationSettings.GetSigningAccountKeySettings(); // Some deployment does not have the AccountKeyPath set, let's fix this... if (accountKey.AccountKeyPath == null) { // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy var foundKeyPath = await hw.FindKeyPathFromDerivation(network, derivationSettings.AccountDerivation, normalOperationTimeout.Token); accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger"); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger else { // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, // but some deployment does not have it, so let's use AccountKeyPath instead if (accountKey.RootFingerprint == null) { var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token); if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey())) { throw new HardwareWalletException($"This store is not configured to use this ledger"); } } // We have the root fingerprint, we can check the root from it else { var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token); if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value) { throw new HardwareWalletException($"This store is not configured to use this ledger"); } } } // Some deployment does not have the RootFingerprint set, let's fix this... if (accountKey.RootFingerprint == null) { accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint(); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } var psbtResponse = new CreatePSBTResponse() { PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork), ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork) }; derivationSettings.RebaseKeyPaths(psbtResponse.PSBT); signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token); result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() }; } } catch (OperationCanceledException) { result = new LedgerTestResult() { Success = false, Error = "Timeout" }; } catch (Exception ex) { result = new LedgerTestResult() { Success = false, Error = ex.Message }; } finally { hw.Dispose(); } try { if (result != null) { UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings)); await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); } } catch { } finally { await webSocket.CloseSocket(); } } return(new EmptyResult()); }
public void ShouldPassTheLongestTestInBIP174() { JObject testcase = (JObject)testdata["final"]; var network = Network.TestNet; var master = ExtKey.Parse((string)testcase["master"], network); var masterFP = BitConverter.ToUInt32(master.PrivateKey.PubKey.Hash.ToBytes().SafeSubarray(0, 4), 0); var tx = network.CreateTransaction(); tx.Version = 2; var scriptPubKey1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out1"]["script"])); var money1 = Money.Coins((decimal)testcase["out1"]["value"]); var scriptPubKey2 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out2"]["script"])); var money2 = Money.Coins((decimal)testcase["out2"]["value"]); tx.Outputs.Add(new TxOut(value: money1, scriptPubKey: scriptPubKey1)); tx.Outputs.Add(new TxOut(value: money2, scriptPubKey: scriptPubKey2)); tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in1"]["txid"]), (uint)testcase["in1"]["index"])); tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in2"]["txid"]), (uint)testcase["in2"]["index"])); var expected = PSBT.Parse((string)testcase["psbt1"], Network.Main); var psbt = PSBT.FromTransaction(tx); Assert.Equal(expected, psbt, ComparerInstance); var prevtx1 = Transaction.Parse((string)testcase["prevtx1"], network); var prevtx2 = Transaction.Parse((string)testcase["prevtx2"], network); psbt.AddTransactions(prevtx1, prevtx2); var redeem1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem1"])); var redeem2 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem2"])); var witness_script1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["witness1"])); foreach (var sc in new Script[] { redeem1, redeem2, witness_script1 }) { psbt.AddScript(sc); } for (int i = 0; i < 6; i++) { var pk = testcase[$"pubkey{i}"]; var pubkey = new PubKey((string)pk["hex"]); var path = KeyPath.Parse((string)pk["path"]); psbt.AddKeyPath(pubkey, Tuple.Create(masterFP, path)); } expected = PSBT.Parse((string)testcase["psbt2"], Network.Main); Assert.Equal(expected, psbt, ComparerInstance); foreach (var psbtin in psbt.Inputs) { psbtin.SighashType = SigHash.All; } expected = PSBT.Parse((string)testcase["psbt3"], Network.Main); Assert.Equal(expected, psbt, ComparerInstance); psbt.CheckSanity(); var psbtForBob = psbt.Clone(); // path 1 ... alice Assert.Equal(psbt, psbtForBob, ComparerInstance); var aliceKey1 = master.Derive(new KeyPath((string)testcase["key7"]["path"])).PrivateKey; var aliceKey2 = master.Derive(new KeyPath((string)testcase["key8"]["path"])).PrivateKey; psbt.SignAll(aliceKey1, aliceKey2); expected = PSBT.Parse((string)testcase["psbt4"], Network.Main); Assert.Equal(expected, psbt); // path 2 ... bob. var bobKey1 = master.Derive(new KeyPath((string)testcase["key9"]["path"])).PrivateKey; var bobKey2 = master.Derive(new KeyPath((string)testcase["key10"]["path"])).PrivateKey; var bobKeyhex1 = (string)testcase["key9"]["wif"]; var bobKeyhex2 = (string)testcase["key10"]["wif"]; Assert.Equal(bobKey1, new BitcoinSecret(bobKeyhex1, network).PrivateKey); Assert.Equal(bobKey2, new BitcoinSecret(bobKeyhex2, network).PrivateKey); psbtForBob.UseLowR = false; psbtForBob.SignAll(bobKey1, bobKey2); expected = PSBT.Parse((string)testcase["psbt5"], Network.Main); Assert.Equal(expected, psbtForBob); // merge above 2 var combined = psbt.Combine(psbtForBob); expected = PSBT.Parse((string)testcase["psbtcombined"], Network.Main); Assert.Equal(expected, combined); var finalized = psbt.Finalize(); expected = PSBT.Parse((string)testcase["psbtfinalized"], Network.Main); Assert.Equal(expected, finalized); var finalTX = psbt.ExtractTX(); var expectedTX = Transaction.Parse((string)testcase["txextracted"], network); AssertEx.CollectionEquals(expectedTX.ToBytes(), finalTX.ToBytes()); }
public async Task CanMakeContract(bool verbose) { var alice = CreateDataDirectory("Alice"); var bob = CreateDataDirectory("Bob"); var parties = new[] { alice, bob }; var olivia = CreateDataDirectory("Olivia"); await Tester.AssertInvokeSuccess(new string[] { "--datadir", olivia, "oracle", "generate", "olivia" }); var oliviaPubKey = Tester.GetLastOutput(); await Tester.AssertInvokeSuccess(new string[] { "--datadir", olivia, "event", "generate", "olivia/coolestguy", "Ninja", "Samurai", "Cowboy", "NicolasDorier" }); var nonce = Tester.GetLastOutput(); foreach (var party in parties) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", party, "oracle", "add", "olivia", oliviaPubKey }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", party, "event", "add", "olivia/coolestguy", nonce, "Ninja", "Samurai", "Cowboy", "NicolasDorier" }); } await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "offer", "BetWithBob", "olivia/coolestguy", "Ninja:-0.4", "cowboy:-1.2", "Samurai:0.6", "NicolasDorier:1" }); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob", }); } var aliceSigner = new Key(); var offerFunding = CreateOfferFunding(Money.Coins(1.2m), aliceSigner); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "setup", "BetWithBob", offerFunding }); var offer = Tester.GetLastOutput(); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob", }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "--offer", "BetWithBob", }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "--offer", "--json", "BetWithBob", }); } await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "review", offer }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "accept", "BetWithAlice", offer }); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "BetWithAlice" }); } var bobSigner = new Key(); var acceptFunding = CreateOfferFunding(Money.Coins(1.0m), bobSigner); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "setup", "BetWithAlice", acceptFunding }); var acceptorSigs = Tester.GetLastOutput(); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "BetWithAlice" }); } await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "checksigs", acceptorSigs }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob" }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob", "--funding" }); var aliceFunding = Tester.GetLastOutput(); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "BetWithAlice", "--funding" }); var bobFunding = Tester.GetLastOutput(); Assert.Equal(aliceFunding, bobFunding); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob" }); var funding = PSBT.Parse(aliceFunding, Network.Main); funding.SignWithKeys(aliceSigner); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "start", "BetWithBob", funding.ToBase64() }); var signMessage = Tester.GetLastOutput(); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "BetWithBob" }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "show", "--refund", "BetWithBob" }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "--refund", "BetWithAlice" }); } await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "checksigs", signMessage }); if (verbose) { await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "BetWithAlice" }); } funding = PSBT.Parse(bobFunding, Network.Main); funding.SignWithKeys(bobSigner); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "start", "--psbt", "BetWithAlice", funding.ToBase64() }); // Should be fully signed var fullySigned = PSBT.Parse(Tester.GetLastOutput(), Network.Main); fullySigned.Finalize(); fullySigned.ExtractTransaction(); // Ready! await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "show", "BetWithAlice" }); // Olivia attest NicolasDorier to be the coolest guy await Tester.AssertInvokeSuccess(new string[] { "--datadir", olivia, "event", "attest", "sign", "olivia/coolestguy", "NicolasDorier" }); var attestation = Tester.GetLastOutput(); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "execute", "BetWithAlice", attestation }); var cet = Tester.GetLastOutput(); // Can Alice extract the attestation through CET? await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "extract", "BetWithBob", cet }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", alice, "dlc", "execute", "BetWithBob" }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "execute", "BetWithAlice" }); await Tester.AssertInvokeSuccess(new string[] { "--datadir", bob, "dlc", "list" }); if (verbose) { foreach (var peer in new[] { (bob, "BetWithAlice"), (alice, "BetWithBob") })
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)); } }
public void CanFollowBIPExample() { var extkey = new BitcoinExtKey("tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF", Network.TestNet); // A creator creating a PSBT for a transaction which creates the following outputs: Transaction tx = extkey.Network.CreateTransaction(); tx.Version = 2; tx.Outputs.Add(Money.Coins(1.49990000m), new Script(Encoders.Hex.DecodeData("0014d85c2b71d0060b09c9886aeb815e50991dda124d"))); tx.Outputs.Add(Money.Coins(1.00000000m), new Script(Encoders.Hex.DecodeData("001400aea9a2e5f0f876a588df5546e8742d1d87008f"))); // and spends the following inputs: tx.Inputs.Add(OutPoint.Parse("75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858-0")); tx.Inputs.Add(OutPoint.Parse("1dea7cd05979072a3578cab271c02244ea8a090bbb46aa680a65ecd027048d83-1")); var actualPsbt = PSBT.FromTransaction(tx, Network.Main); // must create this PSBT: var expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, an updater with only the following: // Previous Transactions: actualPsbt.AddTransactions( Transaction.Parse("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000", Network.Main), Transaction.Parse("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000", Network.Main) ); // Scripts actualPsbt.AddScripts( new Script(Encoders.Hex.DecodeData("5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae")), new Script(Encoders.Hex.DecodeData("00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903")), new Script(Encoders.Hex.DecodeData("522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"))); // Public Keys actualPsbt.AddKeyPath(extkey, new KeyPath("m/0'/0'/0'"), new KeyPath("m/0'/0'/1'"), new KeyPath("m/0'/0'/2'"), new KeyPath("m/0'/0'/3'"), new KeyPath("m/0'/0'/4'"), new KeyPath("m/0'/0'/5'")); // Must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); // An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT: foreach (var input in actualPsbt.Inputs) input.SighashType = SigHash.All; expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); actualPsbt.Settings.UseLowR = false; expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); var tmp = actualPsbt.Clone(); // Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys: actualPsbt.SignWithKeys(extkey.Derive(new KeyPath("m/0'/0'/0'")), extkey.Derive(new KeyPath("m/0'/0'/2'"))); Assert.Equal(expectedPsbt, actualPsbt); actualPsbt = tmp.Clone(); actualPsbt.SignWithKeys(new BitcoinSecret("cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr", Network.TestNet)); actualPsbt.SignWithKeys(new BitcoinSecret("cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d", Network.TestNet)); var part1 = actualPsbt; Assert.Equal(expectedPsbt, actualPsbt); actualPsbt = tmp.Clone(); // Given the above updated PSBT, a signer with the following keys: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt.SignWithKeys(new BitcoinSecret("cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au", Network.TestNet)); actualPsbt.SignWithKeys(new BitcoinSecret("cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE", Network.TestNet)); var part2 = actualPsbt; Assert.Equal(expectedPsbt, actualPsbt); // Given both of the above PSBTs, a combiner must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt = part1.Combine(part2); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, an input finalizer must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt.Finalize(); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, a transaction extractor must create this Bitcoin transaction: var expectedTx = Transaction.Parse("0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000", extkey.Network); Assert.True(actualPsbt.CanExtractTransaction()); var actualTx = actualPsbt.ExtractTransaction().ToHex(); Assert.Equal(expectedTx.ToHex(), actualTx); // Given these two PSBTs with unknown key-value pairs: var psbt1 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00", extkey.Network); var psbt2 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network); // A combiner which orders keys lexicographically must produce the following PSBT: expectedPsbt = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network); actualPsbt = psbt1.Combine(psbt2); Assert.Equal(expectedPsbt, actualPsbt); }
public async Task CanPlayWithPSBT() { using var s = CreateSeleniumTester(newDb: true); await s.StartAsync(); var u1 = s.RegisterNewUser(true); var hot = s.CreateNewStore(); var seed = s.GenerateWallet(isHotWallet: true); var cold = s.CreateNewStore(); s.GenerateWallet(isHotWallet: false, seed: seed.ToString()); // Scenario 1: one user has two stores sharing same seed // one store is hot wallet, the other not. // Here, the cold wallet create a PSBT, then we switch to hot wallet to sign // the PSBT and broadcast s.GoToStore(cold.storeId); var address = await s.FundStoreWallet(); Thread.Sleep(1000); s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); SendAllTo(s, address); s.Driver.FindElement(By.Id("SignWithPSBT")).Click(); var psbt = ExtractPSBT(s); s.GoToStore(hot.storeId); s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt); s.Driver.FindElement(By.Id("Decode")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click(); s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); s.FindAlertMessage(); // Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually // entering the seed on the cold wallet. s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); SendAllTo(s, address); psbt = ExtractPSBT(s); // Let's check it has been signed, then remove the signature. // Also remove the hdkeys so we can test the update later var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); var signedPSBT = psbtParsed.Clone(); Assert.True(psbtParsed.Clone().TryFinalize(out _)); Assert.Single(psbtParsed.Inputs[0].PartialSigs); psbtParsed.Inputs[0].PartialSigs.Clear(); Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); psbtParsed.Inputs[0].HDKeyPaths.Clear(); var skeletonPSBT = psbtParsed; s.GoToStore(cold.storeId); s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); s.Driver.FindElement(By.Id("Decode")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click(); s.Driver.FindElement(By.Id("SignWithSeed")).Click(); s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString()); s.Driver.FindElement(By.Id("Submit")).Click(); s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); s.FindAlertMessage(); // Let's check if the update feature works s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); s.Driver.FindElement(By.Id("Decode")).Click(); s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); s.Driver.WaitForElement(By.Id("update-psbt")).Click(); psbt = ExtractPSBT(s); psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); Assert.Empty(psbtParsed.Inputs[0].PartialSigs); // Let's if we can combine the updated psbt (which has hdkeys, but no sig) // with the signed psbt (which has sig, but no hdkeys) s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64()); s.Driver.FindElement(By.Id("Decode")).Click(); s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); s.Driver.WaitForElement(By.Id("combine-psbt")).Click(); signedPSBT.Inputs[0].HDKeyPaths.Clear(); s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64()); s.Driver.WaitForElement(By.Id("Submit")).Click(); psbt = ExtractPSBT(s); psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); Assert.Single(psbtParsed.Inputs[0].PartialSigs); }
public async Task <IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, SignWithSeedViewModel viewModel) { if (!ModelState.IsValid) { return(View(viewModel)); } var network = NetworkProvider.GetNetwork(walletId.CryptoCode); if (network == null) { throw new FormatException("Valor no válido para el código criptográfico"); } ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork); if (extKey == null) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "La seed o la clave no estaban en un formato válido. Es el 12/24 palabras o comienza con xprv"); } var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork); if (!psbt.IsReadyToSign()) { ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT no está listo para ser firmado"); } if (!ModelState.IsValid) { return(View(viewModel)); } ExtKey signingKey = null; var settings = (await GetDerivationSchemeSettings(walletId)); var signingKeySettings = settings.GetSigningAccountKeySettings(); if (signingKeySettings.RootFingerprint is null) { signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint(); } RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath(); // The user gave the root key, let's try to rebase the PSBT, and derive the account private key if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) { psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath); signingKey = extKey.Derive(rootedKeyPath.KeyPath); } // The user maybe gave the account key, let's try to sign with it else { signingKey = extKey; } var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath); if (balanceChange == Money.Zero) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Esta seed no parece poder firmar esta transacción. Esta es la clave incorrecta o la configuración de la cartera no tiene la ruta de cuenta correcta en la configuración de la cartera."); return(View(viewModel)); } psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath); ModelState.Remove(nameof(viewModel.PSBT)); return(await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString())); }
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"); }); }
public async Task TrezorTKataAsync() { // --- USER INTERACTIONS --- // // Connect and initialize your Trezor T with the following seed phrase: // more maid moon upgrade layer alter marine screen benefit way cover alcohol // Run this test. // displayaddress request: refuse 1 time // displayaddress request: confirm 2 times // displayaddress request: confirm 1 time // signtx request: confirm 23 times + Hold to confirm // // --- USER INTERACTIONS --- var network = Network.Main; var client = new HwiClient(network); using var cts = new CancellationTokenSource(ReasonableRequestTimeout); var enumerate = await client.EnumerateAsync(cts.Token); Assert.Single(enumerate); HwiEnumerateEntry entry = enumerate.Single(); Assert.NotNull(entry.Path); Assert.Equal(HardwareWalletModels.Trezor_T, entry.Model); Assert.True(entry.Fingerprint.HasValue); string devicePath = entry.Path; HardwareWalletModels deviceType = entry.Model; HDFingerprint fingerprint = entry.Fingerprint.Value; await Assert.ThrowsAsync <HwiException>(async() => await client.SetupAsync(deviceType, devicePath, false, cts.Token)); await Assert.ThrowsAsync <HwiException>(async() => await client.RestoreAsync(deviceType, devicePath, false, cts.Token)); // Trezor T doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.PromptPinAsync(deviceType, devicePath, cts.Token)); // Trezor T doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.SendPinAsync(deviceType, devicePath, 1111, cts.Token)); KeyPath keyPath1 = KeyManager.DefaultAccountKeyPath; KeyPath keyPath2 = KeyManager.DefaultAccountKeyPath.Derive(1); ExtPubKey xpub1 = await client.GetXpubAsync(deviceType, devicePath, keyPath1, cts.Token); ExtPubKey xpub2 = await client.GetXpubAsync(deviceType, devicePath, keyPath2, cts.Token); Assert.NotNull(xpub1); Assert.NotNull(xpub2); Assert.NotEqual(xpub1, xpub2); // USER SHOULD REFUSE ACTION await Assert.ThrowsAsync <HwiException>(async() => await client.DisplayAddressAsync(deviceType, devicePath, keyPath1, cts.Token)); // USER: CONFIRM 2 TIMES BitcoinWitPubKeyAddress address1 = await client.DisplayAddressAsync(deviceType, devicePath, keyPath1, cts.Token); // USER: CONFIRM 1 TIME BitcoinWitPubKeyAddress address2 = await client.DisplayAddressAsync(fingerprint, keyPath2, cts.Token); Assert.NotNull(address1); Assert.NotNull(address2); Assert.NotEqual(address1, address2); var expectedAddress1 = xpub1.PubKey.GetAddress(ScriptPubKeyType.Segwit, network); var expectedAddress2 = xpub2.PubKey.GetAddress(ScriptPubKeyType.Segwit, network); Assert.Equal(expectedAddress1, address1); Assert.Equal(expectedAddress2, address2); // USER: CONFIRM 23 TIMES + Hold to confirm // The user has to confirm multiple times because this transaction spends 22 inputs. // The transaction is similar to these transactions: // https://blockstream.info/testnet/tx/580d04a1891bf5b03a972eb63791e57ca39b85476d45f1d82a09732fe4c9214d // https://blockstream.info/testnet/tx/82cd8165a4fb3276354a817ad1b991a0c4af7d6d438f9052f34d58712f873457 PSBT psbt = PSBT.Parse("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", network); PSBT signedPsbt = await client.SignTxAsync(deviceType, devicePath, psbt, cts.Token); Transaction signedTx = signedPsbt.GetOriginalTransaction(); Assert.Equal(psbt.GetOriginalTransaction().GetHash(), signedTx.GetHash()); var checkResult = signedTx.Check(); Assert.Equal(TransactionCheckResult.Success, checkResult); }
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 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")); }
public async Task <PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken) { Guard.NotNull(nameof(originalTx), originalTx); if (originalTx.IsAllFinalized()) { throw new InvalidOperationException("The original PSBT should not be finalized."); } var sentBefore = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath); var oldGlobalTx = originalTx.GetGlobalTransaction(); if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize)) { throw new ArgumentException("originalTx should have utxo information", nameof(originalTx)); } var originalFee = originalTx.GetFee(); var cloned = originalTx.Clone(); if (!cloned.TryFinalize(out var _)) { return(null); } // We make sure we don't send unnecessary information to the receiver foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized())) { finalized.ClearForFinalize(); } foreach (var output in cloned.Outputs) { output.HDKeyPaths.Clear(); } cloned.GlobalXPubs.Clear(); var request = new HttpRequestMessage(HttpMethod.Post, PaymentUrl) { Content = new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain") }; HttpResponseMessage bpuResponse = await TorHttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); if (!bpuResponse.IsSuccessStatusCode) { var errorStr = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false); try { var error = JObject.Parse(errorStr); throw new PayjoinReceiverException((int)bpuResponse.StatusCode, error["errorCode"].Value <string>(), error["message"].Value <string>()); } catch (JsonReaderException) { // will throw bpuResponse.EnsureSuccessStatusCode(); throw; } } var hexOrBase64 = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false); var newPSBT = PSBT.Parse(hexOrBase64, originalTx.Network); // Checking that the PSBT of the receiver is clean if (newPSBT.GlobalXPubs.Any()) { throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT"); } if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT"); } if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0) { throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})"); } // Do not trust on inputs order because the payjoin server should shuffle them. foreach (var input in originalTx.Inputs) { var newInput = newPSBT.Inputs.FindIndexedInput(input.PrevOut); if (newInput is { })
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 <IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, SignWithSeedViewModel viewModel) { if (!ModelState.IsValid) { return(View(viewModel)); } var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork); if (extKey == null) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv"); } var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork); if (!psbt.IsReadyToSign()) { ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed"); } if (!ModelState.IsValid) { return(View(viewModel)); } ExtKey signingKey = null; var settings = GetDerivationSchemeSettings(walletId); var signingKeySettings = settings.GetSigningAccountKeySettings(); if (signingKeySettings.RootFingerprint is null) { signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint(); } RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath(); if (rootedKeyPath == null) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings."); return(View(viewModel)); } // The user gave the root key, let's try to rebase the PSBT, and derive the account private key if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) { psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath); signingKey = extKey.Derive(rootedKeyPath.KeyPath); } else { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings."); return(View(viewModel)); } var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath)); if (!changed) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed."); return(View(viewModel)); } ModelState.Remove(nameof(viewModel.PSBT)); return(await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString())); }
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); } }
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; bool pinProvided = false; 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 || deviceEntry.NeedsPinSent is true) && !pinProvided) { if (!IsTrezorT(deviceEntry)) { await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); return(true); } else { try { // On trezor T this will prompt the password! (https://github.com/bitcoin-core/HWI/issues/283) await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); } catch (HwiException ex) when(ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) { pinProvided = true; } await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); return(true); } } if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPassphraseSent is true) && password == null) { if (IsTrezorT(deviceEntry)) { await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); } else { 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 "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) { pinProvided = true; 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)) { pinProvided = true; await websocketHelper.Send("{ \"info\": \"the pin is correct\"}", cancellationToken); } else { await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken); continue; } break; 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 "refresh-device": case "ask-device": DeviceSelector deviceSelector = (command == "refresh-device" && deviceEntry != null ? deviceEntry.DeviceSelector : null); password = null; pinProvided = false; deviceEntry = null; device = null; var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); deviceEntry = entries.Where(h => deviceSelector == null || SameSelector(deviceSelector, h.DeviceSelector)).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()); }
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 or 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)); } }