Example #1
0
        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);
            }
        }
Example #2
0
        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);
            }
        }