public void RebaseKeyPaths(PSBT psbt) { foreach (var rebase in GetPSBTRebaseKeyRules()) { psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath); } }
public void RebaseKeyPaths(PSBT psbt) { foreach (var rebase in GetPSBTRebaseKeyRules()) { psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint); } }
private static PSBT Sign(Party party, DerivationStrategyBase derivationStrategy, PSBT psbt) { psbt = psbt.Clone(); // NBXplorer does not have knowledge of the account key path, KeyPath are private information of each peer // NBXplorer only derive 0/* and 1/* on top of provided account xpubs, // This mean that the input keypaths in the PSBT are in the form 0/* (as if the account key was the root) // RebaseKeyPaths modifies the PSBT by adding the AccountKeyPath in prefix of all the keypaths of the PSBT // Note that this is not necessary to do this if the account key is the same as root key. // Note that also that you don't have to do this, if you do not pass the account key path in the later SignAll call. // however, this is best practice to rebase the PSBT before signing. // If you sign with an offline device (hw wallet), the wallet would need the rebased PSBT. psbt.RebaseKeyPaths(party.AccountExtPubKey, party.AccountKeyPath); Console.WriteLine("A PSBT is a data structure with all information for a wallet to sign."); var spend = psbt.GetBalance(derivationStrategy, party.AccountExtPubKey, party.AccountKeyPath); Console.WriteLine($"{party.PartyName}, Do you agree to sign this transaction spending {spend}?"); // Ok I sign psbt.SignAll(derivationStrategy, // What addresses to derive? party.RootExtKey.Derive(party.AccountKeyPath), // With which account private keys? party.AccountKeyPath); // What is the keypath of the account private key. If you did not rebased the keypath like before, you can remove this parameter return(psbt); }
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 utxos = await GetUTXOs(network.CryptoCode, strategy, null); 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 availableCoinsByOutpoint = utxos.GetUnspentCoins(request.MinConfirmations).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.Unconfirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? utxos.Confirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? strategy.Derive(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); var outputsKeyInformations = repo.GetKeyInformations(psbt.Outputs.Where(o => !o.HDKeyPaths.Any()).Select(o => o.ScriptPubKey).ToArray()); var utxosByOutpoint = utxos.GetUnspentUTXOs().ToDictionary(u => u.Outpoint); // 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); } } } var pubkeys = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray(); var keyPaths = psbt.Inputs.Select(i => utxosByOutpoint[i.PrevOut].KeyPath).ToList(); if (hasChange && change.KeyPath != null) { keyPaths.Add(change.KeyPath); } var fps = new Dictionary <PubKey, HDFingerprint>(); foreach (var pubkey in pubkeys) { // We derive everything the fastest way possible on multiple cores pubkey.Derive(keyPaths.ToArray()); fps.TryAdd(pubkey.GetPublicKey(), pubkey.GetPublicKey().GetHDFingerPrint()); } foreach (var input in psbt.Inputs) { var utxo = utxosByOutpoint[input.PrevOut]; foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(utxo.KeyPath); NBitcoin.Extensions.TryAdd(input.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], utxo.KeyPath)); } } await Task.WhenAll(psbt.Inputs .Select(async(input) => { if (input.WitnessUtxo == null) // We need previous tx { var prev = await repo.GetSavedTransactions(input.PrevOut.Hash); if (prev?.Any() is true) { input.NonWitnessUtxo = prev[0].Transaction; } } }).ToArray()); var outputsKeyInformationsResult = await outputsKeyInformations; foreach (var output in psbt.Outputs) { foreach (var keyInfo in outputsKeyInformationsResult[output.ScriptPubKey].Where(o => o.DerivationStrategy == strategy)) { foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(keyInfo.KeyPath); NBitcoin.Extensions.TryAdd(output.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], keyInfo.KeyPath)); } } } if (request.RebaseKeyPaths != null) { foreach (var rebase in request.RebaseKeyPaths) { if (rebase.AccountKeyPath == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing")); } if (rebase.AccountKey == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing")); } psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint); } } var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }