Skip to content

Commit

Permalink
Add transaction fee estimation and show fee in view.
Browse files Browse the repository at this point in the history
Closes #10.
  • Loading branch information
jrick committed Feb 24, 2016
1 parent 9c1841e commit 702aaa0
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 81 deletions.
13 changes: 13 additions & 0 deletions Paymetheus.Bitcoin/Errors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2016 The btcsuite developers
// Licensed under the ISC license. See LICENSE file in the project root for full license information.

using System;

namespace Paymetheus.Bitcoin
{
internal static class Errors
{
public static ArgumentOutOfRangeException RequireNonNegative(string paramName) =>
new ArgumentOutOfRangeException(paramName, "Non-negative number required.");
}
}
2 changes: 2 additions & 0 deletions Paymetheus.Bitcoin/Paymetheus.Bitcoin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<Compile Include="BlockChainConsistencyException.cs" />
<Compile Include="BlockChainIdentity.cs" />
<Compile Include="BlockIdentity.cs" />
<Compile Include="Errors.cs" />
<Compile Include="TransactionRules.cs" />
<Compile Include="Util\Base58.cs" />
<Compile Include="Util\ByteCursor.cs" />
Expand All @@ -81,6 +82,7 @@
<Compile Include="Wallet\Checksum.cs" />
<Compile Include="Ripemd160Hash.cs" />
<Compile Include="Wallet\Accounting.cs" />
<Compile Include="Wallet\TransactionFees.cs" />
<Compile Include="Wallet\PgpWordList.cs" />
<Compile Include="Wallet\PgpWordListData.cs" />
<Compile Include="Wallet\TransactionSet.cs" />
Expand Down
38 changes: 38 additions & 0 deletions Paymetheus.Bitcoin/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Paymetheus.Bitcoin.Util;
using System;
using System.Linq;

namespace Paymetheus.Bitcoin
{
Expand Down Expand Up @@ -188,5 +189,42 @@ public void SerializeTo(byte[] destination, int offset = 0)
}
cursor.WriteUInt32(LockTime);
}

private const int RedeemPayToPubKeyHashSigScriptSize = 1 + 72 + 1 + 33 + 1;
private const int PayToPubKeyHashPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1;

// Worst case sizes for compressed p2pkh inputs and outputs.
// Used for estimating an unsigned transaction's worst case serialize size
// after it has been signed.
public const int RedeemPayToPubKeyHashInputSize = 32 + 4 + 4 + RedeemPayToPubKeyHashSigScriptSize;
public const int PayToPubKeyHashOutputSize = 8 + 1 + PayToPubKeyHashPkScriptSize;

/// <summary>
/// Estimates the signed serialize size of an unsigned transaction, assuming
/// all inputs spend P2PKH outputs.
/// </summary>
/// <param name="inputCount">The number of P2PKH outputs the transaction will redeem</param>
/// <param name="outputs"></param>
/// <param name="addChangeOutput">Add the serialize size for an additional P2PKH change output</param>
/// <returns></returns>
public static int EstimateSerializeSize(int inputCount, Output[] outputs, bool addChangeOutput)
{
if (outputs == null)
throw new ArgumentNullException(nameof(outputs));

var outputCount = outputs.Length;

var estimatedSize =
8 + CompactInt.SerializeSize((ulong)inputCount) + CompactInt.SerializeSize((ulong)outputCount) +
inputCount * RedeemPayToPubKeyHashInputSize +
outputs.Aggregate(0, (a, o) => a + o.SerializeSize);

if (addChangeOutput)
{
estimatedSize += PayToPubKeyHashOutputSize;
}

return estimatedSize;
}
}
}
20 changes: 20 additions & 0 deletions Paymetheus.Bitcoin/TransactionRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ public static class TransactionRules
/// </summary>
public static bool IsSaneOutputValue(Amount a) => a >= 0 && a <= MaxOutputValue;

