public string[] Receive(string requestToken) { Safe safe = GetSafeFromCache(requestToken); var addresses = new List <string>(); if (Config.ConnectionType == ConnectionType.Http) { Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerReceiveAddresses = QBitNinjaJutsus.QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive); foreach (var elem in operationsPerReceiveAddresses) { if (elem.Value.Count == 0) { addresses.Add(elem.Key.ToString()); } } } else { throw new InvalidOperationException("Invalid connection type."); } return(addresses.ToArray()); }
public BtcWalletHistory[] GetHistory(string requestToken) { var safe = GetSafeFromCache(requestToken); // 0. Query all operations, grouped our used safe addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QBitNinjaJutsus.QueryOperationsPerSafeAddresses(safe); Dictionary <uint256, List <BalanceOperation> > operationsPerTransactions = QBitNinjaJutsus.GetOperationsPerTransactions(operationsPerAddresses); // 3. Create history records from the transactions // History records is arbitrary data we want to show to the user var txHistoryRecords = new List <Tuple <DateTimeOffset, Money, int, uint256> >(); foreach (var elem in operationsPerTransactions) { var amount = Money.Zero; foreach (var op in elem.Value) { amount += op.Amount; } var firstOp = elem.Value.First(); txHistoryRecords .Add(new Tuple <DateTimeOffset, Money, int, uint256>( firstOp.FirstSeen, amount, firstOp.Confirmations, elem.Key)); } // 4. Order the records by confirmations and time (Simply time does not work, because of a QBitNinja bug) var orderedTxHistoryRecords = txHistoryRecords .OrderByDescending(x => x.Item3) // Confirmations .ThenBy(x => x.Item1); // FirstSeen var history = new List <BtcWalletHistory>(); foreach (var record in orderedTxHistoryRecords) { history.Add(new BtcWalletHistory() { DateTime = record.Item1.DateTime, AmountBtc = record.Item2.ToDecimal(MoneyUnit.BTC),// Item2 is the ConfirmedBalance Confirmed = record.Item3 > 0, TransactionId = record.Item4.ToBytes(lendian: true) }); } return(history.ToArray()); }
public SendResult Send(string requestToken, string address, string btcAmount) { Safe safe = GetSafeFromCache(requestToken); BitcoinAddress addressToSend = null; var Exit = new Func <string, SendResult>( (m) => new SendResult() { Success = false, Message = m.ToString() }); var msg = new StringBuilder(); try { addressToSend = BitcoinAddress.Create(address, Config.Network); } catch (Exception ex) { msg.AppendLine(ex.ToString()); } if (addressToSend == null) { return(Exit(msg.ToString())); } if (Config.ConnectionType == ConnectionType.Http) { Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QBitNinjaJutsus.QueryOperationsPerSafeAddresses(safe); // 1. Gather all the not empty private keys msg.AppendLine("Finding not empty private keys..."); var operationsPerNotEmptyPrivateKeys = new Dictionary <BitcoinExtKey, List <BalanceOperation> >(); foreach (var elem in operationsPerAddresses) { var balance = Money.Zero; foreach (var op in elem.Value) { balance += op.Amount; } if (balance > Money.Zero) { var secret = safe.FindPrivateKey(elem.Key); operationsPerNotEmptyPrivateKeys.Add(secret, elem.Value); } } // 2. Get the script pubkey of the change. msg.AppendLine("Select change address..."); Script changeScriptPubKey = null; Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerChangeAddresses = QBitNinjaJutsus.QueryOperationsPerSafeAddresses(safe, minUnusedKeys: 1, hdPathType: HdPathType.Change); foreach (var elem in operationsPerChangeAddresses) { if (elem.Value.Count == 0) { changeScriptPubKey = safe.FindPrivateKey(elem.Key).ScriptPubKey; } } if (changeScriptPubKey == null) { throw new ArgumentNullException(); } // 3. Gather coins can be spend msg.AppendLine("Gathering unspent coins..."); Dictionary <Coin, bool> unspentCoins = QBitNinjaJutsus.GetUnspentCoins(operationsPerNotEmptyPrivateKeys.Keys); // 4. Get the fee msg.AppendLine("Calculating transaction fee..."); Money fee; try { var txSizeInBytes = 250; using (var client = new HttpClient()) { const string request = @"https://bitcoinfees.earn.com/api/v1/fees/recommended"; var result = client.GetAsync(request, HttpCompletionOption.ResponseContentRead).Result; var json = JObject.Parse(result.Content.ReadAsStringAsync().Result); var fastestSatoshiPerByteFee = json.Value <decimal>("fastestFee"); fee = new Money(fastestSatoshiPerByteFee * txSizeInBytes, MoneyUnit.Satoshi); } } catch { return(Exit("Couldn't calculate transaction fee, try it again later.")); } msg.AppendLine($"Fee: {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); // 5. How much money we can spend? Money availableAmount = Money.Zero; Money unconfirmedAvailableAmount = Money.Zero; foreach (var elem in unspentCoins) { // If can spend unconfirmed add all if (Config.CanSpendUnconfirmed) { availableAmount += elem.Key.Amount; if (!elem.Value) { unconfirmedAvailableAmount += elem.Key.Amount; } } // else only add confirmed ones else { if (elem.Value) { availableAmount += elem.Key.Amount; } } } // 6. How much to spend? Money amountToSend = null; if (string.Equals(btcAmount, "all", StringComparison.OrdinalIgnoreCase)) { amountToSend = availableAmount; amountToSend -= fee; } else { amountToSend = ParseBtcString(btcAmount); } // 7. Do some checks if (amountToSend < Money.Zero || availableAmount < amountToSend + fee) { return(Exit("Not enough coins.")); } decimal feePc = Math.Round((100 * fee.ToDecimal(MoneyUnit.BTC)) / amountToSend.ToDecimal(MoneyUnit.BTC)); if (feePc > 1) { msg.AppendLine(); msg.AppendLine($"The transaction fee is {feePc.ToString("0.#")}% of your transaction amount."); msg.AppendLine( $"Sending:\t {amountToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); msg.AppendLine( $"Fee:\t\t {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); } var confirmedAvailableAmount = availableAmount - unconfirmedAvailableAmount; var totalOutAmount = amountToSend + fee; if (confirmedAvailableAmount < totalOutAmount) { var unconfirmedToSend = totalOutAmount - confirmedAvailableAmount; msg.AppendLine(); msg.AppendLine($"In order to complete this transaction " + $"you have to spend {unconfirmedToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")} unconfirmed btc."); } // 8. Select coins msg.AppendLine("Selecting coins..."); var coinsToSpend = new HashSet <Coin>(); var unspentConfirmedCoins = new List <Coin>(); var unspentUnconfirmedCoins = new List <Coin>(); foreach (var elem in unspentCoins) { if (elem.Value) { unspentConfirmedCoins.Add(elem.Key); } else { unspentUnconfirmedCoins.Add(elem.Key); } } bool haveEnough = QBitNinjaJutsus.SelectCoins(ref coinsToSpend, totalOutAmount, unspentConfirmedCoins); if (!haveEnough) { haveEnough = QBitNinjaJutsus.SelectCoins(ref coinsToSpend, totalOutAmount, unspentUnconfirmedCoins); } if (!haveEnough) { throw new Exception("Not enough funds."); } // 9. Get signing keys var signingKeys = new HashSet <ISecret>(); foreach (var coin in coinsToSpend) { foreach (var elem in operationsPerNotEmptyPrivateKeys) { if (elem.Key.ScriptPubKey == coin.ScriptPubKey) { signingKeys.Add(elem.Key); } } } // 10. Build the transaction msg.AppendLine("Signing transaction..."); var builder = new TransactionBuilder(); var tx = builder .AddCoins(coinsToSpend) .AddKeys(signingKeys.ToArray()) .Send(addressToSend, amountToSend) .SetChange(changeScriptPubKey) .SendFees(fee) .BuildTransaction(true); if (!builder.Verify(tx)) { return(Exit("Couldn't build the transaction.")); } msg.AppendLine($"Transaction Id: {tx.GetHash()}"); var qBitClient = new QBitNinjaClient(Config.Network); // QBit's success response is buggy so let's check manually, too BroadcastResponse broadcastResponse; var success = false; var tried = 0; var maxTry = 7; do { tried++; msg.AppendLine($"Try broadcasting transaction... ({tried})"); broadcastResponse = qBitClient.Broadcast(tx).Result; var getTxResp = qBitClient.GetTransaction(tx.GetHash()).Result; if (getTxResp == null) { Thread.Sleep(3000); } else { success = true; break; } } while (tried <= maxTry); if (!success) { if (broadcastResponse.Error != null) { msg.AppendLine($"Error code: {broadcastResponse.Error.ErrorCode} Reason: {broadcastResponse.Error.Reason}"); } return(Exit($"The transaction might not have been successfully broadcasted. Please check the Transaction ID in a block explorer.")); } msg.AppendLine("Transaction is successfully propagated on the network."); } return(new SendResult() { Success = true, Message = msg.ToString() }); }
public Model.BtcWallet GetBalances(string requestToken) { if (Config.ConnectionType == ConnectionType.Http) { Safe safe = GetSafeFromCache(requestToken); // 0. Query all operations, grouped by addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QBitNinjaJutsus.QueryOperationsPerSafeAddresses(safe); // 1. Get all address history records var addressHistoryRecords = (from elem in operationsPerAddresses from op in elem.Value select new AddressHistoryRecord(elem.Key, op)).ToList(); // 2. Calculate wallet balances Money confirmedWalletBalance; Money unconfirmedWalletBalance; QBitNinjaJutsus.GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance); // 3. Group all address history records by addresses var addressHistoryRecordsPerAddresses = new Dictionary <BitcoinAddress, HashSet <AddressHistoryRecord> >(); var result = new Dictionary <BitcoinAddress, BtcAddress>(); foreach (var address in operationsPerAddresses.Keys) { var recs = new HashSet <AddressHistoryRecord>(); foreach (var record in addressHistoryRecords) { if (record.Address == address) { recs.Add(record); } } addressHistoryRecordsPerAddresses.Add(address, recs); result.Add(address, new BtcAddress() { Address = address.ToString(), History = addressHistoryRecordsPerAddresses[address]? .Select(h => new AddressHistory() { Address = address.ToString(), AmountBtc = h.Amount.ToDecimal(MoneyUnit.BTC), Confirmed = h.Confirmed, FirstSeen = h.FirstSeen, TransactionId = h.TransactionId.ToString() }).ToList() }); } // 4. Calculate address balances foreach (var elem in addressHistoryRecordsPerAddresses) { Money confirmedBalance; Money unconfirmedBalance; QBitNinjaJutsus.GetBalances(elem.Value, out confirmedBalance, out unconfirmedBalance); if (confirmedBalance != Money.Zero || unconfirmedBalance != Money.Zero) { result[elem.Key].ConfirmedBalance = confirmedBalance.ToDecimal(MoneyUnit.BTC); result[elem.Key].UnconfirmedBalance = unconfirmedBalance.ToDecimal(MoneyUnit.BTC); } } return(new Model.BtcWallet { FileName = FileNameFromRequestTokens[requestToken], Addresses = addressHistoryRecordsPerAddresses.Select(c => result[c.Key]).ToList(), ConfirmedWalletBalance = confirmedWalletBalance.ToDecimal(MoneyUnit.BTC), UnconfirmedWalletBalance = unconfirmedWalletBalance.ToDecimal(MoneyUnit.BTC) }); } else { throw new NotImplementedException("Invalid connection type."); } }