public override WalletError Consolidate(IEnumerable <string> tagFrom, string tagTo, BigInteger feeMax, BigInteger feeUnit, out IEnumerable <WalletTx> wtxs, int minConfs = 0, string replaceTxId = null) { wtxs = new List <WalletTx>(); // generate new address to send to var to = NewOrUnusedAddress(tagTo); // calc amount in tagFrom BigInteger amount = 0; foreach (var tag in tagFrom) { amount += this.GetBalance(tag); } // check we have enough funds if (amount <= 0) { logger.LogError("insufficient funds: balance is less then or equal to 0"); return(WalletError.InsufficientFunds); } // create tx template with destination as first output var tx = Transaction.Create(GetNetwork()); var money = new Money((ulong)amount); var toaddr = BitcoinAddress.Create(to.Address, GetNetwork()); var output = tx.Outputs.Add(money, toaddr); // create list of candidate coins to spend based on (a tx we might want to replace and) UTXOs from the selected tag var candidates = new List <CoinCandidate>(); if (replaceTxId != null) { // if we are replacing a tx we need to replace at least one of its inputs var replaceTx = GetTransaction(replaceTxId); if (replaceTx == null) { return(WalletError.NothingToReplace); } var res = WalletError.UnableToReplace; foreach (var tag in tagFrom) { var addrs = GetAddresses(tag); if (UseReplacedTxCoins(addrs, candidates, replaceTxId, replaceTx) == WalletError.Success) { res = WalletError.Success; } } if (res != WalletError.Success) { return(res); } } var utxos = GetClient().GetUTXOs(pubkey); foreach (var tag in tagFrom) { var addrs = GetAddresses(tag); UseUtxoCoins(addrs, candidates, utxos, minConfs); } // add all inputs so we can satisfy our output BigInteger totalInput = 0; var toBeSpent = new List <CoinSpend>(); foreach (var candidate in candidates) { // add to list of coins and private keys to spend var txin = new TxIn(candidate.Coin.Outpoint); txin.Sequence = 0; // RBF: BIP125 tx.Inputs.Add(txin); totalInput += candidate.Coin.Amount.Satoshi; var privateKey = key.ExtKey.Derive(new KeyPath(candidate.Addr.Path)).PrivateKey; toBeSpent.Add(new CoinSpend(candidate.Addr.Address, candidate.Addr, candidate.Coin, privateKey)); } // check we have enough inputs if (totalInput < amount) { logger.LogError("insufficient funds: total inputs are less then sending amount"); return(WalletError.InsufficientFunds); } // adjust fee rate by reducing the output incrementally var feeRate = new FeeRate(new Money(0L)); decimal currentSatsPerByte = 0; while (currentSatsPerByte < (decimal)feeUnit) { tx.Outputs[0].Value -= 1L; amount -= 1; if (amount <= 0) { logger.LogError("insufficient funds: after fees the amount sent is 0"); return(WalletError.InsufficientFunds); } feeRate = GetFeeRate(tx, toBeSpent); currentSatsPerByte = feeRate.SatoshiPerByte; } // sign inputs var coins = from a in toBeSpent select a.Coin; var keys = from a in toBeSpent select a.Key; // check coins are represented as incoming wallet txs CheckCoinsAreInWallet(candidates, utxos.CurrentHeight); tx.Sign(keys.ToArray(), coins.ToArray()); // recalculate fee rate and check it is less then the max fee var fee = tx.GetFee(coins.ToArray()); if (fee.Satoshi > feeMax) { return(WalletError.MaxFeeBreached); } // broadcast transaction var result = GetClient().Broadcast(tx); if (result.Success) { // log outgoing transaction var coinOutput = new CoinOutput(to.Address, amount); var wtxs_ = AddOutgoingTx(tx, toBeSpent, new List <CoinOutput> { coinOutput }, fee.Satoshi, null); ((List <WalletTx>)wtxs).AddRange(wtxs_); return(WalletError.Success); } else { logger.LogError("{0}, {1}, {2}", result.RPCCode, result.RPCCodeMessage, result.RPCMessage); return(WalletError.FailedBroadcast); } }
public override WalletError Spend(string tag, string tagChange, string to, BigInteger amount, BigInteger feeMax, BigInteger feeUnit, out IEnumerable <WalletTx> wtxs, WalletTag tagFor = null, string replaceTxId = null) { wtxs = new List <WalletTx>(); var tagChange_ = db.TagGet(tagChange); Util.WalletAssert(tagChange_ != null, $"Tag '{tagChange}' does not exist"); // create tx template with destination as first output var tx = Transaction.Create(GetNetwork()); var money = new Money((ulong)amount); var toaddr = BitcoinAddress.Create(to, GetNetwork()); var output = tx.Outputs.Add(money, toaddr); // create list of candidate coins to spend based on (a tx we might want to replace and) UTXOs from the selected tag var addrs = GetAddresses(tag); var candidates = new List <CoinCandidate>(); var res = UseReplacedTxCoins(addrs, candidates, replaceTxId); if (res != WalletError.Success) { return(res); } var utxos = GetClient().GetUTXOs(pubkey); UseUtxoCoins(addrs, candidates, utxos); // add inputs until we can satisfy our output BigInteger totalInput = 0; var toBeSpent = new List <CoinSpend>(); foreach (var candidate in candidates) { // add to list of coins and private keys to spend var txin = new TxIn(candidate.Coin.Outpoint); txin.Sequence = 0; // RBF: BIP125 tx.Inputs.Add(txin); totalInput += candidate.Coin.Amount.Satoshi; var privateKey = key.ExtKey.Derive(new KeyPath(candidate.Addr.Path)).PrivateKey; toBeSpent.Add(new CoinSpend(candidate.Addr.Address, candidate.Addr, candidate.Coin, privateKey)); // check if we have enough inputs if (totalInput >= amount) { // check if we have enough fee if (GetFeeRate(tx, toBeSpent).SatoshiPerByte > (decimal)feeUnit) { break; } } } // check we have enough inputs logger.LogDebug($"totalInput {totalInput}, amount: {amount}"); if (totalInput < amount) { logger.LogError("insufficient funds: total inputs are less then sending amount"); return(WalletError.InsufficientFunds); } // check fee rate var feeRate = GetFeeRate(tx, toBeSpent); if (feeRate.SatoshiPerByte > (decimal)feeUnit) { // create a change address var changeAddress = AddChangeAddress(tagChange_); // calculate the target fee var currentFee = feeRate.GetFee(tx.GetVirtualSize()); var targetFee = tx.GetVirtualSize() * (long)feeUnit; var changeOutput = new TxOut(currentFee - targetFee, changeAddress); targetFee += output.GetSerializedSize() * (long)feeUnit; // add the change output changeOutput = tx.Outputs.Add(currentFee - targetFee, changeAddress); } else if (feeRate.SatoshiPerByte < (decimal)feeUnit) { logger.LogError($"insufficient funds: fee rate ({feeRate.SatoshiPerByte} sats/byte) is less then {feeUnit}"); return(WalletError.InsufficientFunds); } var coins = from a in toBeSpent select a.Coin; var keys = from a in toBeSpent select a.Key; // check coins are represented as incoming wallet txs CheckCoinsAreInWallet(candidates, utxos.CurrentHeight); // sign inputs (after adding a change output) tx.Sign(keys.ToArray(), coins.ToArray()); // recalculate fee rate and check it is less then the max fee var fee = tx.GetFee(coins.ToArray()); if (fee.Satoshi > feeMax) { return(WalletError.MaxFeeBreached); } // broadcast transaction var result = GetClient().Broadcast(tx); if (result.Success) { // log outgoing transaction var coinOutput = new CoinOutput(to, amount); var wtxs_ = AddOutgoingTx(tx, toBeSpent, new List <CoinOutput> { coinOutput }, fee.Satoshi, tagFor); ((List <WalletTx>)wtxs).AddRange(wtxs_); return(WalletError.Success); } else { logger.LogError("{0}, {1}, {2}", result.RPCCode, result.RPCCodeMessage, result.RPCMessage); return(WalletError.FailedBroadcast); } }