public static async Task <PSBT> SignTxAsync(HardwareWalletInfo hardwareWalletInfo, PSBT psbt) { var psbtString = psbt.ToBase64(); var networkString = Network == Network.Main ? "" : "--testnet"; try { 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); } catch (IOException ex) when(hardwareWalletInfo.Type == HardwareWalletType.Ledger && (ex.Message.Contains("sign_tx cancelled", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("open failed", StringComparison.OrdinalIgnoreCase))) { throw new IOException("Log into your Bitcoin account on your Ledger. If you're already logged in, log out and log in again."); } catch (IOException ex) when(hardwareWalletInfo.Type == HardwareWalletType.Ledger && ex.Message.Contains("Bad argument", StringComparison.OrdinalIgnoreCase)) { throw new IOException("Ledger refused to sign the transaction."); } }
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 TransactionBroadcasterViewModel() : base("Transaction Broadcaster") { Global = Locator.Current.GetService <Global>(); _buttonText = "Broadcast Transaction"; this.WhenAnyValue(x => x.FinalTransaction) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { try { if (x is null) { TransactionDetails = null; } else { TransactionDetails = TransactionDetailsViewModel.FromBuildTxnResult(Global.BitcoinStore, PSBT.FromTransaction(x.Transaction, Global.Network)); NotificationHelpers.Information("Transaction imported successfully!"); } } catch (Exception ex) { TransactionDetails = null; NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); } }); PasteCommand = ReactiveCommand.CreateFromTask(async() => { try { var textToPaste = await Application.Current.Clipboard.GetTextAsync(); if (string.IsNullOrWhiteSpace(textToPaste)) { FinalTransaction = null; NotificationHelpers.Information("Clipboard is empty!"); } else if (PSBT.TryParse(textToPaste, Global.Network ?? Network.Main, out var signedPsbt)) { if (!signedPsbt.IsAllFinalized()) { signedPsbt.Finalize(); } FinalTransaction = signedPsbt.ExtractSmartTransaction(); } else { FinalTransaction = new SmartTransaction(Transaction.Parse(textToPaste, Global.Network ?? Network.Main), WalletWasabi.Models.Height.Unknown); } } catch (Exception ex) { FinalTransaction = null; NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); } }); IObservable <bool> broadcastTransactionCanExecute = this .WhenAny(x => x.FinalTransaction, (tx) => tx.Value is { })
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, _serializerSettings)); await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); } } catch { } finally { await webSocket.CloseSocket(); } } return(new EmptyResult()); }
private PSBT TryNegotiatePayjoin(IPayjoinClient payjoinClient, TransactionBuilder builder, PSBT psbt, HdPubKey changeHdPubKey) { try { Logger.LogInfo($"Negotiating payjoin payment with `{payjoinClient.PaymentUrl}`."); psbt = payjoinClient.RequestPayjoin( psbt, KeyManager.ExtPubKey, new RootedKeyPath(KeyManager.MasterFingerprint.Value, KeyManager.AccountKeyPath), changeHdPubKey, CancellationToken.None).GetAwaiter().GetResult(); // WTF??! builder.SignPSBT(psbt); Logger.LogInfo("Payjoin payment was negotiated successfully."); } catch (HttpRequestException ex) when(ex.InnerException is TorConnectCommandFailedException innerEx) { if (innerEx.Message.Contains("HostUnreachable")) { Logger.LogWarning("Payjoin server is not reachable. Ignoring..."); } // Ignore. } catch (HttpRequestException e) { Logger.LogWarning($"Payjoin server responded with {e.ToTypeMessageString()}. Ignoring..."); } catch (PayjoinException e) { Logger.LogWarning($"Payjoin server responded with {e.Message}. Ignoring..."); } return(psbt); }
private IActionResult FilePSBT(PSBT psbt, string fileName) { return(File(psbt.ToBytes(), "application/octet-stream", fileName)); }
public static async Task <PSBT> SignPsbtWithoutInputTxsAsync(HwiClient client, HDFingerprint value, PSBT psbt, CancellationToken token) { // Ledger Nano S hackfix https://github.com/MetacoSA/NBitcoin/pull/888 var noinputtx = psbt.Clone(); foreach (var input in noinputtx.Inputs) { input.NonWitnessUtxo = null; } return(await client.SignTxAsync(value, noinputtx, token).ConfigureAwait(false)); }
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}"); JObject json = jtok as JObject; var signedPsbtString = json.Value <string>("psbt"); var signedPsbt = PSBT.Parse(signedPsbtString, Network); signedPsbt.Finalize(); return(signedPsbt); }
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)); CanSerializeInJsonCore(new KeyPath("1/2")); CanSerializeInJsonCore(RootedKeyPath.Parse("7b09d780/0'/0'/2'")); CanSerializeInJsonCore(Network.Main); CanSerializeInJsonCore(new uint256(RandomUtils.GetBytes(32))); CanSerializeInJsonCore(new uint160(RandomUtils.GetBytes(20))); CanSerializeInJsonCore(new AssetId(k.PubKey)); CanSerializeInJsonCore(k.PubKey.ScriptPubKey); CanSerializeInJsonCore(new Key().PubKey.WitHash.GetAddress(Network.Main)); CanSerializeInJsonCore(new Key().PubKey.WitHash.ScriptPubKey.GetWitScriptAddress(Network.Main)); var sig = k.Sign(new uint256(RandomUtils.GetBytes(32))); CanSerializeInJsonCore(sig); CanSerializeInJsonCore(new TransactionSignature(sig, SigHash.All)); CanSerializeInJsonCore(k.PubKey.Hash); CanSerializeInJsonCore(k.PubKey.ScriptPubKey.Hash); CanSerializeInJsonCore(k.PubKey.WitHash); CanSerializeInJsonCore(k); CanSerializeInJsonCore(k.PubKey); CanSerializeInJsonCore(new WitScript(new Script(Op.GetPushOp(sig.ToDER()), Op.GetPushOp(sig.ToDER())))); CanSerializeInJsonCore(new LockTime(1)); CanSerializeInJsonCore(new LockTime(130), out var str); Assert.Equal("130", str); CanSerializeInJsonCore(new LockTime(DateTime.UtcNow)); CanSerializeInJsonCore(new FeeRate(Money.Satoshis(1), 1000)); CanSerializeInJsonCore(new FeeRate(Money.Satoshis(1000), 1000)); CanSerializeInJsonCore(new FeeRate(0.5m)); CanSerializeInJsonCore(new HDFingerprint(0x0a), out str); Assert.Equal("\"0a000000\"", str); var print = Serializer.ToObject <HDFingerprint>("\"0a000000\""); var print2 = Serializer.ToObject <HDFingerprint>("10"); Assert.Equal(print, print2); var printn = Serializer.ToObject <HDFingerprint?>("\"0a000000\""); var print2n = Serializer.ToObject <HDFingerprint?>("10"); Assert.Equal(printn, print2n); Assert.Null(Serializer.ToObject <HDFingerprint?>("")); var psbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", Network.Main); var psbtJson = Serializer.ToString(psbt, Network.Main); var psbt2 = Serializer.ToObject <PSBT>(psbtJson, Network.Main); Assert.Equal(psbt, psbt2); var expectedOutpoint = OutPoint.Parse("44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000"); var actualOutpoint = Serializer.ToObject <OutPoint>("\"44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000\"", Network.Main); Assert.Equal(expectedOutpoint, actualOutpoint); actualOutpoint = Serializer.ToObject <OutPoint>("\"7c02d7d6923ab5e9bbdadf7cf6873a5454ae5aa86d15308ed8d68840a79cf644-1\"", Network.Main); Assert.Equal(expectedOutpoint, actualOutpoint); CanSerializeInJsonCore(expectedOutpoint, out str); Assert.Equal("\"44f69ca74088d6d88e30156da85aae54543a87f67cdfdabbe9b53a92d6d7027c01000000\"", str); }
public 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 }); }
private async Task <BtcTxnSignature> getBtcTxnSignaturesAsync( ECurrency currency, string address, string amount, string fee, string target, string reserve, string reservedFundsRedeemScript, string privateKey ) { GluwaClient client = new GluwaClient(mEnv); Result <BalanceResponse, ErrorResponse> getUnspentOutput = await client.GetBalanceAsync(currency, address, true); List <UnspentOutput> unspentOutputs = getUnspentOutput.Data.UnspentOutputs.OrderByDescending(u => u.Amount).ToList(); Money amountValue = Money.Parse(amount); Money feeValue = Money.Parse(fee); Money reserveAmount = amountValue + feeValue; Money totalRequiredAmountMoney = reserveAmount + feeValue; BigInteger totalRequiredAmount = new BigInteger(totalRequiredAmountMoney.Satoshi); BitcoinAddress sourceAddress = BitcoinAddress.Create(address, mEnv.Network); BitcoinAddress targetAddress = BitcoinAddress.Create(target, mEnv.Network); BitcoinAddress reserveAddress = BitcoinAddress.Create(reserve, mEnv.Network); BitcoinSecret secret = new BitcoinSecret(privateKey, mEnv.Network); List <UnspentOutput> usingUnspentOutputs = new List <UnspentOutput>(); BigInteger unspentOutputTotalAmount = BigInteger.Zero; for (int i = 0; i < unspentOutputs.Count; i++) { if (unspentOutputTotalAmount < totalRequiredAmount && i >= MAX_UNSPENTOUTPUTS_COUNT) { throw new InvalidOperationException($"Could not find up to {MAX_UNSPENTOUTPUTS_COUNT} BTC unspent outputs that can cover the amount and fee."); } if (unspentOutputTotalAmount >= totalRequiredAmount) { break; } usingUnspentOutputs.Add(unspentOutputs[i]); Money sumAmount = Money.Parse(unspentOutputs[i].Amount); unspentOutputTotalAmount += new BigInteger(sumAmount.Satoshi); } List <Coin> coins = new List <Coin>(); for (int i = 0; i < usingUnspentOutputs.Count; i++) { coins.Add(new Coin( fromTxHash: new uint256(usingUnspentOutputs[i].TxHash), fromOutputIndex: (uint)usingUnspentOutputs[i].Index, amount: usingUnspentOutputs[i].Amount, scriptPubKey: Script.FromHex(sourceAddress.ScriptPubKey.ToHex()) )); } TransactionBuilder builder = mEnv.Network.CreateTransactionBuilder(); NBitcoin.Transaction reserveTxSignature = builder .AddKeys(secret) .AddCoins(coins) .Send(reserveAddress, reserveAmount) .SetChange(sourceAddress) .SendFees(fee) .BuildTransaction(true); IEnumerable <Coin> reserveTxCoins = reserveTxSignature.Outputs.AsCoins(); Coin reserveTxCoin = reserveTxCoins.First( c => c.TxOut.ScriptPubKey.GetDestinationAddress(mEnv.Network) == reserveAddress); Script reservedFundsRedeemScriptValue = new Script(reservedFundsRedeemScript); ScriptCoin reservedCoin = new ScriptCoin(reserveTxCoin, reservedFundsRedeemScriptValue); builder = mEnv.Network.CreateTransactionBuilder(); PSBT executePsbt = builder .AddKeys(secret) .AddCoins(reservedCoin) .Send(targetAddress, amount) .SendFees(feeValue) .SetChange(reserveAddress) .BuildPSBT(true); builder = mEnv.Network.CreateTransactionBuilder(); PSBT reclaimPsbt = builder .AddKeys(secret) .AddCoins(reservedCoin) .Send(sourceAddress, amount) .SendFees(feeValue) .SetChange(reserveAddress) .BuildPSBT(true); BtcTxnSignature bTCTxnSignature = new BtcTxnSignature() { ReserveTxnSignature = reserveTxSignature.ToHex(), ExecuteTxnSignature = executePsbt.ToHex(), ReclaimTxnSignature = reclaimPsbt.ToHex() }; return(bTCTxnSignature); }
public WalletProcessPSBTResponse WalletProcessPSBT(PSBT psbt, bool sign = true, SigHash hashType = SigHash.All, bool bip32derivs = false) => WalletProcessPSBTAsync(psbt, sign, hashType, bip32derivs).GetAwaiter().GetResult();
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.Code is HwiErrorCode.DeviceNotInitialized) { await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken); return(true); } if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPinSent is true) && pin is null // Trezor T always show the pin on screen && (deviceEntry.Model != HardwareWalletModels.Trezor_T || deviceEntry.Model != HardwareWalletModels.Trezor_T_Simulator)) { await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); return(true); } if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || 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> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); CreatePSBTSuggestions suggestions = null; if (!(request.DisableFingerprintRandomization is true) && fingerprintService.GetDistribution(network) is FingerprintDistribution distribution) { suggestions ??= new CreatePSBTSuggestions(); var known = new List <(Fingerprint feature, bool value)>(); if (request.RBF is bool rbf) { known.Add((Fingerprint.RBF, rbf)); } if (request.DiscourageFeeSniping is bool feeSnipping) { known.Add((Fingerprint.FeeSniping, feeSnipping)); } if (request.LockTime is LockTime l) { if (l == LockTime.Zero) { known.Add((Fingerprint.TimelockZero, true)); } } if (request.Version is uint version) { if (version == 1) { known.Add((Fingerprint.V1, true)); } if (version == 2) { known.Add((Fingerprint.V2, true)); } } known.Add((Fingerprint.SpendFromMixed, false)); known.Add((Fingerprint.SequenceMixed, false)); if (strategy is DirectDerivationStrategy direct) { if (direct.Segwit) { known.Add((Fingerprint.SpendFromP2WPKH, true)); } else { known.Add((Fingerprint.SpendFromP2PKH, true)); } } else { // TODO: What if multisig? For now we consider it p2wpkh known.Add((Fingerprint.SpendFromP2SHP2WPKH, true)); } Fingerprint fingerprint = distribution.PickFingerprint(txBuilder.ShuffleRandom); try { fingerprint = distribution.KnowingThat(known.ToArray()) .PickFingerprint(txBuilder.ShuffleRandom); } catch (InvalidOperationException) { } request.RBF ??= fingerprint.HasFlag(Fingerprint.RBF); request.DiscourageFeeSniping ??= fingerprint.HasFlag(Fingerprint.FeeSniping); if (request.LockTime is null && fingerprint.HasFlag(Fingerprint.TimelockZero)) { request.LockTime = new LockTime(0); } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V1)) { request.Version = 1; } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V2)) { request.Version = 2; } suggestions.ShouldEnforceLowR = fingerprint.HasFlag(Fingerprint.LowR); } var waiter = Waiters.GetWaiter(network); if (waiter.NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = !(request.RBF is false); if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); } // Discourage fee sniping. // // For a large miner the value of the transactions in the best block and // the mempool can exceed the cost of deliberately attempting to mine two // blocks to orphan the current best block. By setting nLockTime such that // only the next block can include the transaction, we discourage this // practice as the height restricted and limited blocksize gives miners // considering fee sniping fewer options for pulling off this attack. // // A simple way to think about this is from the wallet's point of view we // always want the blockchain to move forward. By setting nLockTime this // way we're basically making the statement that we only want this // transaction to appear in the next block; we don't want to potentially // encourage reorgs by allowing transactions to appear at lower heights // than the next block in forks of the best chain. // // Of course, the subsidy is high enough, and transaction volume low // enough, that fee sniping isn't a problem yet, but by implementing a fix // now we ensure code won't be written that makes assumptions about // nLockTime that preclude a fix later. else if (!(request.DiscourageFeeSniping is false)) { if (waiter.State is BitcoinDWaiterState.Ready) { int blockHeight = ChainProvider.GetChain(network).Height; // Secondly occasionally randomly pick a nLockTime even further back, so // that transactions that are delayed after signing for whatever reason, // e.g. high-latency mix networks and some CoinJoin implementations, have // better privacy. if (txBuilder.ShuffleRandom.Next(0, 10) == 0) { blockHeight = Math.Max(0, blockHeight - txBuilder.ShuffleRandom.Next(0, 100)); } txBuilder.SetLockTime(new LockTime(blockHeight)); } else { txBuilder.SetLockTime(new LockTime(0)); } } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.MinValue != null) { availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= (Money)c.Value.Value).ToDictionary(o => o.Key, o => o.Value); } ICoin[] coins = null; if (strategy.GetDerivation().Redeem != null) { // We need to add the redeem script to the coins var hdKeys = strategy.AsHDRedeemScriptPubKey().AsHDKeyCache(); var arr = availableCoinsByOutpoint.Values.ToArray(); coins = new ICoin[arr.Length]; // Can be very intense CPU wise Parallel.For(0, coins.Length, i => { coins[i] = ((Coin)arr[i].AsCoin()).ToScriptCoin(hdKeys.Derive(arr[i].KeyPath).ScriptPubKey); }); } else { coins = availableCoinsByOutpoint.Values.Select(v => v.AsCoin()).ToArray(); } txBuilder.AddCoins(coins); foreach (var dest in request.Destinations) { if (dest.SweepAll) { try { txBuilder.SendAll(dest.Destination); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any.")); } } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { try { txBuilder.SubtractFees(); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it")); } } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; if (request.ExplicitChangeAddress == null) { var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else { // The provided explicit change might have a known keyPath, let's change for it KeyPath keyPath = null; var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath; } change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { change = (derivation.ScriptPubKey, derivation.KeyPath); txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } txBuilder.SetSigningOptions(SigHash.All); psbt = txBuilder.CreatePSBTFrom(tx, false); var update = new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths, AlwaysIncludeNonWitnessUTXO = request.AlwaysIncludeNonWitnessUTXO, IncludeGlobalXPub = request.IncludeGlobalXPub }; await UpdatePSBTCore(update, network); var resp = new CreatePSBTResponse() { PSBT = update.PSBT, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null, Suggestions = suggestions }; return(Json(resp, network.JsonSerializerSettings)); }
private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network) { var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); if (!psbtObject.IsAllFinalized()) { psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? 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 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() { Outputs = new List <WalletSendModel.TransactionOutput>() { new WalletSendModel.TransactionOutput() { DestinationAddress = 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 = 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); await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync <WalletSendLedgerModel>(); var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync <WalletPSBTReadyViewModel>(); Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT); var signedPSBT = unsignedPSBT.Clone(); signedPSBT.SignAll(user.DerivationScheme, user.ExtKey); vmPSBT.PSBT = signedPSBT.ToBase64(); var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").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); 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> 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(nameof(WalletPSBTReady), vm)); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.SetErrors(errors); return(View(nameof(WalletPSBTReady), 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(nameof(WalletPSBTReady), vm)); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; return(View(nameof(WalletPSBTReady), vm)); } return(RedirectToWalletTransaction(walletId, transaction)); } else if (command == "analyze-psbt") { return(RedirectToWalletPSBT(psbt)); } else { vm.GlobalError = "Unknown command"; return(View(nameof(WalletPSBTReady), vm)); } }
private IActionResult ViewPSBT <T>(PSBT psbt, IEnumerable <T> errors = null) { return(ViewPSBT(psbt, null, errors?.Select(e => e.ToString()).ToList())); }
public static async Task <PSBT> UpdatePSBT(this ExplorerClientProvider explorerClientProvider, DerivationSchemeSettings derivationSchemeSettings, PSBT psbt) { var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt, DerivationScheme = derivationSchemeSettings.AccountDerivation }); if (result == null) { return(null); } derivationSchemeSettings.RebaseKeyPaths(result.PSBT); return(result.PSBT); }
private IActionResult ViewPSBT(PSBT psbt, IEnumerable <string> errors = null) { return(ViewPSBT(psbt, null, errors)); }
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 = (await GetDerivationSchemeSettings(walletId)); var signingKeySettings = settings.GetSigningAccountKeySettings(); if (signingKeySettings.RootFingerprint is null) { signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint(); } RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath(); // The user gave the root key, let's try to rebase the PSBT, and derive the account private key if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) { psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath); signingKey = extKey.Derive(rootedKeyPath.KeyPath); } // The user maybe gave the account key, let's try to sign with it else { signingKey = extKey; } var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath); if (balanceChange == Money.Zero) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed 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 abstract Task <PSBT> SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken);
public async Task <IActionResult> Submit(string cryptoCode, long?maxadditionalfeecontribution, int?additionalfeeoutputindex, decimal minfeerate = -1.0m, bool disableoutputsubstitution = false, int v = 1) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(NotFound()); } if (v != 1) { return(BadRequest(new JObject { new JProperty("errorCode", "version-unsupported"), new JProperty("supported", new JArray(1)), new JProperty("message", "This version of payjoin is not supported.") })); } await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository); ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug) { ctx.Logs.Write($"Payjoin error: {debug}", InvoiceEventData.EventSeverity.Error); return(StatusCode(httpCode, CreatePayjoinError(err, debug))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError("payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError("missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } FeeRate originalFeeRate = null; bool psbtFormat = true; if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT should be finalized"))); } ctx.OriginalTransaction = psbt.ExtractTransaction(); } // BTCPay Server implementation support a transaction instead of PSBT else { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt"))); } ctx.OriginalTransaction = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } FeeRate senderMinFeeRate = minfeerate >= 0.0m ? new FeeRate(minfeerate) : null; Money allowedSenderFeeContribution = Money.Satoshis(maxadditionalfeecontribution is long t && t >= 0 ? t : 0); var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true); if (!mempool.Success) { ctx.DoNotBroadcast(); return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var enforcedLowR = ctx.OriginalTransaction.Inputs.All(IsLowR); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; KeyPath paymentAddressIndex = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; WalletId walletId = null; foreach (var output in psbt.Outputs) { var walletReceiveMatch = _walletReceiveService.GetByScriptPubKey(network.CryptoCode, output.ScriptPubKey); if (walletReceiveMatch is null) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice .GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); walletId = new WalletId(invoice.StoreId, network.CryptoCode.ToUpperInvariant()); } else { var store = await _storeRepository.FindStore(walletReceiveMatch.Item1.StoreId); derivationSchemeSettings = store.GetDerivationSchemeSettings(_btcPayNetworkProvider, walletReceiveMatch.Item1.CryptoCode); walletId = walletReceiveMatch.Item1; } if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (receiverInputsType == ScriptPubKeyType.Legacy) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin")); } if (sendersInputType is ScriptPubKeyType t1 && t1 != receiverInputsType) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type")); } if (walletReceiveMatch is null) { var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); paymentAddressIndex = paymentDetails.KeyPath; if (invoice.GetAllBitcoinPaymentData(false).Any()) { ctx.DoNotBroadcast(); return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } } else { paidSomething = true; due = Money.Zero; paymentAddress = walletReceiveMatch.Item2.Address; paymentAddressIndex = walletReceiveMatch.Item2.KeyPath; } if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) { // We do not broadcast, since we might double spend a delayed transaction of a previous payjoin ctx.DoNotBroadcast(); return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction")); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC), psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO) { selectedUTXOs.Add(utxo.Outpoint, utxo); } ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray(); originalPaymentOutput = output; break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError("invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError("invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin")); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed")); } Money contributedAmount = Money.Zero; var newTx = ctx.OriginalTransaction.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); TxOut feeOutput = additionalfeeoutputindex is int feeOutputIndex && maxadditionalfeecontribution is long v3 && v3 >= 0 && feeOutputIndex >= 0 && feeOutputIndex < newTx.Outputs.Count && !isOurOutput.Contains(newTx.Outputs[feeOutputIndex]) ? newTx.Outputs[feeOutputIndex] : null; int senderInputCount = newTx.Inputs.Count; foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; var newInput = newTx.Inputs.Add(selectedUTXO.Outpoint); newInput.Sequence = newTx.Inputs[(int)(RandomUtils.GetUInt32() % senderInputCount)].Sequence; } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); var coins = psbt.Inputs.Select(i => i.GetSignableCoin()) .Concat(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))).ToArray(); txBuilder.AddCoins(coins); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero && !invoice.IsUnsetTopUp(); i++) { if (disableoutputsubstitution) { break; } if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change if (feeOutput != null) { var outputContribution = Money.Min(additionalFee, feeOutput.Value); outputContribution = Money.Min(outputContribution, feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee)); outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution); feeOutput.Value -= outputContribution; additionalFee -= outputContribution; allowedSenderFeeContribution -= outputContribution; } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee)) { return(CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs")); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.PSBT.Settings.SigningOptions = new SigningOptions() { EnforceLowR = enforcedLowR }; signedInput.Sign(privateKey); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index), ctx.OriginalTransaction.RBF, paymentAddressIndex); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; if (invoice != null) { var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The original transaction has already been accounted"))); } _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = walletId, TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => new KeyValuePair <uint256, List <(string color, Label label)> >(utxo.Key, new List <(string color, Label label)>() { UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice?.Id) })) .ToDictionary(pair => pair.Key, pair => pair.Value) });
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).GetUnspentCoins(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { txBuilder.SendAll(dest.Destination); } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { txBuilder.SubtractFees(); } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; // We first build the transaction with a change which keep the length of the expected change scriptPubKey // This allow us to detect if there is a change later in the constructed transaction. // This defend against bug which can happen if one of the destination is the same as the expected change // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress if (request.ExplicitChangeAddress == null) { // The dummyScriptPubKey is necessary to know the size of the change var dummyScriptPubKey = utxos.FirstOrDefault()?.ScriptPubKey ?? strategy.GetDerivation(0).ScriptPubKey; change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null); } else { change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } if (hasChange) // We need to reserve an address, so we need to build again the psbt { if (request.ExplicitChangeAddress == null) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress); change = (derivation.ScriptPubKey, derivation.KeyPath); } else { change = (request.ExplicitChangeAddress.ScriptPubKey, null); } txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); // Maybe it is a change that we know about, let's search in the DB if (hasChange && change.KeyPath == null) { var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy); if (ki != null) { change = (change.ScriptPubKey, kis.First().KeyPath); } } } await UpdatePSBTCore(new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths }, network); var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> VaultBridgeConnection(string cryptoCode = null, [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId = null) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } cryptoCode = cryptoCode ?? walletId.CryptoCode; using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10))) { var cancellationToken = cts.Token; var network = Networks.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(NotFound()); } var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); var hwi = new Hwi.HwiClient(network.NBitcoinNetwork) { Transport = new HwiWebSocketTransport(websocket) }; Hwi.HwiDeviceClient device = null; HwiEnumerateEntry deviceEntry = null; HDFingerprint? fingerprint = null; string password = null; var websocketHelper = new WebSocketHelper(websocket); async Task <bool> RequireDeviceUnlocking() { if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); return(true); } if (deviceEntry.Code is HwiErrorCode.DeviceNotInitialized) { await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken); return(true); } if (deviceEntry.Code is HwiErrorCode.DeviceNotReady) { if (IsTrezorT(deviceEntry)) { await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); return(true); } else if (deviceEntry.NeedsPinSent is true) { await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); return(true); } else if (deviceEntry.NeedsPassphraseSent is true && password is null) { await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); return(true); } } return(false); } JObject o = null; try { while (true) { var command = await websocketHelper.NextMessageAsync(cancellationToken); switch (command) { case "set-passphrase": device.Password = await websocketHelper.NextMessageAsync(cancellationToken); password = device.Password; break; case "ask-sign": if (await RequireDeviceUnlocking()) { continue; } if (walletId == null) { await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken); continue; } if (fingerprint is null) { fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint; } await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken); o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings); if (!authorization.Succeeded) { await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken); continue; } var psbt = PSBT.Parse(o["psbt"].Value <string>(), network.NBitcoinNetwork); var derivationSettings = GetDerivationSchemeSettings(walletId); derivationSettings.RebaseKeyPaths(psbt); var signing = derivationSettings.GetSigningAccountKeySettings(); if (signing.GetRootedKeyPath()?.MasterFingerprint != fingerprint) { await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken); continue; } var signableInputs = psbt.Inputs .SelectMany(i => i.HDKeyPaths) .Where(i => i.Value.MasterFingerprint == fingerprint) .ToArray(); if (signableInputs.Length > 0) { var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath)).GetPublicKey(); if (actualPubKey != signableInputs[0].Key) { await websocketHelper.Send("{ \"error\": \"wrong-keypath\"}", cancellationToken); continue; } } try { psbt = await device.SignPSBTAsync(psbt, cancellationToken); } catch (Hwi.HwiException) { await websocketHelper.Send("{ \"error\": \"user-reject\"}", cancellationToken); continue; } o = new JObject(); o.Add("psbt", psbt.ToBase64()); await websocketHelper.Send(o.ToString(), cancellationToken); break; case "display-address": if (await RequireDeviceUnlocking()) { continue; } var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken); await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken); break; case "ask-pin": if (device == null) { await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); continue; } try { await device.PromptPinAsync(cancellationToken); } catch (HwiException ex) when(ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) { await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken); continue; } await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken); var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture); if (await device.SendPinAsync(pin, cancellationToken)) { goto askdevice; } else { await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken); continue; } case "ask-xpub": if (await RequireDeviceUnlocking()) { continue; } await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken); var askedXpub = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); var addressType = askedXpub["addressType"].Value <string>(); var accountNumber = askedXpub["accountNumber"].Value <int>(); JObject result = new JObject(); var factory = network.NBXplorerNetwork.DerivationStrategyFactory; if (fingerprint is null) { fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint; } result["fingerprint"] = fingerprint.Value.ToString(); DerivationStrategyBase strategy = null; KeyPath keyPath = null; BitcoinExtPubKey xpub = null; if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy") { await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken); continue; } if (addressType == "segwit") { keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(accountNumber, true); xpub = await device.GetXPubAsync(keyPath); strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.Segwit }); } else if (addressType == "segwitWrapped") { keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(accountNumber, true); xpub = await device.GetXPubAsync(keyPath); strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH }); } else if (addressType == "legacy") { keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(accountNumber, true); xpub = await device.GetXPubAsync(keyPath); strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.Legacy }); } else { await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken); continue; } result.Add(new JProperty("strategy", strategy.ToString())); result.Add(new JProperty("accountKey", xpub.ToString())); result.Add(new JProperty("keyPath", keyPath.ToString())); await websocketHelper.Send(result.ToString(), cancellationToken); break; case "ask-passphrase": if (command == "ask-passphrase") { if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); continue; } // The make the trezor T ask for password await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); } goto askdevice; case "ask-device": askdevice: password = null; deviceEntry = null; device = null; var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); deviceEntry = entries.FirstOrDefault(); if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken); continue; } device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint); fingerprint = device.Fingerprint; JObject json = new JObject(); json.Add("model", device.Model.ToString()); json.Add("fingerprint", device.Fingerprint?.ToString()); await websocketHelper.Send(json.ToString(), cancellationToken); break; } } } catch (FormatException ex) { JObject obj = new JObject(); obj.Add("error", "invalid-network"); obj.Add("details", ex.ToString()); try { await websocketHelper.Send(obj.ToString(), cancellationToken); } catch { } } catch (Exception ex) { JObject obj = new JObject(); obj.Add("error", "unknown-error"); obj.Add("message", ex.Message); obj.Add("details", ex.ToString()); try { await websocketHelper.Send(obj.ToString(), cancellationToken); } catch { } } finally { await websocketHelper.DisposeAsync(cancellationToken); } } return(new EmptyResult()); }
public async Task <PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings, PSBT originalTx, CancellationToken cancellationToken) { if (endpoint == null) { throw new ArgumentNullException(nameof(endpoint)); } if (derivationSchemeSettings == null) { throw new ArgumentNullException(nameof(derivationSchemeSettings)); } if (originalTx == null) { throw new ArgumentNullException(nameof(originalTx)); } var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings(); var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath()); 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.IsAllFinalized() && !cloned.TryFinalize(out var errors)) { return(null); } // We make sure we don't send unnecessary information to the receiver foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized())) { finalized.ClearForFinalize(); } foreach (var output in cloned.Outputs) { output.HDKeyPaths.Clear(); } cloned.GlobalXPubs.Clear(); var bpuresponse = await _httpClient.PostAsync(endpoint, new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken); if (!bpuresponse.IsSuccessStatusCode) { var errorStr = await bpuresponse.Content.ReadAsStringAsync(); 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 hex = await bpuresponse.Content.ReadAsStringAsync(); var newPSBT = PSBT.Parse(hex, 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"); } //////////// newPSBT = await _explorerClientProvider.UpdatePSBT(derivationSchemeSettings, newPSBT); if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0) { throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})"); } // We make sure we don't sign things what should not be signed foreach (var finalized in newPSBT.Inputs.Where(i => i.IsFinalized())) { finalized.ClearForFinalize(); } // Make sure only the only our output have any information foreach (var output in newPSBT.Outputs) { output.HDKeyPaths.Clear(); foreach (var originalOutput in originalTx.Outputs) { if (output.ScriptPubKey == originalOutput.ScriptPubKey) { output.UpdateFrom(originalOutput); } } } // Making sure that our inputs are finalized, and that some of our inputs have not been added var newGlobalTx = newPSBT.GetGlobalTransaction(); int ourInputCount = 0; if (newGlobalTx.Version != oldGlobalTx.Version) { throw new PayjoinSenderException("The version field of the transaction has been modified"); } if (newGlobalTx.LockTime != oldGlobalTx.LockTime) { throw new PayjoinSenderException("The LockTime field of the transaction has been modified"); } foreach (var input in newPSBT.Inputs.CoinsFor(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath())) { if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is PSBTInput ourInput) { ourInputCount++; if (input.IsFinalized()) { throw new PayjoinSenderException("A PSBT input from us should not be finalized"); } if (newGlobalTx.Inputs[input.Index].Sequence != newGlobalTx.Inputs[ourInput.Index].Sequence) { throw new PayjoinSenderException("The sequence of one of our input has been modified"); } } else { throw new PayjoinSenderException( "The payjoin receiver added some of our own inputs in the proposal"); } } // Making sure that the receiver's inputs are finalized and P2PWKH foreach (var input in newPSBT.Inputs) { if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is null) { if (!input.IsFinalized()) { throw new PayjoinSenderException("The payjoin receiver included a non finalized input"); } if (!(input.FinalScriptWitness.GetSigner() is WitKeyId)) { throw new PayjoinSenderException("The payjoin receiver included an input that is not P2PWKH"); } } } if (ourInputCount < originalTx.Inputs.Count) { throw new PayjoinSenderException("The payjoin receiver removed some of our inputs"); } // We limit the number of inputs the receiver can add var addedInputs = newPSBT.Inputs.Count - originalTx.Inputs.Count; if (addedInputs == 0) { throw new PayjoinSenderException("The payjoin receiver did not added any input"); } var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath()); if (sentAfter > sentBefore) { var overPaying = sentAfter - sentBefore; if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize)) { throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly"); } var additionalFee = newPSBT.GetFee() - originalFee; if (overPaying > additionalFee) { throw new PayjoinSenderException("The payjoin receiver is sending more money to himself"); } if (overPaying > originalFee) { throw new PayjoinSenderException("The payjoin receiver is making us pay more than twice the original fee"); } // Let's check the difference is only for the fee and that feerate // did not changed that much var expectedFee = originalFeeRate.GetFee(newVirtualSize); // Signing precisely is hard science, give some breathing room for error. expectedFee += originalFeeRate.GetFee(newPSBT.Inputs.Count * 2); if (overPaying > (expectedFee - originalFee)) { throw new PayjoinSenderException("The payjoin receiver increased the fee rate we are paying too much"); } } return(newPSBT); }
public BroadcastTransactionViewModel( BitcoinStore store, Network network, TransactionBroadcaster broadcaster, SmartTransaction transaction) { Title = "Broadcast Transaction"; var nullMoney = new Money(-1L); var nullOutput = new TxOut(nullMoney, Script.Empty); var psbt = PSBT.FromTransaction(transaction.Transaction, network); TxOut GetOutput(OutPoint outpoint) => store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn) ? prevTxn.Transaction.Outputs[outpoint.N] : nullOutput; var inputAddressAmount = psbt.Inputs .Select(x => x.PrevOut) .Select(GetOutput) .ToArray(); var outputAddressAmount = psbt.Outputs .Select(x => x.GetCoin().TxOut) .ToArray(); var psbtTxn = psbt.GetOriginalTransaction(); _transactionId = psbtTxn.GetHash().ToString(); _inputCount = inputAddressAmount.Length; _inputCountString = $" input{TextHelpers.AddSIfPlural(_inputCount)} and "; _outputCount = outputAddressAmount.Length; _outputCountString = $" output{TextHelpers.AddSIfPlural(_outputCount)}."; _totalInputValue = inputAddressAmount.Any(x => x.Value == nullMoney) ? null : inputAddressAmount.Select(x => x.Value).Sum(); _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney) ? null : outputAddressAmount.Select(x => x.Value).Sum(); _networkFee = TotalInputValue is null || TotalOutputValue is null ? null : TotalInputValue - TotalOutputValue; var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy) .Select(x => !x); NextCommand = ReactiveCommand.CreateFromTask( async() => { try { await broadcaster.SendTransactionAsync(transaction); Navigate().To(new SuccessBroadcastTransactionViewModel()); } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync(Title, ex.ToUserFriendlyString(), "It was not possible to broadcast the transaction."); } }, nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
private async Task <PSBT> UpdatePSBT(DerivationSchemeSettings derivationSchemeSettings, PSBT psbt, BTCPayNetwork network) { var result = await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt, DerivationScheme = derivationSchemeSettings.AccountDerivation, }); if (result == null) { return(null); } derivationSchemeSettings.RebaseKeyPaths(result.PSBT); return(result.PSBT); }
public async Task ColdCardKataAsync() { // --- USER INTERACTIONS --- // // Connect and initialize your Coldcard with the following seed phrase: // more maid moon upgrade layer alter marine screen benefit way cover alcohol // Run this test. // signtx request: refuse // signtx request: 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.Coldcard, entry.Model); Assert.True(entry.Fingerprint.HasValue); string devicePath = entry.Path; HardwareWalletModels deviceType = entry.Model; HDFingerprint fingerprint = entry.Fingerprint.Value; // ColdCard doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.WipeAsync(deviceType, devicePath, cts.Token)); // ColdCard doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.SetupAsync(deviceType, devicePath, false, cts.Token)); // ColdCard doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.RestoreAsync(deviceType, devicePath, false, cts.Token)); // ColdCard doesn't support it. await Assert.ThrowsAsync <HwiException>(async() => await client.PromptPinAsync(deviceType, devicePath, cts.Token)); // ColdCard 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: REFUSE var ex = await Assert.ThrowsAsync <HwiException>(async() => await client.SignTxAsync(deviceType, devicePath, Psbt, cts.Token)); Assert.Equal(HwiErrorCode.ActionCanceled, ex.ErrorCode); // USER: CONFIRM 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); // ColdCard just display the address. There is no confirm/refuse action. BitcoinWitPubKeyAddress address1 = await client.DisplayAddressAsync(deviceType, devicePath, keyPath1, cts.Token); 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); }
public async Task <IActionResult> Submit(string cryptoCode) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(BadRequest(CreatePayjoinError(400, "invalid-network", "Incorrect network"))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError(413, "payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError(411, "missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } Transaction originalTx = null; FeeRate originalFeeRate = null; bool psbtFormat = true; if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError(400, "invalid-format", "invalid transaction or psbt"))); } originalTx = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } else { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT should be finalized"))); } originalTx = psbt.ExtractTransaction(); } async Task BroadcastNow() { await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx); } if (originalTx.Inputs.Any(i => !(i.GetSigner() is WitKeyId))) { return(BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support P2WPKH inputs"))); } if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError(400, "need-utxo-information", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(originalTx, true); if (!mempool.Success) { return(BadRequest(CreatePayjoinError(400, "invalid-transaction", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); async Task UnlockUTXOs() { await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray()); } PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); if (derivationSchemeSettings is null) { continue; } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { return(UnprocessableEntity(CreatePayjoinError(422, "already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray())) { return(BadRequest(CreatePayjoinError(400, "inputs-already-used", "Some of those inputs have already been used to make payjoin transaction"))); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); foreach (var utxo in await SelectUTXO(network, utxos, output.Value, psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray())) { selectedUTXOs.Add(utxo.Outpoint, utxo); } originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError(400, "invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError(400, "invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { await BroadcastNow(); return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin for now"))); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin await UnlockUTXOs(); await BroadcastNow(); return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"))); } Money contributedAmount = Money.Zero; var newTx = originalTx.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; newTx.Inputs.Add(selectedUTXO.Outpoint); } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Probably receiving some spare change, let's add an output to make // it looks more like a normal transaction if (newTx.Outputs.Count == 1) { var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change); var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi; var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey); if (fakeChange.IsDust(minRelayTxFee)) { randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee); fakeChange.Value = randomChangeAmount; } if (randomChangeAmount < contributedAmount) { ourNewOutput.Value -= fakeChange.Value; newTx.Outputs.Add(fakeChange); isOurOutput.Add(fakeChange); } } var rand = new Random(); Utils.Shuffle(newTx.Inputs, rand); Utils.Shuffle(newTx.Outputs, rand); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); txBuilder.AddCoins(psbt.Inputs.Select(i => i.GetCoin())); txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin())); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) { if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++) { if (!isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; } } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee) { await UnlockUTXOs(); await BroadcastNow(); return(UnprocessableEntity(CreatePayjoinError(422, "not-enough-money", "Not enough money is sent to pay for the additional payjoin inputs"))); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); signedInput.UpdateFromCoin(selectedUtxo.AsCoin()); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.Sign(privateKey); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index), originalTx.RBF); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { await UnlockUTXOs(); await BroadcastNow(); return(UnprocessableEntity(CreatePayjoinError(422, "already-paid", $"The original transaction has already been accounted"))); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx); _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); if (psbtFormat) { return(Ok(newPsbt.ToBase64())); } else { return(Ok(newTx.ToHex())); } }