Skip to content

Commit

Permalink
Hook up fee estimation to transaction creation.
Browse files Browse the repository at this point in the history
This change requires a newer version of the wallet's RPC API.

Fixes #10.
Fixes btcsuite#11.
  • Loading branch information
jrick committed Jul 1, 2016
1 parent 508aa36 commit 991e971
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 159 deletions.
2 changes: 1 addition & 1 deletion Paymetheus.Decred/Wallet/TransactionFees.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static Transaction AddChange(Transaction tx, Amount totalInput, OutputScr

var totalNonChangeOutput = tx.Outputs.Sum(o => o.Amount);
var changeAmount = totalInput - totalNonChangeOutput - feeEstimate;
var changeOutput = new Transaction.Output(changeAmount, Transaction.SupportedVersion, changeScript.Script);
var changeOutput = new Transaction.Output(changeAmount, Transaction.Output.LatestPkScriptVersion, changeScript.Script);

// Change should not be created if the change output itself would be considered dust.
if (TransactionRules.IsDust(changeOutput, feePerKb))
Expand Down
4 changes: 3 additions & 1 deletion Paymetheus.Decred/Wallet/UnspentOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Paymetheus.Decred.Wallet
{
public sealed class UnspentOutput
{
public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase)
public UnspentOutput(Blake256Hash txHash, uint outputIndex, byte tree, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase)
{
if (txHash == null)
throw new ArgumentNullException(nameof(txHash));
Expand All @@ -18,6 +18,7 @@ public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, Outpu

TransactionHash = txHash;
OutputIndex = outputIndex;
Tree = tree;
Amount = amount;
PkScript = pkScript;
SeenTime = seenTime;
Expand All @@ -26,6 +27,7 @@ public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, Outpu

public Blake256Hash TransactionHash { get; }
public uint OutputIndex { get; }
public byte Tree { get; }
public Amount Amount { get; }
public OutputScript PkScript { get; }
public DateTimeOffset SeenTime { get; }
Expand Down
191 changes: 109 additions & 82 deletions Paymetheus.Rpc/Api.cs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Paymetheus.Rpc/Marshalers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ public static UnspentOutput MarshalUnspentOutput(FundTransactionResponse.Types.P
{
var txHash = new Blake256Hash(o.TransactionHash.ToByteArray());
var outputIndex = o.OutputIndex;
var tree = (byte)o.Tree;
var amount = (Amount)o.Amount;
var pkScript = OutputScript.ParseScript(o.PkScript.ToByteArray());
var seenTime = DateTimeOffsetExtras.FromUnixTimeSeconds(o.ReceiveTime);
var isFromCoinbase = o.FromCoinbase;

return new UnspentOutput(txHash, outputIndex, amount, pkScript, seenTime, isFromCoinbase);
return new UnspentOutput(txHash, outputIndex, tree, amount, pkScript, seenTime, isFromCoinbase);
}
}
}
14 changes: 4 additions & 10 deletions Paymetheus.Rpc/WalletClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Google.Protobuf;
using Grpc.Core;
using Paymetheus.Decred;
using Paymetheus.Decred.Script;
using Paymetheus.Decred.Wallet;
using System;
using System.Collections.Generic;
Expand All @@ -21,7 +20,7 @@ namespace Paymetheus.Rpc
{
public sealed class WalletClient : IDisposable
{
private static readonly SemanticVersion RequiredRpcServerVersion = new SemanticVersion(2, 0, 2);
private static readonly SemanticVersion RequiredRpcServerVersion = new SemanticVersion(2, 1, 0);

public static void Initialize()
{
Expand Down Expand Up @@ -281,7 +280,7 @@ public async Task<Tuple<List<UnspentOutput>, Amount>> SelectUnspentOutputs(Accou
return Tuple.Create(outputs, total);
}

public async Task<Tuple<List<UnspentOutput>, Amount, OutputScript>> FundTransactionAsync(
public async Task<Tuple<List<UnspentOutput>, Amount>> FundTransactionAsync(
Account account, Amount targetAmount, int requiredConfirmations)
{
var client = WalletService.NewClient(_channel);
Expand All @@ -291,17 +290,12 @@ public async Task<Tuple<List<UnspentOutput>, Amount, OutputScript>> FundTransact
TargetAmount = targetAmount,
RequiredConfirmations = requiredConfirmations,
IncludeImmatureCoinbases = false,
IncludeChangeScript = true,
IncludeChangeScript = false,
};
var response = await client.FundTransactionAsync(request, cancellationToken: _tokenSource.Token);
var outputs = response.SelectedOutputs.Select(MarshalUnspentOutput).ToList();
var total = (Amount)response.TotalAmount;
var changeScript = (OutputScript)null;
if (response.ChangePkScript?.Length != 0)
{
changeScript = OutputScript.ParseScript(response.ChangePkScript.ToByteArray());
}
return Tuple.Create(outputs, total, changeScript);
return Tuple.Create(outputs, total);
}

public async Task<Tuple<Transaction, bool>> SignTransactionAsync(string passphrase, Transaction tx)
Expand Down
1 change: 1 addition & 0 deletions Paymetheus.Rpc/protos/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ message FundTransactionResponse {
bytes pk_script = 4;
int64 receive_time = 5;
bool from_coinbase = 6;
int32 tree = 7;
}
repeated PreviousOutput selected_outputs = 1;
int64 total_amount = 2;
Expand Down
173 changes: 109 additions & 64 deletions Paymetheus/ViewModels/CreateTransactionViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public CreateTransactionViewModel() : base()
AddPendingOutput();
}

private Transaction _pendingTransaction;
private OutputScript _changeScript;

private AccountViewModel _selectedAccount;
public AccountViewModel SelectedAccount
{
Expand Down Expand Up @@ -197,15 +200,23 @@ public bool PublishChecked
public ICommand AddPendingOutputCommand { get; }
public ICommand RemovePendingOutputCommand { get; }

private void AddPendingOutput()
private async void AddPendingOutput()
{
var pendingOutput = new PendingOutput();
pendingOutput.Changed += PendingOutput_Changed;
PendingOutputs.Add(pendingOutput);
RecalculateTransaction();

try
{
await RecalculatePendingTransaction();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}

private void RemovePendingOutput(PendingOutput item)
private async void RemovePendingOutput(PendingOutput item)
{
if (PendingOutputs.Remove(item))
{
Expand All @@ -216,34 +227,106 @@ private void RemovePendingOutput(PendingOutput item)
AddPendingOutput();
}

RecalculateTransaction();
try
{
await RecalculatePendingTransaction();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}
}

private void PendingOutput_Changed(object sender, EventArgs e)
private async void PendingOutput_Changed(object sender, EventArgs e)
{
RecalculateTransaction();
try
{
await RecalculatePendingTransaction();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}

private void RecalculateTransaction()
private async Task RecalculatePendingTransaction()
{
if (PendingOutputs.Count > 0 && PendingOutputs.All(x => x.IsValid))
if (PendingOutputs.Count == 0 || PendingOutputs.Any(x => !x.IsValid))
{
UnsetPendingTransaction();
return;
}

var walletClient = App.Current.Synchronizer?.WalletRpcClient;

if (_changeScript == null)
{
var changeAddress = await walletClient.NextInternalAddressAsync(SelectedAccount.Account);
_changeScript = Address.Decode(changeAddress).BuildScript();
}

var outputs = PendingOutputs.Select(po =>
{
var amount = po.OutputAmount;
var script = po.BuildOutputScript().Script;
return new Transaction.Output(amount, Transaction.Output.LatestPkScriptVersion, script);
}).ToArray();

TransactionAuthor.InputSource inputSource = async targetAmount =>
{
// TODO: calculate estimated fee
EstimatedFee = 0;
EstimatedRemainingBalance = 0;
var inputs = new Transaction.Input[0];
// TODO: don't hardcode confs
var funding = await walletClient.FundTransactionAsync(SelectedAccount.Account, targetAmount, 1);
if (funding.Item2 >= targetAmount)
{
inputs = funding.Item1.Select(o =>
Transaction.Input.CreateFromPrefix(new Transaction.OutPoint(o.TransactionHash, o.OutputIndex, o.Tree),
TransactionRules.MaxInputSequence)).ToArray();
}
return Tuple.Create(funding.Item2, inputs);
};

// TODO: only make executable if we know the transaction can be created
FinishCreateTransaction.Executable = true;
try
{
var r = await TransactionAuthor.BuildUnsignedTransaction(outputs, _changeScript,
TransactionFees.DefaultFeePerKb, inputSource);
SetPendingTransaction(r.Item1, r.Item2, outputs.Sum(o => o.Amount));
}
else
catch (Exception ex)
{
EstimatedFee = null;
EstimatedRemainingBalance = null;
FinishCreateTransaction.Executable = false;
UnsetPendingTransaction();

// Insufficient funds will need a nicer error displayed somehow. For now, hide it
// while disabling the UI to create the transaction. All other errors are unexpected.
if (!(ex is InsufficientFundsException)) throw;
}
}

private void UnsetPendingTransaction()
{
_pendingTransaction = null;
EstimatedFee = null;
EstimatedRemainingBalance = null;
FinishCreateTransaction.Executable = false;
}

private void SetPendingTransaction(Transaction unsignedTransaction, Amount inputAmount, Amount targetOutput)
{
var wallet = App.Current.Synchronizer.Wallet;
if (wallet == null)
return;

var actualFee = TransactionFees.ActualFee(unsignedTransaction, inputAmount);
var totalAccountBalance = wallet.LookupAccountProperties(SelectedAccount.Account).TotalBalance;

_pendingTransaction = unsignedTransaction;

EstimatedFee = actualFee;
EstimatedRemainingBalance = totalAccountBalance - targetOutput - actualFee;
FinishCreateTransaction.Executable = true;
}

private void FinishCreateTransactionAction()
{
try
Expand Down Expand Up @@ -273,55 +356,16 @@ private void SignTransaction(bool publish)
var shell = ViewModelLocator.ShellViewModel as ShellViewModel;
if (shell != null)
{
Func<string, Task> action = (passphrase) => SignTransactionWithPassphrase(passphrase, outputs, publish);
Func<string, Task> action =
passphrase => SignTransactionWithPassphrase(passphrase, _pendingTransaction, publish);
shell.VisibleDialogContent = new PassphraseDialogViewModel(shell, "Enter passphrase to sign transaction", "Sign", action);
}
}

private async Task SignTransactionWithPassphrase(string passphrase, Transaction.Output[] outputs, bool publishImmediately)
private async Task SignTransactionWithPassphrase(string passphrase, Transaction tx, bool publishImmediately)
{
var walletClient = App.Current.Synchronizer.WalletRpcClient;
var requiredConfirmations = 1; // TODO: Don't hardcode confs.
var targetAmount = outputs.Sum(o => o.Amount);
var targetFee = (Amount)1e6; // TODO: Don't hardcode fee/kB.
var funding = await walletClient.FundTransactionAsync(SelectedAccount.Account, targetAmount + targetFee, requiredConfirmations);
var fundingAmount = funding.Item2;
if (fundingAmount < targetAmount + targetFee)
{
MessageBox.Show($"Transaction requires {(Amount)(targetAmount + targetFee)} input value but only {fundingAmount} is spendable.",
"Insufficient funds to create transaction.");
return;
}

var selectedOutputs = funding.Item1;
var inputs = selectedOutputs
.Select(o =>
{
var prevOutPoint = new Transaction.OutPoint(o.TransactionHash, o.OutputIndex, 0);
return Transaction.Input.CreateFromPrefix(prevOutPoint, TransactionRules.MaxInputSequence);
})
.ToArray();

// TODO: Port the fee estimation logic from btcwallet. Using a hardcoded fee is unacceptable.
var estimatedFee = targetFee;

var changePkScript = funding.Item3;
if (changePkScript != null)
{
// Change output amount is calculated by solving for changeAmount with the equation:
// estimatedFee = fundingAmount - (targetAmount + changeAmount)
var changeOutput = new Transaction.Output(fundingAmount - targetAmount - estimatedFee,
Transaction.Output.LatestPkScriptVersion, changePkScript.Script);
var outputsList = outputs.ToList();
// TODO: Randomize change output position.
outputsList.Add(changeOutput);
outputs = outputsList.ToArray();
}

// TODO: User may want to set the locktime.
var unsignedTransaction = new Transaction(Transaction.SupportedVersion, inputs, outputs, 0, 0);

var signingResponse = await walletClient.SignTransactionAsync(passphrase, unsignedTransaction);
var signingResponse = await walletClient.SignTransactionAsync(passphrase, tx);
var complete = signingResponse.Item2;
if (!complete)
{
Expand All @@ -330,18 +374,19 @@ private async Task SignTransactionWithPassphrase(string passphrase, Transaction.
}
var signedTransaction = signingResponse.Item1;

MessageBox.Show($"Created tx with {estimatedFee} fee.");

if (!publishImmediately)
{
MessageBox.Show("Reviewing signed transaction before publishing is not implemented yet.");
return;
}

// TODO: The client just deserialized the transaction, so serializing it is a
// little silly. This could be optimized.
await walletClient.PublishTransactionAsync(signedTransaction.Serialize());
MessageBox.Show("Published transaction.");

_pendingTransaction = null;
_changeScript = null;
PendingOutputs.Clear();
AddPendingOutput();
}
}
}

0 comments on commit 991e971

Please sign in to comment.