Commit 0a7b7a87 authored by Jeremy Bokobza's avatar Jeremy Bokobza Committed by GitHub

Merge pull request #36 from bokobza/feature/walletmanager

Added Tracker object to manage syncing of blocks
parents 36d50d41 c3c089a3
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,22 @@ 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>
/// <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);
}
}
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
}
......@@ -191,19 +210,27 @@ namespace Breeze.Wallet
/// Transaction id.
/// </summary>
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 Id { get; set; }
/// <summary>
/// The transaction amount.
/// </summary>
[JsonProperty(PropertyName = "amount")]
[JsonConverter(typeof(MoneyJsonConverter))]
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>
/// The height of the block including this transaction.
/// </summary>
[JsonProperty(PropertyName = "blockHeight")]
public int BlockHeight { get; set; }
public int? BlockHeight { get; set; }
/// <summary>
/// Whether this transaction has been confirmed or not.
......
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>();
});
......
This diff is collapsed.
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