/// <summary>
/// Check whether an output is considered dust for a given transaction relay fee.
/// Transactions with dust outputs are rejected by mempool.
/// </summary>
/// <param name="output">Transaction output to check</param>
/// <param name="relayFeePerKb">Mempool relay fee/kB</param>
/// <returns>Whether the output is dust</returns>
public static bool IsDust(Transaction.Output output, Amount relayFeePerKb)
{
// TODO: Rather than assumming the output is P2PKH and using the size of a
// P2PKH input script to estimate the total cost to the network, a better
// estimate could be used if the output script is one of the other recognized
// script kinds.
var totalSize = output.SerializeSize + Transaction.RedeemPayToPubKeyHashInputSize;

// Dust is defined as an output value where the total cost to the network
// (output size + input size) is greater than 1/3 of the relay fee.
return output.Amount * 1000 / (3 * totalSize) < relayFeePerKb;
}

public static void CheckSanity(Transaction tx)
{
if (tx == null)
Expand Down
89 changes: 89 additions & 0 deletions Paymetheus.Bitcoin/Wallet/TransactionFees.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2016 The btcsuite developers
// Licensed under the ISC license. See LICENSE file in the project root for full license information.

using Paymetheus.Bitcoin.Script;
using System;
using System.Linq;

namespace Paymetheus.Bitcoin.Wallet
{
public static class TransactionFees
{
public static readonly Amount DefaultFeePerKb = 1000;

public static Amount FeeForSerializeSize(Amount feePerKb, int txSerializeSize)
{
if (feePerKb < 0)
throw Errors.RequireNonNegative(nameof(feePerKb));
if (txSerializeSize < 0)
throw Errors.RequireNonNegative(nameof(txSerializeSize));

var fee = feePerKb * txSerializeSize / 1000;

if (fee == 0 && feePerKb > 0)
fee = feePerKb;

if (!TransactionRules.IsSaneOutputValue(fee))
throw new TransactionRuleException($"Fee of {fee} is invalid");

return fee;
}

public static Amount ActualFee(Transaction tx, Amount totalInput)
{
if (tx == null)
throw new ArgumentNullException(nameof(tx));
if (totalInput < 0)
throw Errors.RequireNonNegative(nameof(totalInput));

var totalOutput = tx.Outputs.Aggregate((Amount)0, (a, o) => a + o.Amount);
return totalInput - totalOutput;
}

public static Amount EstimatedFeePerKb(Transaction tx, Amount totalInput)
{
if (tx == null)
throw new ArgumentNullException(nameof(tx));
if (totalInput < 0)
throw Errors.RequireNonNegative(nameof(totalInput));

var estimatedSize = Transaction.EstimateSerializeSize(tx.Inputs.Length, tx.Outputs, false);
var actualFee = ActualFee(tx, totalInput);
return actualFee * 1000 / estimatedSize;
}

/// <summary>
/// Potentially adds a change output to a transaction to set an appropiate fee.
/// </summary>
public static Transaction AddChange(Transaction tx, Amount totalInput, OutputScript changeScript, Amount feePerKb)
{
if (tx == null)
throw new ArgumentNullException(nameof(tx));
if (totalInput < 0)
throw Errors.RequireNonNegative(nameof(totalInput));
if (changeScript == null)
throw new ArgumentNullException(nameof(changeScript));
if (feePerKb < 0)
throw Errors.RequireNonNegative(nameof(feePerKb));

var txSerializeSizeEstimate = Transaction.EstimateSerializeSize(tx.Inputs.Length, tx.Outputs, true);
var feeEstimate = FeeForSerializeSize(feePerKb, txSerializeSizeEstimate);

var totalNonChangeOutput = tx.Outputs.Aggregate((Amount)0, (a, o) => a + o.Amount);
var changeAmount = totalInput - totalNonChangeOutput - feeEstimate;
var changeOutput = new Transaction.Output(changeAmount, changeScript.Script);

// Change should not be created if the change output itself would be considered dust.
if (TransactionRules.IsDust(changeOutput, feePerKb))
{
return tx;
}

var outputList = tx.Outputs.ToList();
outputList.Add(changeOutput); // TODO: Randomize change output position.
var outputs = outputList.ToArray();

return new Transaction(tx.Version, tx.Inputs, outputs, tx.LockTime);
}
}
}
11 changes: 3 additions & 8 deletions Paymetheus.Rpc/WalletClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,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 @@ -281,17 +281,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
Loading

0 comments on commit 702aaa0

Please sign in to comment.