public void Setup() { var seed = new ExtKey(); accPath = new KeyPath("87'/0'/0'").ToRootedKeyPath(seed.GetPublicKey().GetHDFingerPrint()); acc = seed.Derive(accPath.KeyPath).AsHDKeyCache(); var coins = Enumerable .Range(0, 1300) .Select(i => new Coin(RandomOutpoint(), new TxOut(Money.Coins(1.0m), acc.Derive(0).Derive((uint)i).GetPublicKey().GetScriptPubKey(ScriptPubKeyType.Segwit)))) .ToArray(); var tx = Transaction.Create(Network.Main); foreach (var c in coins) { tx.Inputs.Add(c.Outpoint); } tx.Outputs.Add(Money.Coins(1299.0m), new Key()); var psbt = PSBT.FromTransaction(tx, Network.Main); psbt.AddCoins(coins); for (int i = 0; i < coins.Length; i++) { psbt.Inputs[i].AddKeyPath(acc.Derive(0).Derive((uint)i).GetPublicKey(), accPath.Derive(0).Derive((uint)i)); } psbtStr = psbt.ToBase64(); psbt.SignAll(acc.AsHDScriptPubKey(ScriptPubKeyType.Segwit), acc, accPath); psbtSignedStr = psbt.ToBase64(); }
public static void WriteTransaction(this InvocationContext ctx, Transaction tx, Coin?fundingCoin, Network network) { if (ctx.ParseResult.ValueForOption <bool>("psbt")) { var psbt = PSBT.FromTransaction(tx, network); 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; } if (fundingCoin is Coin) { psbt.AddCoins(fundingCoin); } ctx.WritePSBT(psbt); } else { if (ctx.ParseResult.ValueForOption <bool>("json")) { ctx.Console.Out.Write(tx.ToString()); } else { ctx.Console.Out.Write(tx.ToHex()); } } }
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; EnableCancel = true; EnableBack = false; this.WhenAnyValue(x => x.IsBusy) .Subscribe(x => EnableCancel = !x); var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy) .Select(x => !x); NextCommand = ReactiveCommand.CreateFromTask( async() => await OnNext(broadcaster, transaction), nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public PSBT CreateSetupPSBT(Network network) { if (FundingInputs is null || PubKeys is null) { throw new InvalidOperationException("Funding inputs or pubkeys are null"); } Transaction tx = network.CreateTransaction(); foreach (var input in FundingInputs) { var c = input.AsCoin(); tx.Inputs.Add(c.Outpoint); } var total = FundingInputs.Select(c => c.AsCoin().Amount).Sum(); tx.Outputs.Add(TotalCollateral, PubKeys.PayoutAddress); tx.Outputs.Add(TotalCollateral - total, ChangeAddress); var psbt = PSBT.FromTransaction(tx, network); foreach (var input in FundingInputs) { var c = input.AsCoin(); var psbtInput = psbt.Inputs.FindIndexedInput(c.Outpoint); psbtInput.NonWitnessUtxo = input.InputTransaction; psbtInput.RedeemScript = input.RedeemScript; if (input.MaxWitnessLength is int) { psbtInput.Unknown.Add(MaxWitnessLengthKey, Utils.ToBytes((uint)input.MaxWitnessLength, true)); } } return(psbt); }
public FundingPSBT Build(Network network) { var fundingScript = GetFundingScript(); var p2wsh = fundingScript.WitHash.ScriptPubKey; Transaction?tx = null; if (transactionOverride is null) { tx = network.CreateTransaction(); tx.Version = 2; tx.LockTime = 0; foreach (var input in Offerer.FundingCoins) { tx.Inputs.Add(input.Outpoint, Script.Empty); } foreach (var input in Acceptor.FundingCoins) { tx.Inputs.Add(input.Outpoint, Script.Empty); } foreach (var input in tx.Inputs) { input.Sequence = 0xffffffff; } tx.Outputs.Add(Offerer.Collateral + Acceptor.Collateral, p2wsh); var totalInput = Offerer.FundingCoins.Select(s => s.Amount).Sum(); if (Offerer.Change is Script change) { tx.Outputs.Add(totalInput - Offerer.Collateral, change); } totalInput = Acceptor.FundingCoins.Select(s => s.Amount).Sum(); if (Acceptor.Change is Script change2) { tx.Outputs.Add(totalInput - Acceptor.Collateral, change2); } var expectedFee = FeeRate.GetFee(700); var parts = expectedFee.Split(2).ToArray(); tx.Outputs[1].Value -= parts[1]; tx.Outputs[2].Value -= parts[1]; var futureFee = FeeRate.GetFee(169); parts = futureFee.Split(2).ToArray(); tx.Outputs[1].Value -= parts[1]; tx.Outputs[2].Value -= parts[1]; tx.Outputs[0].Value += futureFee; } else { tx = transactionOverride; } var psbt = PSBT.FromTransaction(tx, network); psbt.AddCoins(Offerer.FundingCoins); psbt.AddCoins(Acceptor.FundingCoins); return(new FundingPSBT(psbt, new ScriptCoin(tx, 0, fundingScript))); }
public void CanRebaseKeypathInPSBT() { var masterExtkey = new BitcoinExtKey("tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF", Network.TestNet); var masterFP = masterExtkey.GetPublicKey().GetHDFingerPrint(); var accountExtKey = masterExtkey.Derive(new KeyPath("0'/0'/0'")); var accountRootedKeyPath = new KeyPath("0'/0'/0'").ToRootedKeyPath(masterExtkey); uint hardenedFlag = 0x80000000U; retry: Transaction funding = masterExtkey.Network.CreateTransaction(); funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(0 | hardenedFlag).ScriptPubKey); funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(1 | hardenedFlag).ScriptPubKey); Transaction tx = masterExtkey.Network.CreateTransaction(); tx.Version = 2; tx.Outputs.Add(Money.Coins(1.49990000m), new Script(Encoders.Hex.DecodeData("0014d85c2b71d0060b09c9886aeb815e50991dda124d"))); tx.Outputs.Add(Money.Coins(1.00000000m), new Script(Encoders.Hex.DecodeData("001400aea9a2e5f0f876a588df5546e8742d1d87008f"))); tx.Inputs.Add(funding, 0); tx.Inputs.Add(funding, 1); var psbt = PSBT.FromTransaction(tx, Network.TestNet); psbt.AddTransactions(funding); psbt.AddKeyPath(accountExtKey, Tuple.Create(new KeyPath(0 | hardenedFlag), funding.Outputs[0].ScriptPubKey), Tuple.Create(new KeyPath(1 | hardenedFlag), funding.Outputs[1].ScriptPubKey)); Assert.Equal(new KeyPath(0 | hardenedFlag), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].KeyPath); Assert.Equal(new KeyPath(1 | hardenedFlag), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].KeyPath); Assert.Equal(accountExtKey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].MasterFingerprint); Assert.Equal(accountExtKey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].MasterFingerprint); var memento = psbt.Clone(); psbt.GlobalXPubs.Add(accountExtKey.Neuter(), new RootedKeyPath(accountExtKey, new KeyPath())); var rebasingKeyPath = new RootedKeyPath(masterExtkey.GetPublicKey().GetHDFingerPrint(), new KeyPath("0'/0'/0'")); psbt.RebaseKeyPaths(accountExtKey, rebasingKeyPath); Assert.Equal(psbt.GlobalXPubs.Single().Value, rebasingKeyPath); psbt.ToString(); psbt = PSBT.Parse(psbt.ToHex(), psbt.Network); Assert.Equal(psbt.GlobalXPubs.Single().Value, rebasingKeyPath); Assert.Equal(new KeyPath("0'/0'/0'").Derive(0 | hardenedFlag), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].KeyPath); Assert.Equal(new KeyPath("0'/0'/0'").Derive(1 | hardenedFlag), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].KeyPath); Assert.Equal(masterExtkey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].MasterFingerprint); Assert.Equal(masterExtkey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].MasterFingerprint); Assert.NotEqual(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey)); Assert.Equal(psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey), psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey, accountRootedKeyPath)); if (hardenedFlag != 0) // If hardened, we can't get the balance from the account pubkey { Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey.Neuter(), accountRootedKeyPath)); } else { Assert.Equal(psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey), psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey.Neuter(), accountRootedKeyPath)); } Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey.Derive(new KeyPath("0'/0'/1'")), new KeyPath("0'/0'/1'").ToRootedKeyPath(masterFP))); Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey.Neuter())); // Can't derive! if (hardenedFlag != 0) { hardenedFlag = 0; goto retry; } }
public void ShouldCaptureExceptionInFinalization() { var keys = new Key[] { new Key(), new Key(), new Key() }; var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray()); var network = Network.Main; var funds = CreateDummyFunds(network, keys, redeem); var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false); var psbt = PSBT.FromTransaction(tx); psbt.TryFinalize(out var errors); Assert.Equal(6, errors.Length); }
public void ShouldCaptureExceptionInFinalization() { var keys = new Key[] { new Key(), new Key(), new Key() }; var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray()); var network = Network.Main; var funds = CreateDummyFunds(network, keys, redeem); var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false); var psbt = PSBT.FromTransaction(tx, Network.Main); var ex = Assert.Throws<PSBTException>(() => psbt.Finalize()); Assert.Equal(6, ex.Errors.GroupBy(e => e.InputIndex).Count()); }
public Task <string> HandleAsync(string body, CancellationToken cancellationToken) { if (!PSBT.TryParse(body, Network, out var psbt)) { throw new Exception("What the heck are you trying to do?"); } if (!psbt.IsAllFinalized()) { throw new Exception("The PSBT should be finalized"); } // Which coin to use var toUse = WalletManager.GetWallets() .Where(x => x.State == WalletState.Started && !x.KeyManager.IsWatchOnly && !x.KeyManager.IsHardwareWallet) .SelectMany(wallet => wallet.Coins.Select(coin => new { wallet.KeyManager, coin })) .Where(x => x.coin.AnonymitySet >= PrivacyLevelThreshold && !x.coin.Unavailable) .OrderBy(x => x.coin.IsBanned) .ThenBy(x => x.coin.Confirmed) .ThenBy(x => x.coin.Height) .First(); // Fees var originalFeeRate = psbt.GetEstimatedFeeRate(); var paymentTx = psbt.ExtractTransaction(); foreach (var input in paymentTx.Inputs) { input.WitScript = WitScript.Empty; } // Get prv key for signature var serverCoinKey = toUse.KeyManager.GetSecrets("chaincase", toUse.coin.ScriptPubKey).First(); var serverCoin = toUse.coin.GetCoin(); paymentTx.Inputs.Add(serverCoin.Outpoint); var paymentOutput = paymentTx.Outputs.First(); var inputSizeInVBytes = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m); // Get final value paymentOutput.Value += (Money)serverCoin.Amount - originalFeeRate.GetFee(inputSizeInVBytes); var newPsbt = PSBT.FromTransaction(paymentTx, Network.Main); var serverCoinToSign = newPsbt.Inputs.FindIndexedInput(serverCoin.Outpoint); serverCoinToSign.UpdateFromCoin(serverCoin); serverCoinToSign.Sign(serverCoinKey.PrivateKey); serverCoinToSign.FinalizeInput(); NotificationManager.ScheduleNotification("PayJoin Received", "The sender has paid you in a CoinJoin style transaction", 1); return(Task.FromResult(newPsbt.ToHex())); }
public PSBT BuildFundingPSBT() { var psbt = PSBT.FromTransaction(BuildFunding(), this.network); foreach (var coin in Remote?.FundingCoins ?? Array.Empty <Coin>()) { psbt.AddCoins(coin); } foreach (var coin in Us?.FundingCoins ?? Array.Empty <Coin>()) { psbt.AddCoins(coin); } AddFundingSigs(Remote, psbt); AddFundingSigs(Us, psbt); return(psbt); }
private void ProcessTransaction(SmartTransaction transaction, Network network) { var nullMoney = new Money(-1L); var nullOutput = new TxOut(nullMoney, Script.Empty); var psbt = PSBT.FromTransaction(transaction.Transaction, network); TxOut GetOutput(OutPoint outpoint) => Services.BitcoinStore.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; var totalInputValue = inputAddressAmount.Any(x => x.Value == nullMoney) ? null : inputAddressAmount.Select(x => x.Value).Sum(); InputAmountString = totalInputValue is null ? "Unknown" : $"{totalInputValue.ToFormattedString()} BTC"; OutputCount = outputAddressAmount.Length; var totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney) ? null : outputAddressAmount.Select(x => x.Value).Sum(); OutputAmountString = totalOutputValue is null ? "Unknown" : $"{totalOutputValue.ToFormattedString()} BTC"; var networkFee = totalInputValue is null || totalOutputValue is null ? null : totalInputValue - totalOutputValue; FeeString = networkFee?.ToFeeDisplayUnitString() ?? "Unknown"; }
public void ShouldPreserveOriginalTxPropertyAsPossible() { var keys = new Key[] { new Key(), new Key(), new Key() }; var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray()); var network = Network.Main; var funds = CreateDummyFunds(network, keys, redeem); // 1. without signature nor scripts. var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false); // 2. with (unsigned) scriptSig and witness. tx = CreateTxToSpendFunds(funds, keys, redeem, true, false); var psbt = PSBT.FromTransaction(tx).AddCoins(funds); Assert.Null(psbt.Inputs[0].FinalScriptSig); // it is not finalized since it is not signed Assert.Null(psbt.Inputs[1].FinalScriptWitness); // This too Assert.NotNull(psbt.Inputs[2].RedeemScript); // But it holds redeem script. Assert.NotNull(psbt.Inputs[3].WitnessScript); // And witness script. Assert.NotNull(psbt.Inputs[5].WitnessScript); // even in p2sh-nested-p2wsh Assert.NotNull(psbt.Inputs[5].RedeemScript); // 3. with finalized scriptSig and witness tx = CreateTxToSpendFunds(funds, keys, redeem, true, true); psbt = PSBT.FromTransaction(tx) .AddTransactions(funds) .Finalize(); Assert.Equal(tx.ToHex(), psbt.GetOriginalTransaction().ToHex()); // Check that we can still get the original tx Assert.NotNull(psbt.Inputs[0].FinalScriptSig); // it should be finalized Assert.NotNull(psbt.Inputs[0].NonWitnessUtxo); Assert.NotNull(psbt.Inputs[1].FinalScriptWitness); // p2wpkh too Assert.NotNull(psbt.Inputs[1].WitnessUtxo); Assert.Null(psbt.Inputs[2].RedeemScript); Assert.Null(psbt.Inputs[3].WitnessScript); Assert.NotNull(psbt.Inputs[4].FinalScriptSig); // Same principle holds for p2sh-nested version. Assert.NotNull(psbt.Inputs[4].FinalScriptWitness); Assert.Null(psbt.Inputs[5].WitnessScript); Assert.Null(psbt.Inputs[5].RedeemScript); Assert.Empty(psbt.Inputs[2].PartialSigs); // It can not hold partial_sigs Assert.Empty(psbt.Inputs[3].PartialSigs); // Even in p2wsh Assert.Empty(psbt.Inputs[5].PartialSigs); // And p2sh-p2wsh }
/// <summary> /// This is slow, provably because `Add*` methods will iterate over inputs. /// </summary> /// <param name="network"></param> /// <returns></returns> public static Gen <PSBT> SanePSBT(Network network) => from inputN in Gen.Choose(0, 8) from scripts in Gen.ListOf(inputN, ScriptGenerator.RandomScriptSig()) from txOuts in Gen.Sequence(scripts.Select(sc => OutputFromRedeem(sc))) from prevN in Gen.Choose(0, 5) from prevTxs in Gen.Sequence(txOuts.Select(o => TXFromOutput(o, network, prevN))) let txins = prevTxs.Select(tx => new TxIn(new OutPoint(tx.GetHash(), prevN))) from locktime in PrimitiveGenerator.UInt32() let tx = LegacyTransactionGenerators.ComposeTx(network.CreateTransaction(), txins.ToList(), txOuts.ToList(), locktime) from TxsToAdd in Gen.SubListOf(prevTxs) from CoinsToAdd in Gen.SubListOf(prevTxs.SelectMany(tx => tx.Outputs.AsCoins())) from scriptsToAdd in Gen.SubListOf <Script>(scripts) let psbt = PSBT .FromTransaction(tx) .AddTransactions(prevTxs.ToArray()) .AddCoins(CoinsToAdd.ToArray()) .TryAddScript(scriptsToAdd.ToArray()) select psbt;
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 void ShouldPassTheLongestTestInBIP174() { JObject testcase = (JObject)testdata["final"]; var network = Network.TestNet; var master = ExtKey.Parse((string)testcase["master"], network); var masterFP = BitConverter.ToUInt32(master.PrivateKey.PubKey.Hash.ToBytes().SafeSubarray(0, 4), 0); var tx = network.CreateTransaction(); tx.Version = 2; var scriptPubKey1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out1"]["script"])); var money1 = Money.Coins((decimal)testcase["out1"]["value"]); var scriptPubKey2 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out2"]["script"])); var money2 = Money.Coins((decimal)testcase["out2"]["value"]); tx.Outputs.Add(new TxOut(value: money1, scriptPubKey: scriptPubKey1)); tx.Outputs.Add(new TxOut(value: money2, scriptPubKey: scriptPubKey2)); tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in1"]["txid"]), (uint)testcase["in1"]["index"])); tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in2"]["txid"]), (uint)testcase["in2"]["index"])); var expected = PSBT.Parse((string)testcase["psbt1"], Network.Main); var psbt = PSBT.FromTransaction(tx); Assert.Equal(expected, psbt, ComparerInstance); var prevtx1 = Transaction.Parse((string)testcase["prevtx1"], network); var prevtx2 = Transaction.Parse((string)testcase["prevtx2"], network); psbt.AddTransactions(prevtx1, prevtx2); var redeem1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem1"])); var redeem2 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem2"])); var witness_script1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["witness1"])); foreach (var sc in new Script[] { redeem1, redeem2, witness_script1 }) { psbt.AddScript(sc); } for (int i = 0; i < 6; i++) { var pk = testcase[$"pubkey{i}"]; var pubkey = new PubKey((string)pk["hex"]); var path = KeyPath.Parse((string)pk["path"]); psbt.AddKeyPath(pubkey, Tuple.Create(masterFP, path)); } expected = PSBT.Parse((string)testcase["psbt2"], Network.Main); Assert.Equal(expected, psbt, ComparerInstance); foreach (var psbtin in psbt.Inputs) { psbtin.SighashType = SigHash.All; } expected = PSBT.Parse((string)testcase["psbt3"], Network.Main); Assert.Equal(expected, psbt, ComparerInstance); psbt.CheckSanity(); var psbtForBob = psbt.Clone(); // path 1 ... alice Assert.Equal(psbt, psbtForBob, ComparerInstance); var aliceKey1 = master.Derive(new KeyPath((string)testcase["key7"]["path"])).PrivateKey; var aliceKey2 = master.Derive(new KeyPath((string)testcase["key8"]["path"])).PrivateKey; psbt.SignAll(aliceKey1, aliceKey2); expected = PSBT.Parse((string)testcase["psbt4"], Network.Main); Assert.Equal(expected, psbt); // path 2 ... bob. var bobKey1 = master.Derive(new KeyPath((string)testcase["key9"]["path"])).PrivateKey; var bobKey2 = master.Derive(new KeyPath((string)testcase["key10"]["path"])).PrivateKey; var bobKeyhex1 = (string)testcase["key9"]["wif"]; var bobKeyhex2 = (string)testcase["key10"]["wif"]; Assert.Equal(bobKey1, new BitcoinSecret(bobKeyhex1, network).PrivateKey); Assert.Equal(bobKey2, new BitcoinSecret(bobKeyhex2, network).PrivateKey); psbtForBob.UseLowR = false; psbtForBob.SignAll(bobKey1, bobKey2); expected = PSBT.Parse((string)testcase["psbt5"], Network.Main); Assert.Equal(expected, psbtForBob); // merge above 2 var combined = psbt.Combine(psbtForBob); expected = PSBT.Parse((string)testcase["psbtcombined"], Network.Main); Assert.Equal(expected, combined); var finalized = psbt.Finalize(); expected = PSBT.Parse((string)testcase["psbtfinalized"], Network.Main); Assert.Equal(expected, finalized); var finalTX = psbt.ExtractTX(); var expectedTX = Transaction.Parse((string)testcase["txextracted"], network); AssertEx.CollectionEquals(expectedTX.ToBytes(), finalTX.ToBytes()); }
public void AddingScriptCoinShouldResultMoreInfoThanAddingSeparatelyInCaseOfP2SH() { var keys = new Key[] { new Key(), new Key(), new Key() }; var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray()); var network = Network.Main; var funds = CreateDummyFunds(network, keys, redeem); var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false); var psbt = PSBT.FromTransaction(tx); // case 1: Check that it will result to more info by adding ScriptCoin in case of p2sh-p2wpkh var coins1 = DummyFundsToCoins(funds, null, null); // without script var scriptCoins2 = DummyFundsToCoins(funds, null, keys[0]); // only with p2sh-p2wpkh redeem. var psbt1 = psbt.Clone().AddCoins(coins1).AddScript(redeem); var psbt2 = psbt.Clone().AddCoins(scriptCoins2).AddScript(redeem); for (int i = 0; i < 6; i++) { Output.WriteLine($"Testing {i}"); var a = psbt1.Inputs[i]; var e = psbt2.Inputs[i]; // Since there are no way psbt can know p2sh-p2wpkh is actually a witness input in case we add coins and scripts separately, // coin will not be added to the inputs[4]. if (i == 4) // p2sh-p2wpkh { Assert.NotEqual(a.ToBytes(), e.ToBytes()); Assert.Null(a.RedeemScript); Assert.Null(a.WitnessUtxo); Assert.NotNull(e.RedeemScript); Assert.NotNull(e.WitnessUtxo); } // but otherwise, it will be the same. else { AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes()); } } // case 2: bare p2sh and p2sh-pw2sh var scriptCoins3 = DummyFundsToCoins(funds, redeem, keys[0]); // with full scripts. var psbt3 = psbt.Clone().AddCoins(scriptCoins3); for (int i = 0; i < 6; i++) { Output.WriteLine($"Testing {i}"); var a = psbt2.Inputs[i]; var e = psbt3.Inputs[i]; if (i == 2 || i == 5) // p2sh or p2sh-p2wsh { Assert.NotEqual <byte[]>(a.ToBytes(), e.ToBytes()); Assert.Null(a.WitnessUtxo); Assert.Null(a.RedeemScript); Assert.NotNull(e.RedeemScript); if (i == 5) // p2sh-p2wsh { Assert.NotNull(e.WitnessUtxo); Assert.NotNull(e.WitnessScript); } } else { AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes()); } } }
public void CanUpdate() { var network = Network.Main; var alice = Key.Parse("L23Ng7B8iXTcQ9emwDYpabUJVsxDQDxKwePrTwAZo1VT9xcDPfBF", network); var bob = Key.Parse("L2nRrbzZytXSTjn95a4droGrAj5uwSEeG3JUHeNwdB9pUHu8Znjo", network); var carol = Key.Parse("KzHNhJn4P3FML22cQ9yr6rc35hmTPwVmaCnXkc114fQ8ZKRR9hoK", network); var keys = new Key[] { alice, bob, carol }; var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray()); var funds = CreateDummyFunds(network, keys, redeem); var tx = CreateTxToSpendFunds(funds, keys, redeem, true, false); var coins = DummyFundsToCoins(funds, redeem, alice); var psbtWithCoins = PSBT.FromTransaction(tx, true) .AddCoins(coins); Assert.Null(psbtWithCoins.Inputs[0].WitnessUtxo); Assert.NotNull(psbtWithCoins.Inputs[1].WitnessUtxo); Assert.Null(psbtWithCoins.Inputs[2].WitnessUtxo); Assert.NotNull(psbtWithCoins.Inputs[3].WitnessUtxo); Assert.NotNull(psbtWithCoins.Inputs[4].WitnessUtxo); Assert.NotNull(psbtWithCoins.Inputs[5].WitnessUtxo); // Check if it holds scripts as expected. Assert.Null(psbtWithCoins.Inputs[0].RedeemScript); // p2pkh Assert.Null(psbtWithCoins.Inputs[0].WitnessScript); // p2pkh Assert.Null(psbtWithCoins.Inputs[1].WitnessScript); // p2wpkh Assert.NotNull(psbtWithCoins.Inputs[2].RedeemScript); // p2sh Assert.NotNull(psbtWithCoins.Inputs[4].RedeemScript); // p2sh-p2wpkh Assert.NotNull(psbtWithCoins.Inputs[5].RedeemScript); // p2sh-p2wsh Assert.NotNull(psbtWithCoins.Inputs[3].WitnessScript); // p2wsh Assert.NotNull(psbtWithCoins.Inputs[5].WitnessScript); // p2sh-p2wsh // Operation must be idempotent. var tmp = psbtWithCoins.Clone().AddCoins(coins); Assert.Equal(tmp, psbtWithCoins, ComparerInstance); var signedPSBTWithCoins = psbtWithCoins .SignAll(alice); Assert.Empty(signedPSBTWithCoins.Inputs[0].PartialSigs); // can not sign for non segwit input without non-witness UTXO Assert.Empty(signedPSBTWithCoins.Inputs[2].PartialSigs); // This too. // otherwise, It will increase Partial sigs count. Assert.Single(signedPSBTWithCoins.Inputs[1].PartialSigs); Assert.Single(signedPSBTWithCoins.Inputs[3].PartialSigs); Assert.Single(signedPSBTWithCoins.Inputs[4].PartialSigs); Assert.Single(signedPSBTWithCoins.Inputs[5].PartialSigs); var ex = Assert.Throws <AggregateException>(() => signedPSBTWithCoins.Finalize() ); var finalizationErrors = ex.InnerExceptions; // Only p2wpkh and p2sh-p2wpkh will succeed. Assert.Equal(4, finalizationErrors.Count); var psbtWithTXs = PSBT.FromTransaction(tx, true) .AddTransactions(funds); Assert.Null(psbtWithTXs.Inputs[0].WitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[0].NonWitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[1].WitnessUtxo); Assert.Null(psbtWithTXs.Inputs[2].WitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[2].NonWitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[3].WitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[4].WitnessUtxo); Assert.NotNull(psbtWithTXs.Inputs[5].WitnessUtxo); // Operation must be idempotent. tmp = psbtWithTXs.Clone() .AddCoins(coins) .AddTransactions(funds); Assert.Equal(psbtWithTXs, tmp, ComparerInstance); var clonedPSBT = psbtWithTXs.Clone(); clonedPSBT.SignAll(keys[0]); psbtWithTXs.SignAll(keys[1], keys[2]); var whollySignedPSBT = clonedPSBT.Combine(psbtWithTXs); // must sign only once for whole kinds of non-multisig tx. Assert.Single(whollySignedPSBT.Inputs[0].PartialSigs); Assert.Single(whollySignedPSBT.Inputs[1].PartialSigs); Assert.Single(whollySignedPSBT.Inputs[4].PartialSigs); // for multisig Assert.Equal(3, whollySignedPSBT.Inputs[2].PartialSigs.Count); Assert.Equal(3, whollySignedPSBT.Inputs[2].PartialSigs.Values.Select(v => v.Item2).Distinct().Count()); Assert.Equal(3, whollySignedPSBT.Inputs[3].PartialSigs.Count); Assert.Equal(3, whollySignedPSBT.Inputs[3].PartialSigs.Values.Select(v => v.Item2).Distinct().Count()); Assert.Equal(3, whollySignedPSBT.Inputs[5].PartialSigs.Count); Assert.Equal(3, whollySignedPSBT.Inputs[5].PartialSigs.Values.Select(v => v.Item2).Distinct().Count()); Assert.False(whollySignedPSBT.CanExtractTX()); var finalizedPSBT = whollySignedPSBT.Finalize(); Assert.True(finalizedPSBT.CanExtractTX()); var finalTX = finalizedPSBT.ExtractTX(); var result = finalTX.Check(); Assert.Equal(TransactionCheckResult.Success, result); var builder = network.CreateTransactionBuilder(); builder.AddCoins(coins).AddKeys(keys); if (!builder.Verify(finalTX, (Money)null, out var errors)) { throw new InvalidOperationException(errors.Aggregate(string.Empty, (a, b) => a + ";\n" + b)); } }
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{(_inputCount > 1 ? 's' : "")} and "; _outputCount = outputAddressAmount.Length; _outputCountString = $" output{(_outputCount > 1 ? 's' : "")}."; _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(ex.ToUserFriendlyString(), "It was not possible to broadcast the transaction."); } }, nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public void CanFollowBIPExample() { var extkey = new BitcoinExtKey("tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF", Network.TestNet); // A creator creating a PSBT for a transaction which creates the following outputs: Transaction tx = extkey.Network.CreateTransaction(); tx.Version = 2; tx.Outputs.Add(Money.Coins(1.49990000m), new Script(Encoders.Hex.DecodeData("0014d85c2b71d0060b09c9886aeb815e50991dda124d"))); tx.Outputs.Add(Money.Coins(1.00000000m), new Script(Encoders.Hex.DecodeData("001400aea9a2e5f0f876a588df5546e8742d1d87008f"))); // and spends the following inputs: tx.Inputs.Add(OutPoint.Parse("75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858-0")); tx.Inputs.Add(OutPoint.Parse("1dea7cd05979072a3578cab271c02244ea8a090bbb46aa680a65ecd027048d83-1")); var actualPsbt = PSBT.FromTransaction(tx, Network.Main); // must create this PSBT: var expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, an updater with only the following: // Previous Transactions: actualPsbt.AddTransactions( Transaction.Parse("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000", Network.Main), Transaction.Parse("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000", Network.Main) ); // Scripts actualPsbt.AddScripts( new Script(Encoders.Hex.DecodeData("5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae")), new Script(Encoders.Hex.DecodeData("00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903")), new Script(Encoders.Hex.DecodeData("522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"))); // Public Keys actualPsbt.AddKeyPath(extkey, new KeyPath("m/0'/0'/0'"), new KeyPath("m/0'/0'/1'"), new KeyPath("m/0'/0'/2'"), new KeyPath("m/0'/0'/3'"), new KeyPath("m/0'/0'/4'"), new KeyPath("m/0'/0'/5'")); // Must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); // An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT: foreach (var input in actualPsbt.Inputs) input.SighashType = SigHash.All; expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); Assert.Equal(expectedPsbt, actualPsbt); actualPsbt.Settings.UseLowR = false; expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); var tmp = actualPsbt.Clone(); // Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys: actualPsbt.SignWithKeys(extkey.Derive(new KeyPath("m/0'/0'/0'")), extkey.Derive(new KeyPath("m/0'/0'/2'"))); Assert.Equal(expectedPsbt, actualPsbt); actualPsbt = tmp.Clone(); actualPsbt.SignWithKeys(new BitcoinSecret("cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr", Network.TestNet)); actualPsbt.SignWithKeys(new BitcoinSecret("cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d", Network.TestNet)); var part1 = actualPsbt; Assert.Equal(expectedPsbt, actualPsbt); actualPsbt = tmp.Clone(); // Given the above updated PSBT, a signer with the following keys: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt.SignWithKeys(new BitcoinSecret("cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au", Network.TestNet)); actualPsbt.SignWithKeys(new BitcoinSecret("cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE", Network.TestNet)); var part2 = actualPsbt; Assert.Equal(expectedPsbt, actualPsbt); // Given both of the above PSBTs, a combiner must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt = part1.Combine(part2); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, an input finalizer must create this PSBT: expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network); actualPsbt.Finalize(); Assert.Equal(expectedPsbt, actualPsbt); // Given the above PSBT, a transaction extractor must create this Bitcoin transaction: var expectedTx = Transaction.Parse("0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000", extkey.Network); Assert.True(actualPsbt.CanExtractTransaction()); var actualTx = actualPsbt.ExtractTransaction().ToHex(); Assert.Equal(expectedTx.ToHex(), actualTx); // Given these two PSBTs with unknown key-value pairs: var psbt1 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00", extkey.Network); var psbt2 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network); // A combiner which orders keys lexicographically must produce the following PSBT: expectedPsbt = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network); actualPsbt = psbt1.Combine(psbt2); Assert.Equal(expectedPsbt, actualPsbt); }
public async Task <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}"); 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; 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 receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //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")); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { ctx.DoNotBroadcast(); return(UnprocessableEntity(CreatePayjoinError("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(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) { 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; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); 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; var rand = new Random(); 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[rand.Next(0, 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; 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.Sign(privateKey, new SigningOptions() { EnforceLowR = enforcedLowR }); 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); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins), 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) { return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The original transaction has already been accounted"))); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = new WalletId(invoice.StoreId, network.CryptoCode), TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => new KeyValuePair <uint256, List <(string color, string label)> >(utxo.Key, new List <(string color, string label)>() { UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id) })) .ToDictionary(pair => pair.Key, pair => pair.Value) });
public BroadcastTransactionViewModel( BitcoinStore store, Network network, TransactionBroadcaster broadcaster, SmartTransaction 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{(_inputCount > 1 ? 's' : "")} and "; _outputCount = outputAddressAmount.Length; _outputCountString = $" output{(_outputCount > 1 ? 's' : "")}."; _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() => { IsBusy = true; try { await broadcaster.SendTransactionAsync(transaction); } catch (Exception ex) { // TODO: Notify the user Logger.LogError(ex); } Navigate().Back(); IsBusy = false; }, nextCommandCanExecute); }
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); } var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (sendersInputType is null) { return(BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support segwit inputs (of the same type)"))); } 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 receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"))); } if (sendersInputType != receiverInputsType) { return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin with the sender's inputs type"))); } 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(); Array.Sort(utxos, UTXODeterministicComparer.Instance); 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; // Randomly round the amount to make the payment output look like a change output var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount)); while (roundMultiple > 1_000UL) { if (RandomUtils.GetUInt32() % 2 == 0) { roundMultiple = roundMultiple / 10; } else { randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple; break; } } 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.GetSignableCoin())); txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))); 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); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); 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 && HexEncoder.IsWellFormed(rawBody)) { return(Ok(newPsbt.ToHex())); } else if (psbtFormat) { return(Ok(newPsbt.ToBase64())); } else { return(Ok(newTx.ToHex())); } }
public FundingPSBT Build(Network network) { var offererCoins = Offerer.FundingInputs.Select(f => f.AsCoin()).ToArray(); var acceptorCoins = Acceptor.FundingInputs.Select(f => f.AsCoin()).ToArray(); var fundingScript = GetFundingScript(); var p2wsh = fundingScript.WitHash.ScriptPubKey; Transaction?tx = null; if (transactionOverride is null) { tx = network.CreateTransaction(); tx.Version = 2; tx.LockTime = 0; foreach (var input in Offerer.FundingInputs.Concat(Acceptor.FundingInputs)) { var txin = tx.Inputs.Add(input.AsCoin().Outpoint, Script.Empty); if (input.RedeemScript is Script) { txin.ScriptSig = new Script(Op.GetPushOp(input.RedeemScript.ToBytes())); } } foreach (var input in tx.Inputs) { input.Sequence = 0xffffffff; } tx.Outputs.Add(Offerer.Collateral + Acceptor.Collateral, p2wsh); var totalInput = offererCoins.Select(s => s.Amount).Sum(); if (Offerer.Change is Script change) { tx.Outputs.Add(totalInput - Offerer.Collateral, change); } totalInput = acceptorCoins.Select(s => s.Amount).Sum(); if (Acceptor.Change is Script change2) { tx.Outputs.Add(totalInput - Acceptor.Collateral, change2); } tx.Outputs[1].Value -= FeeRate.GetFee(Offerer.VSizes.Funding); tx.Outputs[2].Value -= FeeRate.GetFee(Acceptor.VSizes.Funding); var offererFee = FeeRate.GetFee(Offerer.VSizes.CET); var acceptorFee = FeeRate.GetFee(Acceptor.VSizes.CET); tx.Outputs[1].Value -= offererFee; tx.Outputs[2].Value -= acceptorFee; tx.Outputs[0].Value += offererFee + acceptorFee; } else { tx = transactionOverride; } var psbt = PSBT.FromTransaction(tx, network); foreach (var input in Offerer.FundingInputs.Concat(Acceptor.FundingInputs)) { var txin = psbt.Inputs.FindIndexedInput(input.AsCoin().Outpoint); txin.RedeemScript = input.RedeemScript; txin.NonWitnessUtxo = input.InputTransaction; if (txin.NonWitnessUtxo is null) { txin.WitnessUtxo = input.Output; if (txin.WitnessUtxo is null) { throw new InvalidOperationException("Input funding not having witness Utxo nor NonWitnessUtxo"); } } } return(new FundingPSBT(psbt, new ScriptCoin(tx, 0, fundingScript))); }