Commit c3c089a3 authored by Jeremy Bokobza's avatar Jeremy Bokobza

Added synchronization of blocks and transactions into the relevant addresses.

parent a17f8ca6
...@@ -94,6 +94,8 @@ namespace Breeze.Wallet ...@@ -94,6 +94,8 @@ namespace Breeze.Wallet
/// </summary> /// </summary>
/// <param name="coinType">The type of coin this transaction relates to.</param> /// <param name="coinType">The type of coin this transaction relates to.</param>
/// <param name="transaction">The transaction.</param> /// <param name="transaction">The transaction.</param>
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction); /// <param name="blockHeight">The height of the block this transaction came from. Null if it was not a transaction included in a block.</param>
/// <param name="blockTime">The block time.</param>
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction, int? blockHeight = null, uint? blockTime = null);
} }
} }
...@@ -210,19 +210,27 @@ namespace Breeze.Wallet ...@@ -210,19 +210,27 @@ namespace Breeze.Wallet
/// Transaction id. /// Transaction id.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "id")] [JsonProperty(PropertyName = "id")]
public string Id { get; set; } [JsonConverter(typeof(UInt256JsonConverter))]
public uint256 Id { get; set; }
/// <summary> /// <summary>
/// The transaction amount. /// The transaction amount.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "amount")] [JsonProperty(PropertyName = "amount")]
[JsonConverter(typeof(MoneyJsonConverter))]
public Money Amount { get; set; } public Money Amount { get; set; }
/// <summary>
/// The index of this scriptPubKey in the transaction it is contained.
/// </summary>
[JsonProperty(PropertyName = "index")]
public int? Index { get; set; }
/// <summary> /// <summary>
/// The height of the block including this transaction. /// The height of the block including this transaction.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "blockHeight")] [JsonProperty(PropertyName = "blockHeight")]
public int BlockHeight { get; set; } public int? BlockHeight { get; set; }
/// <summary> /// <summary>
/// Whether this transaction has been confirmed or not. /// Whether this transaction has been confirmed or not.
......
...@@ -17,9 +17,13 @@ namespace Breeze.Wallet ...@@ -17,9 +17,13 @@ namespace Breeze.Wallet
public class WalletManager : IWalletManager public class WalletManager : IWalletManager
{ {
public List<Wallet> Wallets { get; } public List<Wallet> Wallets { get; }
public HashSet<Script> PubKeys { get; }
public HashSet<TransactionDetails> TrackedTransactions { get; }
public WalletManager() public WalletManager()
{ {
this.Wallets = new List<Wallet>(); this.Wallets = new List<Wallet>();
// find wallets and load them in memory // find wallets and load them in memory
...@@ -27,6 +31,11 @@ namespace Breeze.Wallet ...@@ -27,6 +31,11 @@ namespace Breeze.Wallet
{ {
this.Load(this.GetWallet(path)); this.Load(this.GetWallet(path));
} }
// load data in memory for faster lookups
// TODO get the coin type from somewhere else
this.PubKeys = this.LoadKeys(CoinType.Bitcoin);
this.TrackedTransactions = this.LoadTransactions(CoinType.Bitcoin);
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -184,8 +193,11 @@ namespace Breeze.Wallet ...@@ -184,8 +193,11 @@ namespace Breeze.Wallet
CreationTime = DateTimeOffset.Now CreationTime = DateTimeOffset.Now
}}); }});
// persists the address to the wallet file
this.SaveToFile(wallet); this.SaveToFile(wallet);
// adds the address to the list of tracked addresses
this.PubKeys.Add(address.ScriptPubKey);
return address.ToString(); return address.ToString();
} }
...@@ -219,6 +231,11 @@ namespace Breeze.Wallet ...@@ -219,6 +231,11 @@ namespace Breeze.Wallet
{ {
Console.WriteLine($"block notification: height: {height}, block hash: {block.Header.GetHash()}, coin type: {coinType}"); Console.WriteLine($"block notification: height: {height}, block hash: {block.Header.GetHash()}, coin type: {coinType}");
foreach (Transaction transaction in block.Transactions)
{
this.ProcessTransaction(coinType, transaction, height, block.Header.Time);
}
// update the wallets with the last processed block height // update the wallets with the last processed block height
foreach (var wallet in this.Wallets) foreach (var wallet in this.Wallets)
{ {
...@@ -230,9 +247,88 @@ namespace Breeze.Wallet ...@@ -230,9 +247,88 @@ namespace Breeze.Wallet
} }
/// <inheritdoc /> /// <inheritdoc />
public void ProcessTransaction(CoinType coinType, Transaction transaction) public void ProcessTransaction(CoinType coinType, Transaction transaction, int? blockHeight = null, uint? blockTime = null)
{ {
Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}, coin type: {coinType}"); Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}, coin type: {coinType}");
foreach (var k in this.PubKeys)
{
// check if the outputs contain one of our addresses
var utxo = transaction.Outputs.SingleOrDefault(o => k == o.ScriptPubKey);
if (utxo != null)
{
AddTransactionToWallet(coinType, transaction.GetHash(), transaction.Time, transaction.Outputs.IndexOf(utxo), utxo.Value, k, blockHeight, blockTime);
}
// if the inputs have a reference to a transaction containing one of our scripts
foreach (TxIn input in transaction.Inputs.Where(txIn => this.TrackedTransactions.Any(trackedTx => trackedTx.Hash == txIn.PrevOut.Hash)))
{
TransactionDetails tTx = this.TrackedTransactions.Single(trackedTx => trackedTx.Hash == input.PrevOut.Hash);
// compare the index of the output in its original transaction and the index references in the input
if (input.PrevOut.N == tTx.Index)
{
AddTransactionToWallet(coinType, transaction.GetHash(), transaction.Time, null, -tTx.Amount, k, blockHeight, blockTime);
}
}
}
}
/// <summary>
/// Adds the transaction to the wallet.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <param name="transactionHash">The transaction hash.</param>
/// <param name="time">The time.</param>
/// <param name="index">The index.</param>
/// <param name="amount">The amount.</param>
/// <param name="script">The script.</param>
/// <param name="blockHeight">Height of the block.</param>
/// <param name="blockTime">The block time.</param>
private void AddTransactionToWallet(CoinType coinType, uint256 transactionHash, uint time, int? index, Money amount, Script script, int? blockHeight = null, uint? blockTime = null)
{
// selects all the transactions we already have in the wallet
var txs = this.Wallets.
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)).
SelectMany(a => a.Accounts).
SelectMany(a => a.ExternalAddresses).
SelectMany(t => t.Transactions);
// add this transaction if it is not in the list
if (txs.All(t => t.Id != transactionHash))
{
foreach (var wallet in this.Wallets)
{
foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == coinType))
{
foreach (var account in accountRoot.Accounts)
{
foreach (var address in account.ExternalAddresses.Where(a => a.ScriptPubKey == script))
{
address.Transactions = address.Transactions.Concat(new[]
{
new TransactionData
{
Amount = amount,
BlockHeight = blockHeight,
Confirmed = blockHeight.HasValue,
Id = transactionHash,
CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time),
Index = index
}
});
}
}
}
}
this.TrackedTransactions.Add(new TransactionDetails
{
Hash = transactionHash,
Index = index,
Amount = amount
});
}
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -375,5 +471,51 @@ namespace Breeze.Wallet ...@@ -375,5 +471,51 @@ namespace Breeze.Wallet
return $"{Environment.GetEnvironmentVariable("HOME")}/.breeze"; return $"{Environment.GetEnvironmentVariable("HOME")}/.breeze";
} }
/// <summary>
/// Loads the script pub key we're tracking for faster lookups.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
private HashSet<Script> LoadKeys(CoinType coinType)
{
return new HashSet<Script>(this.Wallets.
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)).
SelectMany(a => a.Accounts).
SelectMany(a => a.ExternalAddresses).
Select(s => s.ScriptPubKey));
// uncomment the following for testing on a random address
// Select(t => (new BitcoinPubKeyAddress(t.Address, Network.Main)).ScriptPubKey));
}
/// <summary>
/// Loads the transactions we're tracking in memory for faster lookups.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
private HashSet<TransactionDetails> LoadTransactions(CoinType coinType)
{
return new HashSet<TransactionDetails>(this.Wallets.
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)).
SelectMany(a => a.Accounts).
SelectMany(a => a.ExternalAddresses).
SelectMany(t => t.Transactions).
Select(t => new TransactionDetails
{
Hash = t.Id,
Index = t.Index,
Amount = t.Amount
}));
}
}
public class TransactionDetails
{
public uint256 Hash { get; set; }
public int? Index { get; set; }
public Money Amount { get; internal set; }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment