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 67e7966
Show file tree
Hide file tree
Showing 8 changed files with 256 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
5 changes: 5 additions & 0 deletions Paymetheus.Bitcoin/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,10 @@ public void SerializeTo(byte[] destination, int offset = 0)
}
cursor.WriteUInt32(LockTime);
}

public static int EstimateSerializeSize(int inputCount, int outputCount) =>
TransactionRules.TransactionOverhead +
TransactionRules.InputOverhead * inputCount +
TransactionRules.OutputOverhead * outputCount;
}
}
26 changes: 26 additions & 0 deletions Paymetheus.Bitcoin/TransactionRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public static class TransactionRules

public static readonly Amount MaxOutputValue = (long)21e6 * (long)1e8;

public const int TransactionOverhead = 4 + 4 + 1 + 1;
public const int RedeemPayToPubKeyHashSigScriptOverhead = 1 + 70 + 1 + 33 + 1;
public const int InputOverhead = 32 + 4 + 4 + RedeemPayToPubKeyHashSigScriptOverhead;
public const int PayToPubKeyHashScriptOverhead = 1 + 1 + 1 + 20 + 1 + 1;
public const int OutputOverhead = 8 + 1 + PayToPubKeyHashScriptOverhead;

/// <summary>
/// Perform a preliminary check that the Amount is within the bounds of valid
/// transaction output values. Additional checks are required before authoring
Expand All @@ -25,6 +31,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 + InputOverhead;

// 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.Length);
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.Length + 1);
var feeEstimate = FeeForSerializeSize(feePerKb, txSerializeSizeEstimate);

var totalNonChangeOutput = tx.Outputs.Aggregate((Amount)0, (acc, output) => acc + output.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 67e7966

Please sign in to comment.