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 @@ ...@@ -2,12 +2,10 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.InteropServices;
using System.Security; using System.Security;
using Breeze.Wallet.Errors; using Breeze.Wallet.Errors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Breeze.Wallet.Models; using Breeze.Wallet.Models;
using Breeze.Wallet.Wrappers;
using NBitcoin; using NBitcoin;
namespace Breeze.Wallet.Controllers namespace Breeze.Wallet.Controllers
...@@ -122,6 +120,9 @@ namespace Breeze.Wallet.Controllers ...@@ -122,6 +120,9 @@ namespace Breeze.Wallet.Controllers
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath); DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.RecoverWallet(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic); 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 return this.Json(new WalletModel
{ {
Network = wallet.Network.Name, Network = wallet.Network.Name,
...@@ -372,23 +373,11 @@ namespace Breeze.Wallet.Controllers ...@@ -372,23 +373,11 @@ namespace Breeze.Wallet.Controllers
{ {
if (string.IsNullOrEmpty(folderPath)) if (string.IsNullOrEmpty(folderPath))
{ {
folderPath = GetDefaultWalletFolderPath(); folderPath = WalletManager.GetDefaultWalletFolderPath();
} }
return Directory.CreateDirectory(folderPath); 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 ...@@ -66,7 +66,7 @@ namespace Breeze.Wallet
/// Creates the new address. /// Creates the new address.
/// </summary> /// </summary>
/// <param name="walletName">The name of the wallet in which this address will be created.</param> /// <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> /// <param name="accountName">The name of the account in which this address will be created.</param>
/// <returns>The new address, in Base58 format.</returns> /// <returns>The new address, in Base58 format.</returns>
string CreateNewAddress(string walletName, CoinType coinType, string accountName); string CreateNewAddress(string walletName, CoinType coinType, string accountName);
...@@ -80,5 +80,22 @@ namespace Breeze.Wallet ...@@ -80,5 +80,22 @@ namespace Breeze.Wallet
WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType, bool allowUnconfirmed); WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType, bool allowUnconfirmed);
bool SendTransaction(string transactionHex); 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 NBitcoin;
using Stratis.Bitcoin; using Stratis.Bitcoin;
using Breeze.Wallet.Wrappers;
using Stratis.Bitcoin.Builder;
namespace Breeze.Wallet.Notifications namespace Breeze.Wallet.Notifications
{ {
...@@ -9,27 +7,28 @@ 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. /// Observer that receives notifications about the arrival of new <see cref="Block"/>s.
/// </summary> /// </summary>
public class BlockObserver : SignalObserver<Block> 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> /// <summary>
/// Manages what happens when a new block is received. /// Manages what happens when a new block is received.
/// </summary> /// </summary>
/// <param name="block">The new block</param> /// <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 hash = block.Header.GetHash();
var height = this.chain.GetBlock(hash).Height; var height = this.chain.GetBlock(hash).Height;
this.trackerWrapper.NotifyAboutBlock(height, block); this.walletManager.ProcessBlock(this.coinType, height, block);
} }
} }
} }
using NBitcoin; using NBitcoin;
using Stratis.Bitcoin; using Stratis.Bitcoin;
using Breeze.Wallet.Wrappers;
namespace Breeze.Wallet.Notifications namespace Breeze.Wallet.Notifications
{ {
...@@ -8,21 +7,24 @@ 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. /// Observer that receives notifications about the arrival of new <see cref="Transaction"/>s.
/// </summary> /// </summary>
public class TransactionObserver : SignalObserver<Transaction> 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> /// <summary>
/// Manages what happens when a new transaction is received. /// Manages what happens when a new transaction is received.
/// </summary> /// </summary>
/// <param name="transaction">The new transaction</param> /// <param name="transaction">The new transaction</param>
protected override void OnNextCore(Transaction transaction) protected override void OnNextCore(Transaction transaction)
{ {
this.trackerWrapper.NotifyAboutTransaction(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 ...@@ -69,6 +69,12 @@ namespace Breeze.Wallet
[JsonProperty(PropertyName = "coinType")] [JsonProperty(PropertyName = "coinType")]
public CoinType CoinType { get; set; } 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> /// <summary>
/// The accounts used in the wallet. /// The accounts used in the wallet.
/// </summary> /// </summary>
...@@ -79,9 +85,22 @@ namespace Breeze.Wallet ...@@ -79,9 +85,22 @@ namespace Breeze.Wallet
/// <summary> /// <summary>
/// The type of coin, as specified in BIP44. /// The type of coin, as specified in BIP44.
/// </summary> /// </summary>
/// <remarks>For more, see https://github.com/satoshilabs/slips/blob/master/slip-0044.md</remarks>
public enum CoinType public enum CoinType
{ {
/// <summary>
/// Bitcoin
/// </summary>
Bitcoin = 0, Bitcoin = 0,
/// <summary>
/// Testnet (all coins)
/// </summary>
Testnet = 1,
/// <summary>
/// Stratis
/// </summary>
Stratis = 105 Stratis = 105
} }
...@@ -191,19 +210,27 @@ namespace Breeze.Wallet ...@@ -191,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.
......
using Stratis.Bitcoin.Builder.Feature; using Stratis.Bitcoin.Builder.Feature;
using Breeze.Wallet.Controllers; using Breeze.Wallet.Controllers;
using Breeze.Wallet.Notifications;
using Breeze.Wallet.Wrappers;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NBitcoin; using NBitcoin;
using Stratis.Bitcoin; using Stratis.Bitcoin;
...@@ -11,23 +9,24 @@ namespace Breeze.Wallet ...@@ -11,23 +9,24 @@ namespace Breeze.Wallet
{ {
public class WalletFeature : FullNodeFeature public class WalletFeature : FullNodeFeature
{ {
private readonly ITrackerWrapper trackerWrapper; private readonly ITracker tracker;
private readonly Signals signals; private readonly IWalletManager walletManager;
private readonly ConcurrentChain chain;
public WalletFeature(ITracker tracker, IWalletManager walletManager)
public WalletFeature(ITrackerWrapper trackerWrapper, Signals signals, ConcurrentChain chain)
{ {
this.trackerWrapper = trackerWrapper; this.tracker = tracker;
this.signals = signals; this.walletManager = walletManager;
this.chain = chain;
} }
public override void Start() public override void Start()
{
this.tracker.Initialize();
}
public override void Stop()
{ {
BlockSubscriber sub = new BlockSubscriber(signals.Blocks, new BlockObserver(chain, trackerWrapper)); this.walletManager.Dispose();
sub.Subscribe(); base.Stop();
TransactionSubscriber txSub = new TransactionSubscriber(signals.Transactions, new TransactionObserver(trackerWrapper));
txSub.Subscribe();
} }
} }
...@@ -41,7 +40,7 @@ namespace Breeze.Wallet ...@@ -41,7 +40,7 @@ namespace Breeze.Wallet
.AddFeature<WalletFeature>() .AddFeature<WalletFeature>()
.FeatureServices(services => .FeatureServices(services =>
{ {
services.AddSingleton<ITrackerWrapper, TrackerWrapper>(); services.AddSingleton<ITracker, Tracker>();
services.AddSingleton<IWalletManager, WalletManager>(); services.AddSingleton<IWalletManager, WalletManager>();
services.AddSingleton<WalletController>(); 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