public static (Func <Context, Append>[] appends, Identifier keyid) inputs_and_change( OutputData[] coins, WalletConfig config, Keychain keychain, ulong amount ) { var parts = new List <Func <Context, Append> >(); // calculate the total across all inputs, and how much is left var total = coins.Select(s => s.Value).Aggregate((a, b) => a + b); if (total < amount) { throw new WalletErrorException(WalletError.NotEnoughFunds); } // sender is responsible for setting the fee on the partial tx // recipient should double check the fee calculation and not blindly trust the // sender var fee = Types.tx_fee((uint)coins.Length, 2, null); parts.Add(c => c.with_fee(fee)); // if we are spending 10,000 coins to send 1,000 then our change will be 9,000 // the fee will come out of the amount itself // if the fee is 80 then the recipient will only receive 920 // but our change will still be 9,000 var change = total - amount; // build inputs using the appropriate derived key_ids foreach (var coin in coins) { var keyId = keychain.Derive_key_id(coin.NChild); parts.Add(c => c.Input(coin.Value, keyId)); } // track the output representing our change var changeKey = WalletData.With_wallet(config.DataFileDir, walletData => { var rootKeyId = keychain.Root_key_id(); var changeDerivation = walletData.Next_child(rootKeyId.Clone()); var changekey = keychain.Derive_key_id(changeDerivation); walletData.Add_output(new OutputData( rootKeyId.HexValue, changekey.HexValue, changeDerivation, change, OutputStatus.Unconfirmed, 0, 0, false)); return(changekey); }); parts.Add(c => c.Output(change, changeKey.Clone())); return(parts.ToArray(), changeKey); }
/// Issue a new transaction to the provided sender by spending some of our /// walvar /// UTXOs. The destination can be "stdout" (for command line) or a URL to the /// recipients walvar receiver (to be implemented). public static void issue_send_tx( WalletConfig config, Keychain keychain, ulong amount, ulong minimumConfirmations, string dest, uint maxOutputs, bool selectionStrategy ) { Checker.refresh_outputs(config, keychain); var chainTip = Checker.get_tip_from_node(config); var currentHeight = chainTip.Height; // proof of concept - set lock_height on the tx var lockHeight = chainTip.Height; var(tx, blindSum, coins, changeKey) = build_send_tx( config, keychain, amount, currentHeight, minimumConfirmations, lockHeight, maxOutputs, selectionStrategy); var partialTx = PartialTx.build_partial_tx(amount, blindSum, tx); // Closure to acquire walvar lock and lock the coins being spent // so we avoid accidental double spend attempt. void UpdateWallet() { WalletData.With_wallet(config.DataFileDir, walletData => { foreach (var coin in coins) { walletData.Lock_output(coin); } return(walletData); }); } // Closure to acquire walvar lock and devare the change output in case of tx failure. void RollbackWallet() { WalletData.With_wallet(config.DataFileDir, walletData => { Log.Information("cleaning up unused change output from walvar"); walletData.Delete_output(changeKey); return(walletData); }); } if (dest == "stdout") { var jsonTx = JsonConvert.SerializeObject(partialTx, Formatting.Indented); UpdateWallet(); Console.WriteLine(jsonTx); } else if (dest.StartsWith("http")) { var url = $"{dest}/v1/receive/transaction"; Log.Debug("Posting partial transaction to {url}", url); try { Client.send_partial_tx(url, partialTx); UpdateWallet(); } catch { Log.Error("Communication with receiver failed. Aborting transaction"); RollbackWallet(); } } else { throw new Exception($"dest not in expected format: {dest}"); } }
/// Builds a single api query to retrieve the latest output data from the node. /// So we can refresh the local wallet outputs. public static bool refresh_outputs(WalletConfig config, Keychain keychain) { Log.Debug("Refreshing wallet outputs"); var walletOutputs = new Dictionary <string, Identifier>(); var commits = new List <Commitment>(); // build a local map of wallet outputs by commits // and a list of outputs we want to query the node for WalletData.Read_wallet(config.DataFileDir, walletData => { foreach (var op in walletData .Outputs .Values .Where(w => w.RootKeyId == keychain.Root_key_id().HexValue&& w.Status != OutputStatus.Spent)) { var commit = keychain.commit_with_key_index(op.Value, op.NChild); commits.Add(commit); walletOutputs.Add(commit.Hex, Identifier.From_hex(op.KeyId)); } return(walletData); }); // build the necessary query params - // ?id=xxx&id=yyy&id=zzz var queryParams = commits.Select(s => $"id={s.Hex}"); var queryString = string.Join("&", queryParams); var url = $"{config.CheckNodeApiHttpAddr}/v1/chain/utxos/byids?{queryString}"; // build a map of api outputs by commit so we can look them up efficiently var apiOutputs = new Dictionary <string, ApiOutput>(); //HttpClient here // todo:asyncification HttpResponseMessage response; try { response = ApiClient.GetAsync(url).Result; } catch (Exception ex) { Log.Warning(ex, "Failed to refresh from {Url}", url); return(false); } // if we got anything other than 200 back from server, don't attempt to refresh the wallet // data after if (!response.IsSuccessStatusCode) { Log.Warning("Failed to refresh from {Url} : {StatusCode} - {ReasonPhrase}", url, response.StatusCode, response.ReasonPhrase); return(false); } var content = response.Content.ReadAsStringAsync().Result; var outputs = JsonConvert.DeserializeObject <ApiOutput[]>(content); foreach (var op in outputs) { apiOutputs.Add(HexUtil.to_hex(op.Commit), op); } // now for each commit, find the output in the wallet and // the corresponding api output (if it exists) // and refresh it in-place in the wallet. // Note: minimizing the time we spend holding the wallet lock. WalletData.With_wallet(config.DataFileDir, walletData => { foreach (var commit in commits) { var id = walletOutputs[commit.Hex]; if (walletData.Outputs.TryGetValue(id.HexValue, out var op)) { if (apiOutputs.TryGetValue(commit.Hex, out var apiOutput)) { refresh_output(op, apiOutput); } else { mark_spent_output(op); } } } return(walletData); }); return(true); }