Commit 717cc616 authored by Jeremy Bokobza's avatar Jeremy Bokobza

made the addition of transactions a bit more effective

parent bb1d5725
...@@ -21,10 +21,6 @@ namespace Breeze.Wallet ...@@ -21,10 +21,6 @@ namespace Breeze.Wallet
{ {
public List<Wallet> Wallets { get; } public List<Wallet> Wallets { get; }
public HashSet<Script> PubKeys { get; set; }
public HashSet<TransactionDetails> TrackedTransactions { get; }
private const int UnusedAddressesBuffer = 20; private const int UnusedAddressesBuffer = 20;
private const int WalletRecoveryAccountsCount = 3; private const int WalletRecoveryAccountsCount = 3;
...@@ -35,6 +31,8 @@ namespace Breeze.Wallet ...@@ -35,6 +31,8 @@ namespace Breeze.Wallet
private readonly ConnectionManager connectionManager; private readonly ConnectionManager connectionManager;
private readonly Dictionary<Script, ICollection<TransactionData>> keysLookup;
/// <summary> /// <summary>
/// Occurs when a transaction is found. /// Occurs when a transaction is found.
/// </summary> /// </summary>
...@@ -54,9 +52,9 @@ namespace Breeze.Wallet ...@@ -54,9 +52,9 @@ namespace Breeze.Wallet
this.coinType = (CoinType)netwrok.Consensus.CoinType; this.coinType = (CoinType)netwrok.Consensus.CoinType;
// load data in memory for faster lookups // load data in memory for faster lookups
// TODO get the coin type from somewhere else this.LoadKeysLookup();
this.PubKeys = this.LoadKeys(this.coinType);
this.TrackedTransactions = this.LoadTransactions(this.coinType); // register events
this.TransactionFound += this.OnTransactionFound; this.TransactionFound += this.OnTransactionFound;
} }
...@@ -89,7 +87,7 @@ namespace Breeze.Wallet ...@@ -89,7 +87,7 @@ namespace Breeze.Wallet
// save the changes to the file and add addresses to be tracked // save the changes to the file and add addresses to be tracked
this.SaveToFile(wallet); this.SaveToFile(wallet);
this.PubKeys = this.LoadKeys(this.coinType); this.LoadKeysLookup();
this.Load(wallet); this.Load(wallet);
return mnemonic; return mnemonic;
...@@ -134,7 +132,7 @@ namespace Breeze.Wallet ...@@ -134,7 +132,7 @@ namespace Breeze.Wallet
// save the changes to the file and add addresses to be tracked // save the changes to the file and add addresses to be tracked
this.SaveToFile(wallet); this.SaveToFile(wallet);
this.PubKeys = this.LoadKeys(this.coinType); this.LoadKeysLookup();
this.Load(wallet); this.Load(wallet);
return wallet; return wallet;
...@@ -236,7 +234,7 @@ namespace Breeze.Wallet ...@@ -236,7 +234,7 @@ namespace Breeze.Wallet
this.SaveToFile(wallet); this.SaveToFile(wallet);
// adds the address to the list of tracked addresses // adds the address to the list of tracked addresses
this.PubKeys = this.LoadKeys(coinType); this.LoadKeysLookup();
return account.GetFirstUnusedReceivingAddress().Address; return account.GetFirstUnusedReceivingAddress().Address;
} }
...@@ -435,7 +433,7 @@ namespace Breeze.Wallet ...@@ -435,7 +433,7 @@ namespace Breeze.Wallet
/// <inheritdoc /> /// <inheritdoc />
public void ProcessBlock(int height, Block block) public void ProcessBlock(int height, Block block)
{ {
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: {this.coinType}");
foreach (Transaction transaction in block.Transactions) foreach (Transaction transaction in block.Transactions)
{ {
...@@ -457,7 +455,8 @@ namespace Breeze.Wallet ...@@ -457,7 +455,8 @@ namespace Breeze.Wallet
{ {
Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}, coin type: {this.coinType}"); Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}, coin type: {this.coinType}");
foreach (var pubKey in this.PubKeys) // check the outputs
foreach (var pubKey in this.keysLookup.Keys)
{ {
// check if the outputs contain one of our addresses // check if the outputs contain one of our addresses
var utxo = transaction.Outputs.SingleOrDefault(o => pubKey == o.ScriptPubKey); var utxo = transaction.Outputs.SingleOrDefault(o => pubKey == o.ScriptPubKey);
...@@ -465,18 +464,16 @@ namespace Breeze.Wallet ...@@ -465,18 +464,16 @@ namespace Breeze.Wallet
{ {
AddTransactionToWallet(transaction.GetHash(), transaction.Time, transaction.Outputs.IndexOf(utxo), utxo.Value, pubKey, blockHeight, blockTime); AddTransactionToWallet(transaction.GetHash(), transaction.Time, transaction.Outputs.IndexOf(utxo), utxo.Value, pubKey, blockHeight, blockTime);
} }
}
// if the inputs have a reference to a transaction containing one of our scripts // check the inputs - include those that have a reference to a transaction containing one of our scripts and the same index
foreach (TxIn input in transaction.Inputs.Where(txIn => this.TrackedTransactions.Any(trackedTx => trackedTx.Hash == txIn.PrevOut.Hash))) foreach (TxIn input in transaction.Inputs.Where(txIn => this.keysLookup.Values.SelectMany(v => v).Any(trackedTx => trackedTx.Id == txIn.PrevOut.Hash && trackedTx.Index == txIn.PrevOut.N)))
{ {
TransactionDetails tTx = this.TrackedTransactions.Single(trackedTx => trackedTx.Hash == input.PrevOut.Hash); TransactionData tTx = this.keysLookup.Values.SelectMany(v => v).Single(trackedTx => trackedTx.Id == input.PrevOut.Hash && trackedTx.Index == input.PrevOut.N);
// compare the index of the output in its original transaction and the index references in the input // find the script this input references
if (input.PrevOut.N == tTx.Index) var keyToSpend = this.keysLookup.Single(v => v.Value.Contains(tTx)).Key;
{ AddTransactionToWallet(transaction.GetHash(), transaction.Time, null, -tTx.Amount, keyToSpend, blockHeight, blockTime, tTx.Id, tTx.Index);
AddTransactionToWallet(transaction.GetHash(), transaction.Time, null, -tTx.Amount, pubKey, blockHeight, blockTime, tTx.Hash, tTx.Index);
}
}
} }
} }
...@@ -494,81 +491,64 @@ namespace Breeze.Wallet ...@@ -494,81 +491,64 @@ namespace Breeze.Wallet
/// <param name="spendingTransactionIndex">The index of the output in the transaction being referenced, if this is a spending transaction.</param> /// <param name="spendingTransactionIndex">The index of the output in the transaction being referenced, if this is a spending transaction.</param>
private void AddTransactionToWallet(uint256 transactionHash, uint time, int? index, Money amount, Script script, int? blockHeight = null, uint? blockTime = null, uint256 spendingTransactionId = null, int? spendingTransactionIndex = null) private void AddTransactionToWallet(uint256 transactionHash, uint time, int? index, Money amount, Script script, int? blockHeight = null, uint? blockTime = null, uint256 spendingTransactionId = null, int? spendingTransactionIndex = null)
{ {
// selects all the transactions we already have in the wallet this.keysLookup.TryGetValue(script, out ICollection<TransactionData> trans);
var txs = this.Wallets.SelectMany(w => w.GetAllTransactionsByCoinType(this.coinType)); trans.Add(new TransactionData
{
// add this transaction if it is not in the list Amount = amount,
if (txs.All(t => t.Id != transactionHash || t.Index != index)) BlockHeight = blockHeight,
Confirmed = blockHeight.HasValue,
Id = transactionHash,
CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time),
Index = index
});
// if this is a spending transaction, mark the spent transaction as such
if (spendingTransactionId != null)
{ {
foreach (var wallet in this.Wallets) var transactions = this.keysLookup.Values.SelectMany(v => v).Where(t => t.Id == spendingTransactionId);
if (transactions.Any())
{ {
foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == this.coinType)) transactions.Single(t => t.Index == spendingTransactionIndex).SpentInTransaction = transactionHash;
{
foreach (var account in accountRoot.Accounts)
{
var receivingAddress = account.ExternalAddresses.SingleOrDefault(a => a.ScriptPubKey == script);
var changeAddress = account.InternalAddresses.SingleOrDefault(a => a.ScriptPubKey == script);
bool isChange = receivingAddress == null && changeAddress != null;
var address = receivingAddress ?? changeAddress;
if (address != null)
{
address.Transactions.Add(new TransactionData
{
Amount = amount,
BlockHeight = blockHeight,
Confirmed = blockHeight.HasValue,
Id = transactionHash,
CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time),
Index = index
});
// notify a transaction has been found
this.TransactionFound?.Invoke(this, new TransactionFoundEventArgs(wallet, accountRoot.CoinType, account, address, isChange));
// if this is a spending transaction, mark the spent transaction as such
if (spendingTransactionId != null)
{
var transactions = account.GetTransactionsById(spendingTransactionId);
if (transactions.Any())
{
transactions.Single(t => t.Index == spendingTransactionIndex).SpentInTransaction = transactionHash;
}
}
}
}
}
} }
this.TrackedTransactions.Add(new TransactionDetails
{
Hash = transactionHash,
Index = index,
Amount = amount
});
} }
// notify a transaction has been found
this.TransactionFound?.Invoke(this, new TransactionFoundEventArgs(script, transactionHash));
} }
private void OnTransactionFound(object sender, TransactionFoundEventArgs a) private void OnTransactionFound(object sender, TransactionFoundEventArgs a)
{ {
Console.WriteLine("event raised"); foreach (Wallet wallet in this.Wallets)
{
var wallet = this.Wallets.Single(w => w.Name == a.WalletName); foreach (var account in wallet.GetAccountsByCoinType(this.coinType))
var accountsRoot = wallet.AccountsRoot.Single(ar => ar.CoinType == a.CoinType); {
var account = accountsRoot.Accounts.Single(acc => acc.Name == a.AccountName); bool isChange;
if (account.ExternalAddresses.Any(address => address.ScriptPubKey == a.Script))
{
isChange = false;
}
else if (account.InternalAddresses.Any(address => address.ScriptPubKey == a.Script))
{
isChange = true;
}
else
{
continue;
}
// calculate how many accounts to add to keep a buffer of 20 unused addresses // calculate how many accounts to add to keep a buffer of 20 unused addresses
int lastUsedAddressIndex = account.GetLastUsedAddress(a.IsChange).Index; int lastUsedAddressIndex = account.GetLastUsedAddress(isChange).Index;
int addressesCount = a.IsChange ? account.InternalAddresses.Count() : account.ExternalAddresses.Count(); int addressesCount = isChange ? account.InternalAddresses.Count() : account.ExternalAddresses.Count();
int emptyAddressesCount = addressesCount - lastUsedAddressIndex - 1; int emptyAddressesCount = addressesCount - lastUsedAddressIndex - 1;
int accountsToAdd = UnusedAddressesBuffer - emptyAddressesCount; int accountsToAdd = UnusedAddressesBuffer - emptyAddressesCount;
this.CreateAddressesInAccount(account, wallet.Network, accountsToAdd, a.IsChange); this.CreateAddressesInAccount(account, wallet.Network, accountsToAdd, isChange);
// persists the address to the wallet file // persists the address to the wallet file
this.SaveToFile(wallet); this.SaveToFile(wallet);
}
}
// adds the address to the list of tracked addresses this.LoadKeysLookup();
this.LoadKeys(a.CoinType);
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -716,40 +696,24 @@ namespace Breeze.Wallet ...@@ -716,40 +696,24 @@ namespace Breeze.Wallet
} }
/// <summary> /// <summary>
/// Loads the script pub key we're tracking for faster lookups. /// Loads the keys and transactions we're tracking in memory for faster lookups.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
private HashSet<Script> LoadKeys(CoinType coinType)
{
var keys = new HashSet<Script>();
foreach (Wallet wallet in this.Wallets)
{
keys.UnionWith(wallet.GetAllPubKeysByCoinType(coinType));
}
return keys;
}
/// <summary>
/// Loads the transactions we're tracking in memory for faster lookups.
/// </summary> /// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns> /// <returns></returns>
private HashSet<TransactionDetails> LoadTransactions(CoinType coinType) private void LoadKeysLookup()
{ {
var keys = new HashSet<TransactionDetails>(); this.keysLookup = new Dictionary<Script, ICollection<TransactionData>>();
foreach (Wallet wallet in this.Wallets) foreach (var wallet in this.Wallets)
{ {
keys.UnionWith(wallet.GetAllTransactionsByCoinType(coinType) var accounts = wallet.GetAccountsByCoinType(this.coinType);
.Select(t => foreach (var account in accounts)
new TransactionDetails {
var addresses = account.ExternalAddresses.Concat(account.InternalAddresses);
foreach (var address in addresses)
{ {
Hash = t.Id, this.keysLookup.Add(address.ScriptPubKey, address.Transactions);
Index = t.Index, }
Amount = t.Amount }
}));
} }
return keys;
} }
/// <summary> /// <summary>
...@@ -781,19 +745,14 @@ namespace Breeze.Wallet ...@@ -781,19 +745,14 @@ namespace Breeze.Wallet
public class TransactionFoundEventArgs : EventArgs public class TransactionFoundEventArgs : EventArgs
{ {
public string WalletName { get; set; } public Script Script { get; set; }
public string AccountName { get; set; }
public CoinType CoinType { get; set; } public uint256 TransactionHash { get; set; }
public string Address { get; set; }
public bool IsChange { get; set; } public TransactionFoundEventArgs(Script script, uint256 transactionHash)
{
public TransactionFoundEventArgs(Wallet wallet, CoinType coinType, HdAccount account, HdAddress address, bool isChange) this.Script = script;
{ this.TransactionHash = transactionHash;
this.WalletName = wallet.Name;
this.CoinType = coinType;
this.AccountName = account.Name;
this.Address = address.Address;
this.IsChange = isChange;
} }
} }
} }
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