Commit a17f8ca6 authored by Jeremy Bokobza's avatar Jeremy Bokobza

Added Tracker object whose role is to figure out from where in the chain do we need to sync blocks.

parent a48af926
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
namespace Breeze.Wallet
{
public static class ChainExtensions
{
/// <summary>
/// Determines whether the chain is downloaded and up to date.
/// </summary>
/// <param name="chain">The chain.</param>
public static bool IsDownloaded(this ConcurrentChain chain)
{
return chain.Tip.Header.BlockTime.ToUnixTimeSeconds() > (DateTimeOffset.Now.ToUnixTimeSeconds() - TimeSpan.FromHours(1).TotalSeconds);
}
/// <summary>
/// Gets the type of the coin this chain relates to.
/// Obviously this method and how we figure out what coin we're on needs to be revisited.
/// </summary>
/// <param name="chain">The chain.</param>
/// <returns></returns>
/// <exception cref="System.Exception">No support for this coin.</exception>
public static CoinType GetCoinType(this ConcurrentChain chain)
{
uint256 genesis = chain.Genesis.Header.GetHash();
switch (genesis.ToString())
{
case "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f":
return CoinType.Bitcoin;
case "b0e511e965aeb40614ca65a1b79bd6e4e7ef299fa23e575a64b079691e9d4690":
return CoinType.Stratis;
case "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943":
return CoinType.Testnet;
default:
throw new Exception("No support for this coin.");
}
}
}
}
......@@ -2,12 +2,10 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Security;
using Breeze.Wallet.Errors;
using Microsoft.AspNetCore.Mvc;
using Breeze.Wallet.Models;
using Breeze.Wallet.Wrappers;
using NBitcoin;
namespace Breeze.Wallet.Controllers
......@@ -122,6 +120,9 @@ namespace Breeze.Wallet.Controllers
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.RecoverWallet(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic);
// TODO give the tracker the date at which this wallet was originally created so that it can start syncing blocks for it
return this.Json(new WalletModel
{
Network = wallet.Network.Name,
......@@ -372,23 +373,11 @@ namespace Breeze.Wallet.Controllers
{
if (string.IsNullOrEmpty(folderPath))
{
folderPath = GetDefaultWalletFolderPath();
folderPath = WalletManager.GetDefaultWalletFolderPath();
}
return Directory.CreateDirectory(folderPath);
}
/// <summary>
/// Gets the path of the default folder in which the wallets will be stored.
/// </summary>
/// <returns>The folder path for Windows, Linux or OSX systems.</returns>
private static string GetDefaultWalletFolderPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"{Environment.GetEnvironmentVariable("AppData")}\Breeze";
}
return $"{Environment.GetEnvironmentVariable("HOME")}/.breeze";
}
}
}
using System.Threading.Tasks;
using NBitcoin;
namespace Breeze.Wallet
{
public interface ITracker
{
/// <summary>
/// Initializes the tracker.
/// </summary>
/// <returns></returns>
Task Initialize();
/// <summary>
/// Waits for the chain to download.
/// </summary>
/// <returns></returns>
Task WaitForChainDownloadAsync();
}
}
......@@ -66,7 +66,7 @@ namespace Breeze.Wallet
/// Creates the new address.
/// </summary>
/// <param name="walletName">The name of the wallet in which this address will be created.</param>
/// <param name="coinType">the type of coin for which to create an account.</param>
/// <param name="coinType">The type of coin for which to create an account.</param>
/// <param name="accountName">The name of the account in which this address will be created.</param>
/// <returns>The new address, in Base58 format.</returns>
string CreateNewAddress(string walletName, CoinType coinType, string accountName);
......@@ -80,5 +80,20 @@ namespace Breeze.Wallet
WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType, bool allowUnconfirmed);
bool SendTransaction(string transactionHex);
/// <summary>
/// Processes a block received from the network.
/// </summary>
/// <param name="coinType">The type of coin this block relates to.</param>
/// <param name="height">The height of the block in the blockchain.</param>
/// <param name="block">The block.</param>
void ProcessBlock(CoinType coinType, int height, Block block);
/// <summary>
/// Processes a transaction received from the network.
/// </summary>
/// <param name="coinType">The type of coin this transaction relates to.</param>
/// <param name="transaction">The transaction.</param>
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction);
}
}
using NBitcoin;
using Stratis.Bitcoin;
using Breeze.Wallet.Wrappers;
using Stratis.Bitcoin.Builder;
namespace Breeze.Wallet.Notifications
{
......@@ -9,27 +7,28 @@ namespace Breeze.Wallet.Notifications
/// Observer that receives notifications about the arrival of new <see cref="Block"/>s.
/// </summary>
public class BlockObserver : SignalObserver<Block>
{
private readonly ConcurrentChain chain;
{
private readonly ConcurrentChain chain;
private readonly CoinType coinType;
private readonly IWalletManager walletManager;
private readonly ITrackerWrapper trackerWrapper;
public BlockObserver(ConcurrentChain chain, CoinType coinType, IWalletManager walletManager)
{
this.chain = chain;
this.coinType = coinType;
this.walletManager = walletManager;
}
public BlockObserver(ConcurrentChain chain, ITrackerWrapper trackerWrapper)
{
this.chain = chain;
this.trackerWrapper = trackerWrapper;
}
/// <summary>
/// Manages what happens when a new block is received.
/// </summary>
/// <param name="block">The new block</param>
protected override void OnNextCore(Block block)
{
protected override void OnNextCore(Block block)
{
var hash = block.Header.GetHash();
var height = this.chain.GetBlock(hash).Height;
this.trackerWrapper.NotifyAboutBlock(height, block);
}
}
this.walletManager.ProcessBlock(this.coinType, height, block);
}
}
}
using NBitcoin;
using Stratis.Bitcoin;
using Breeze.Wallet.Wrappers;
namespace Breeze.Wallet.Notifications
{
......@@ -8,21 +7,24 @@ namespace Breeze.Wallet.Notifications
/// Observer that receives notifications about the arrival of new <see cref="Transaction"/>s.
/// </summary>
public class TransactionObserver : SignalObserver<Transaction>
{
private readonly ITrackerWrapper trackerWrapper;
{
private readonly CoinType coinType;
private readonly IWalletManager walletManager;
public TransactionObserver(CoinType coinType, IWalletManager walletManager)
{
this.coinType = coinType;
this.walletManager = walletManager;
}
public TransactionObserver(ITrackerWrapper trackerWrapper)
{
this.trackerWrapper = trackerWrapper;
}
/// <summary>
/// Manages what happens when a new transaction is received.
/// </summary>
/// <param name="transaction">The new transaction</param>
protected override void OnNextCore(Transaction transaction)
{
this.trackerWrapper.NotifyAboutTransaction(transaction);
}
}
protected override void OnNextCore(Transaction transaction)
{
this.walletManager.ProcessTransaction(this.coinType, transaction);
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Breeze.Wallet.Notifications;
using NBitcoin;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Notifications;
using Stratis.Bitcoin.Utilities;
namespace Breeze.Wallet
{
public class Tracker : ITracker
{
private readonly WalletManager walletManager;
private readonly ConcurrentChain chain;
private readonly Signals signals;
private readonly BlockNotification blockNotification;
private readonly CoinType coinType;
public Tracker(IWalletManager walletManager, ConcurrentChain chain, Signals signals, BlockNotification blockNotification)
{
this.walletManager = walletManager as WalletManager;
this.chain = chain;
this.signals = signals;
this.blockNotification = blockNotification;
this.coinType = chain.GetCoinType();
}
/// <inheritdoc />
public async Task Initialize()
{
// get the chain headers. This needs to be up-to-date before we really do anything
await this.WaitForChainDownloadAsync();
// subscribe to receiving blocks and transactions
BlockSubscriber sub = new BlockSubscriber(this.signals.Blocks, new BlockObserver(this.chain, this.coinType, this.walletManager));
sub.Subscribe();
TransactionSubscriber txSub = new TransactionSubscriber(this.signals.Transactions, new TransactionObserver(this.coinType, this.walletManager));
txSub.Subscribe();
// start syncing blocks
this.blockNotification.SyncFrom(this.chain.GetBlock(this.FindBestHeightForSyncing()).HashBlock);
}
private int FindBestHeightForSyncing()
{
// if there are no wallets, get blocks from now
if (!this.walletManager.Wallets.Any())
{
return this.chain.Tip.Height;
}
// sync the accounts with new blocks, starting from the most out of date
int? syncFromHeight = this.walletManager.Wallets.Min(w => w.AccountsRoot.Single(a => a.CoinType == this.coinType).LastBlockSyncedHeight);
if (syncFromHeight == null)
{
return this.chain.Tip.Height;
}
return Math.Min(syncFromHeight.Value, this.chain.Tip.Height);
}
/// <inheritdoc />
public Task WaitForChainDownloadAsync()
{
// make sure the chain is downloaded
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
return AsyncLoop.Run("WalletFeature.DownloadChain", token =>
{
// wait until the chain is downloaded. We wait until a block is from an hour ago.
if (this.chain.IsDownloaded())
{
cancellationTokenSource.Cancel();
}
return Task.CompletedTask;
},
cancellationTokenSource.Token,
repeatEvery: TimeSpans.FiveSeconds);
}
private bool BlocksSynced()
{
return this.walletManager.Wallets.All(w => w.AccountsRoot.Single(a => a.CoinType == this.coinType).LastBlockSyncedHeight == this.chain.Tip.Height);
}
}
}
......@@ -69,6 +69,12 @@ namespace Breeze.Wallet
[JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; }
/// <summary>
/// The height of the last block that was synced.
/// </summary>
[JsonProperty(PropertyName = "lastBlockSyncedHeight", NullValueHandling = NullValueHandling.Ignore)]
public int? LastBlockSyncedHeight { get; set; }
/// <summary>
/// The accounts used in the wallet.
/// </summary>
......@@ -79,9 +85,22 @@ namespace Breeze.Wallet
/// <summary>
/// The type of coin, as specified in BIP44.
/// </summary>
/// <remarks>For more, see https://github.com/satoshilabs/slips/blob/master/slip-0044.md</remarks>
public enum CoinType
{
/// <summary>
/// Bitcoin
/// </summary>
Bitcoin = 0,
/// <summary>
/// Testnet (all coins)
/// </summary>
Testnet = 1,
/// <summary>
/// Stratis
/// </summary>
Stratis = 105
}
......
using Stratis.Bitcoin.Builder.Feature;
using Breeze.Wallet.Controllers;
using Breeze.Wallet.Notifications;
using Breeze.Wallet.Wrappers;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using Stratis.Bitcoin;
......@@ -11,23 +9,24 @@ namespace Breeze.Wallet
{
public class WalletFeature : FullNodeFeature
{
private readonly ITrackerWrapper trackerWrapper;
private readonly Signals signals;
private readonly ConcurrentChain chain;
public WalletFeature(ITrackerWrapper trackerWrapper, Signals signals, ConcurrentChain chain)
private readonly ITracker tracker;
private readonly IWalletManager walletManager;
public WalletFeature(ITracker tracker, IWalletManager walletManager)
{
this.trackerWrapper = trackerWrapper;
this.signals = signals;
this.chain = chain;
this.tracker = tracker;
this.walletManager = walletManager;
}
public override void Start()
{
this.tracker.Initialize();
}
public override void Stop()
{
BlockSubscriber sub = new BlockSubscriber(signals.Blocks, new BlockObserver(chain, trackerWrapper));
sub.Subscribe();
TransactionSubscriber txSub = new TransactionSubscriber(signals.Transactions, new TransactionObserver(trackerWrapper));
txSub.Subscribe();
this.walletManager.Dispose();
base.Stop();
}
}
......@@ -41,7 +40,7 @@ namespace Breeze.Wallet
.AddFeature<WalletFeature>()
.FeatureServices(services =>
{
services.AddSingleton<ITrackerWrapper, TrackerWrapper>();
services.AddSingleton<ITracker, Tracker>();
services.AddSingleton<IWalletManager, WalletManager>();
services.AddSingleton<WalletController>();
});
......
......@@ -2,11 +2,12 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models;
using NBitcoin;
using Newtonsoft.Json;
using Stratis.Bitcoin.Utilities;
using Transaction = NBitcoin.Transaction;
namespace Breeze.Wallet
{
......@@ -16,10 +17,16 @@ namespace Breeze.Wallet
public class WalletManager : IWalletManager
{
public List<Wallet> Wallets { get; }
public WalletManager()
{
{
this.Wallets = new List<Wallet>();
// find wallets and load them in memory
foreach (var path in this.GetWalletFilesPaths())
{
this.Load(this.GetWallet(path));
}
}
/// <inheritdoc />
......@@ -37,6 +44,7 @@ namespace Breeze.Wallet
ExtKey extendedKey = mnemonic.DeriveExtKey(passphrase);
// create a wallet file
Wallet wallet = this.GenerateWalletFile(password, folderPath, name, WalletHelpers.GetNetwork(network), extendedKey);
this.Load(wallet);
......@@ -48,11 +56,8 @@ namespace Breeze.Wallet
{
string walletFilePath = Path.Combine(folderPath, $"{name}.json");
if (!File.Exists(walletFilePath))
throw new FileNotFoundException($"No wallet file found at {walletFilePath}");
// load the file from the local system
Wallet wallet = JsonConvert.DeserializeObject<Wallet>(File.ReadAllText(walletFilePath));
Wallet wallet = this.GetWallet(walletFilePath);
this.Load(wallet);
return wallet;
......@@ -199,8 +204,7 @@ namespace Breeze.Wallet
throw new System.NotImplementedException();
}
public WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType,
bool allowUnconfirmed)
public WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType, bool allowUnconfirmed)
{
throw new System.NotImplementedException();
}
......@@ -210,6 +214,27 @@ namespace Breeze.Wallet
throw new System.NotImplementedException();
}
/// <inheritdoc />
public void ProcessBlock(CoinType coinType, int height, Block block)
{
Console.WriteLine($"block notification: height: {height}, block hash: {block.Header.GetHash()}, coin type: {coinType}");
// update the wallets with the last processed block height
foreach (var wallet in this.Wallets)
{
foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == coinType))
{
accountRoot.LastBlockSyncedHeight = height;
}
}
}
/// <inheritdoc />
public void ProcessTransaction(CoinType coinType, Transaction transaction)
{
Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}, coin type: {coinType}");
}
/// <inheritdoc />
public void DeleteWallet(string walletFilePath)
{
......@@ -219,7 +244,11 @@ namespace Breeze.Wallet
/// <inheritdoc />
public void Dispose()
{
// TODO Safely persist the wallet before disposing
// safely persist the wallets to the file system before disposing
foreach (var wallet in this.Wallets)
{
this.SaveToFile(wallet);
}
}
/// <summary>
......@@ -230,7 +259,6 @@ namespace Breeze.Wallet
/// <param name="name">The name of the wallet.</param>
/// <param name="network">The network this wallet is for.</param>
/// <param name="extendedKey">The root key used to generate keys.</param>
/// <param name="coinType">The type of coin for which this wallet is created.</param>
/// <param name="creationTime">The time this wallet was created.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
......@@ -250,8 +278,10 @@ namespace Breeze.Wallet
Network = network,
AccountsRoot = new List<AccountRoot> {
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Bitcoin },
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Testnet },
new AccountRoot { Accounts = new List<HdAccount>(), CoinType = CoinType.Stratis} },
WalletFilePath = walletFilePath
WalletFilePath = walletFilePath,
};
// create a folder if none exists and persist the file
......@@ -270,16 +300,33 @@ namespace Breeze.Wallet
File.WriteAllText(wallet.WalletFilePath, JsonConvert.SerializeObject(wallet, Formatting.Indented));
}
/// <summary>
/// Gets the wallet located at the specified path.
/// </summary>
/// <param name="walletFilePath">The wallet file path.</param>
/// <returns></returns>
/// <exception cref="System.IO.FileNotFoundException"></exception>
private Wallet GetWallet(string walletFilePath)
{
if (!File.Exists(walletFilePath))
throw new FileNotFoundException($"No wallet file found at {walletFilePath}");
// load the file from the local system
return JsonConvert.DeserializeObject<Wallet>(File.ReadAllText(walletFilePath));
}
/// <summary>
/// Loads the wallet to be used by the manager.
/// </summary>
/// <param name="wallet">The wallet to load.</param>
private void Load(Wallet wallet)
{
if (this.Wallets.All(w => w.Name != wallet.Name))
if (this.Wallets.Any(w => w.Name == wallet.Name))
{
this.Wallets.Add(wallet);
return;
}
this.Wallets.Add(wallet);
}
private BitcoinPubKeyAddress GenerateAddress(string accountExtPubKey, int index, bool isChange, Network network)
......@@ -290,6 +337,14 @@ namespace Breeze.Wallet
return extPubKey.PubKey.GetAddress(network);
}
private IEnumerable<string> GetWalletFilesPaths()
{
// TODO look in user-chosen folder as well.
// maybe the api can maintain a list of wallet paths it knows about
var defaultFolderPath = GetDefaultWalletFolderPath();
return Directory.EnumerateFiles(defaultFolderPath, "*.json", SearchOption.TopDirectoryOnly);
}
/// <summary>
/// Creates the bip44 path.
/// </summary>
......@@ -306,5 +361,19 @@ namespace Breeze.Wallet
int change = isChange ? 1 : 0;
return $"m/44'/{(int)coinType}'/{accountIndex}'/{change}/{addressIndex}";
}
/// <summary>
/// Gets the path of the default folder in which the wallets will be stored.
/// </summary>
/// <returns>The folder path for Windows, Linux or OSX systems.</returns>
public static string GetDefaultWalletFolderPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"{Environment.GetEnvironmentVariable("AppData")}\Breeze";
}
return $"{Environment.GetEnvironmentVariable("HOME")}/.breeze";
}
}
}
using NBitcoin;
namespace Breeze.Wallet.Wrappers
{
public interface ITrackerWrapper
{
void NotifyAboutBlock(int height, Block block);
void NotifyAboutTransaction(Transaction transaction);
uint256 GetLastProcessedBlock();
}
}
using NBitcoin;
using System;
namespace Breeze.Wallet.Wrappers
{
public class TrackerWrapper : ITrackerWrapper
{
// private readonly Tracker tracker;
public TrackerWrapper(Network network)
{
//this.tracker = new Tracker(network);
}
/// <summary>
/// Get the hash of the last block that has been succesfully processed.
/// </summary>
/// <returns>The hash of the block</returns>
public uint256 GetLastProcessedBlock()
{
// TODO use Tracker.BestHeight. Genesis hash for now.
return uint256.Parse("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f");
}
public void NotifyAboutBlock(int height, Block block)
{
// this.tracker.AddOrReplaceBlock(new Height(height), block);
Console.WriteLine($"block notification: height: {height}, block hash: {block.Header.GetHash()}");
}
public void NotifyAboutTransaction(Transaction transaction)
{
// TODO what should the height be? is it necessary?
// this.tracker.ProcessTransaction(new SmartTransaction(transaction, new Height(0)));
Console.WriteLine($"transaction notification: tx hash {transaction.GetHash()}");
}
}
}
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