Commit 36cf64fb authored by Dan Gershony's avatar Dan Gershony Committed by GitHub

Merge pull request #52 from bokobza/feature/build-transaction

Sending funds
parents 86d060a2 3c3778b8
...@@ -148,9 +148,17 @@ POST /wallet/send-transaction - Attempts to send a transaction ...@@ -148,9 +148,17 @@ POST /wallet/send-transaction - Attempts to send a transaction
### Parameters ### Parameters
``` ```
{ {
"password": "password" "password": "123456",
"folderPath": "Wallets", // optional, if the folder path is not the default one
"name": "myWallet"
} }
``` ```
### Response
```
200 (OK)
```
## POST /wallet/recover - Recovers the wallet ## POST /wallet/recover - Recovers the wallet
### Parameters ### Parameters
``` ```
...@@ -158,11 +166,16 @@ POST /wallet/send-transaction - Attempts to send a transaction ...@@ -158,11 +166,16 @@ POST /wallet/send-transaction - Attempts to send a transaction
"network": "main", // "main" or "testnet" "network": "main", // "main" or "testnet"
"password": "password", "password": "password",
"mnemonic": "foo bar buz", "mnemonic": "foo bar buz",
"creationTime": "2017-02-03" // DateTimeOffset.ParseExact("1998-01-01", "yyyy-MM-dd", CultureInfo.InvariantCulture), utc time "name": "testwallet-recovered",
"folderPath": "Wallets", // optional, if the folder path is not the default one
"creationTime": "2017-02-25 16:20:33" // date from which to start looking for transactions
} }
``` ```
### Response ### Response
Cannot check if the password is good or not. If the password is wrong it'll recover a wallet with the wrong password. Cannot check if the password is good or not. If the password is wrong it'll recover a wallet with the wrong password.
```
200 (OK)
```
## DELETE /wallet - Deletes the wallet ## DELETE /wallet - Deletes the wallet
Works as expected. Works as expected.
......
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20170106-08" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20170106-08" />
<PackageReference Include="NStratis" Version="3.0.2.17" /> <PackageReference Include="NStratis" Version="3.0.2.23" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0-beta5-build1225" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0-beta5-build1225" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" /> <PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.2.0-beta5-build3474" /> <PackageReference Include="xunit" Version="2.2.0-beta5-build3474" />
<PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.0" /> <PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.0" />
<PackageReference Include="Moq" Version="4.7.8" /> <PackageReference Include="Moq" Version="4.7.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>
...@@ -66,12 +66,8 @@ namespace Breeze.Api.Tests ...@@ -66,12 +66,8 @@ namespace Breeze.Api.Tests
// Assert // Assert
mockWalletWrapper.VerifyAll(); mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result); var viewResult = Assert.IsType<OkResult>(result);
Assert.NotNull(viewResult.Value); Assert.Equal(200, viewResult.StatusCode);
Assert.IsType<WalletModel>(viewResult.Value);
var model = viewResult.Value as WalletModel;
Assert.Equal("Main", model.Network);
} }
[Fact] [Fact]
...@@ -97,12 +93,8 @@ namespace Breeze.Api.Tests ...@@ -97,12 +93,8 @@ namespace Breeze.Api.Tests
// Assert // Assert
mockWalletWrapper.VerifyAll(); mockWalletWrapper.VerifyAll();
var viewResult = Assert.IsType<JsonResult>(result); var viewResult = Assert.IsType<OkResult>(result);
Assert.NotNull(viewResult.Value); Assert.Equal(200, viewResult.StatusCode);
Assert.IsType<WalletModel>(viewResult.Value);
var model = viewResult.Value as WalletModel;
Assert.Equal("Main", model.Network);
} }
[Fact] [Fact]
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"variables": [], "variables": [],
"info": { "info": {
"name": "Wallet", "name": "Wallet",
"_postman_id": "57013f2c-02dc-df32-41e9-6e4aaa14ad5e", "_postman_id": "b5720ab4-24a5-6957-0ea6-766a9cbaf488",
"description": "Requests relating to operations on the wallet", "description": "Requests relating to operations on the wallet",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
}, },
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{ \n\t\"password\": \"123456\",\n\t\"network\": \"Main\",\n\t\"folderPath\": \"Wallets\",\n\t\"name\": \"myFirstWallet\"\n}" "raw": "{ \n\t\"password\": \"123456\",\n\t\"network\": \"testnet\",\n\t\"name\": \"testwallet\"\n}"
}, },
"description": "" "description": ""
}, },
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\r\n \"password\": \"password\",\r\n \"address\": \"1FYp9uguYCz7DgSF9jTWDeZF8kdRKQTXPg\",\r\n \"amount\": \"0.12\",\r\n \"feeType\": \"low\",\r\n \"allowUnconfirmed\": \"true\"\r\n}" "raw": "{\r\n\t\"walletName\": \"testwallet\",\r\n\t\"accountName\": \"account 0\",\r\n\t\"coinType\": 1,\r\n \"password\": \"password\",\r\n \"destinationAddress\": \"1FYp9uguYCz7DgSF9jTWDeZF8kdRKQTXPg\",\r\n \"amount\": \"0.12\",\r\n \"feeType\": \"low\",\r\n \"allowUnconfirmed\": \"true\"\r\n}"
}, },
"description": "" "description": ""
}, },
......
...@@ -78,14 +78,9 @@ namespace Breeze.Wallet.Controllers ...@@ -78,14 +78,9 @@ namespace Breeze.Wallet.Controllers
{ {
// get the wallet folder // get the wallet folder
DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath); DirectoryInfo walletFolder = GetWalletFolder(request.FolderPath);
Wallet wallet = this.walletManager.LoadWallet(request.Password, walletFolder.FullName, request.Name); Wallet wallet = this.walletManager.LoadWallet(request.Password, walletFolder.FullName, request.Name);
return this.Json(new WalletModel
{ return this.Ok();
Network = wallet.Network.Name,
// Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
FileName = wallet.WalletFilePath
});
} }
catch (FileNotFoundException e) catch (FileNotFoundException e)
{ {
...@@ -122,18 +117,12 @@ namespace Breeze.Wallet.Controllers ...@@ -122,18 +117,12 @@ namespace Breeze.Wallet.Controllers
{ {
// get the wallet folder // get the wallet folder
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, null, request.CreationDate); Wallet wallet = this.walletManager.RecoverWallet(request.Password, walletFolder.FullName, request.Name, request.Network, request.Mnemonic, null, request.CreationDate);
// start syncing the wallet from the creation date // start syncing the wallet from the creation date
this.tracker.SyncFrom(request.CreationDate); this.tracker.SyncFrom(request.CreationDate);
return this.Json(new WalletModel return this.Ok();
{
Network = wallet.Network.Name,
// Addresses = wallet.GetFirstNAddresses(10).Select(a => a.ToWif()),
FileName = wallet.WalletFilePath
});
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
{ {
...@@ -287,12 +276,11 @@ namespace Breeze.Wallet.Controllers ...@@ -287,12 +276,11 @@ namespace Breeze.Wallet.Controllers
try try
{ {
var transaction = this.walletManager.BuildTransaction(request.WalletName, request.AccountName, request.CoinType, request.Password, request.DestinationAddress, request.Amount, request.FeeType, request.AllowUnconfirmed); var transactionResult = this.walletManager.BuildTransaction(request.WalletName, request.AccountName, request.CoinType, request.Password, request.DestinationAddress, request.Amount, request.FeeType, request.AllowUnconfirmed);
var fee = transaction.TotalOut - request.Amount;
var model = new WalletBuildTransactionModel var model = new WalletBuildTransactionModel
{ {
Hex = transaction.ToHex(), Hex = transactionResult.hex,
Fee = fee Fee = transactionResult.fee
}; };
return this.Json(model); return this.Json(model);
} }
......
...@@ -135,25 +135,28 @@ namespace Breeze.Wallet ...@@ -135,25 +135,28 @@ namespace Breeze.Wallet
/// <param name="feeType">The type of fee to be included.</param> /// <param name="feeType">The type of fee to be included.</param>
/// <param name="allowUnconfirmed">Whether or not we allow this transaction to rely on unconfirmed outputs.</param> /// <param name="allowUnconfirmed">Whether or not we allow this transaction to rely on unconfirmed outputs.</param>
/// <returns></returns> /// <returns></returns>
NBitcoin.Transaction BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed); (string hex, Money fee) BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed);
/// <summary>
/// Sends a transaction to the network.
/// </summary>
/// <param name="transactionHex">The hex of the transaction.</param>
/// <returns></returns>
bool SendTransaction(string transactionHex); bool SendTransaction(string transactionHex);
/// <summary> /// <summary>
/// Processes a block received from the network. /// Processes a block received from the network.
/// </summary> /// </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="height">The height of the block in the blockchain.</param>
/// <param name="block">The block.</param> /// <param name="block">The block.</param>
void ProcessBlock(CoinType coinType, int height, Block block); void ProcessBlock(int height, Block block);
/// <summary> /// <summary>
/// Processes a transaction received from the network. /// Processes a transaction received from the network.
/// </summary> /// </summary>
/// <param name="coinType">The type of coin this transaction relates to.</param>
/// <param name="transaction">The transaction.</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="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> /// <param name="blockTime">The block time.</param>
void ProcessTransaction(CoinType coinType, NBitcoin.Transaction transaction, int? blockHeight = null, uint? blockTime = null); void ProcessTransaction(Transaction transaction, int? blockHeight = null, uint? blockTime = null);
} }
} }
...@@ -9,13 +9,11 @@ namespace Breeze.Wallet.Notifications ...@@ -9,13 +9,11 @@ namespace Breeze.Wallet.Notifications
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 IWalletManager walletManager;
public BlockObserver(ConcurrentChain chain, CoinType coinType, IWalletManager walletManager) public BlockObserver(ConcurrentChain chain, IWalletManager walletManager)
{ {
this.chain = chain; this.chain = chain;
this.coinType = coinType;
this.walletManager = walletManager; this.walletManager = walletManager;
} }
...@@ -28,7 +26,7 @@ namespace Breeze.Wallet.Notifications ...@@ -28,7 +26,7 @@ namespace Breeze.Wallet.Notifications
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.walletManager.ProcessBlock(this.coinType, height, block); this.walletManager.ProcessBlock(height, block);
} }
} }
} }
...@@ -8,13 +8,10 @@ namespace Breeze.Wallet.Notifications ...@@ -8,13 +8,10 @@ namespace Breeze.Wallet.Notifications
/// </summary> /// </summary>
public class TransactionObserver : SignalObserver<Transaction> public class TransactionObserver : SignalObserver<Transaction>
{ {
private readonly CoinType coinType;
private readonly IWalletManager walletManager; private readonly IWalletManager walletManager;
public TransactionObserver(CoinType coinType, IWalletManager walletManager) public TransactionObserver(IWalletManager walletManager)
{ {
this.coinType = coinType;
this.walletManager = walletManager; this.walletManager = walletManager;
} }
...@@ -24,7 +21,7 @@ namespace Breeze.Wallet.Notifications ...@@ -24,7 +21,7 @@ namespace Breeze.Wallet.Notifications
/// <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.walletManager.ProcessTransaction(this.coinType, transaction); this.walletManager.ProcessTransaction(transaction);
} }
} }
} }
...@@ -38,9 +38,9 @@ namespace Breeze.Wallet ...@@ -38,9 +38,9 @@ namespace Breeze.Wallet
await this.WaitForChainDownloadAsync(); await this.WaitForChainDownloadAsync();
// subscribe to receiving blocks and transactions // subscribe to receiving blocks and transactions
BlockSubscriber sub = new BlockSubscriber(this.signals.Blocks, new BlockObserver(this.chain, this.coinType, this.walletManager)); BlockSubscriber sub = new BlockSubscriber(this.signals.Blocks, new BlockObserver(this.chain, this.walletManager));
sub.Subscribe(); sub.Subscribe();
TransactionSubscriber txSub = new TransactionSubscriber(this.signals.Transactions, new TransactionObserver(this.coinType, this.walletManager)); TransactionSubscriber txSub = new TransactionSubscriber(this.signals.Transactions, new TransactionObserver(this.walletManager));
txSub.Subscribe(); txSub.Subscribe();
// start syncing blocks // start syncing blocks
......
...@@ -84,6 +84,20 @@ namespace Breeze.Wallet ...@@ -84,6 +84,20 @@ namespace Breeze.Wallet
} }
return result; return result;
} }
/// <summary>
/// Gets all the pub keys conatined in this wallet.
/// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns>
public IEnumerable<Script> GetAllPubKeysByCoinType(CoinType coinType)
{
var accounts = this.GetAccountsByCoinType(coinType).ToList();
foreach (var address in accounts.SelectMany(a => a.ExternalAddresses).Concat(accounts.SelectMany(a => a.InternalAddresses)))
{
yield return address.ScriptPubKey;
}
}
} }
/// <summary> /// <summary>
...@@ -107,7 +121,7 @@ namespace Breeze.Wallet ...@@ -107,7 +121,7 @@ namespace Breeze.Wallet
/// The accounts used in the wallet. /// The accounts used in the wallet.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "accounts")] [JsonProperty(PropertyName = "accounts")]
public IEnumerable<HdAccount> Accounts { get; set; } public ICollection<HdAccount> Accounts { get; set; }
/// <summary> /// <summary>
/// Gets the first account that contains no transaction. /// Gets the first account that contains no transaction.
...@@ -142,6 +156,9 @@ namespace Breeze.Wallet ...@@ -142,6 +156,9 @@ namespace Breeze.Wallet
} }
return account; return account;
} }
} }
/// <summary> /// <summary>
...@@ -210,13 +227,13 @@ namespace Breeze.Wallet ...@@ -210,13 +227,13 @@ namespace Breeze.Wallet
/// The list of external addresses, typically used for receiving money. /// The list of external addresses, typically used for receiving money.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "externalAddresses")] [JsonProperty(PropertyName = "externalAddresses")]
public IEnumerable<HdAddress> ExternalAddresses { get; set; } public ICollection<HdAddress> ExternalAddresses { get; set; }
/// <summary> /// <summary>
/// The list of internal addresses, typically used to receive change. /// The list of internal addresses, typically used to receive change.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "internalAddresses")] [JsonProperty(PropertyName = "internalAddresses")]
public IEnumerable<HdAddress> InternalAddresses { get; set; } public ICollection<HdAddress> InternalAddresses { get; set; }
/// <summary> /// <summary>
/// Gets the type of coin this account is for. /// Gets the type of coin this account is for.
...@@ -357,7 +374,7 @@ namespace Breeze.Wallet ...@@ -357,7 +374,7 @@ namespace Breeze.Wallet
/// A list of transactions involving this address. /// A list of transactions involving this address.
/// </summary> /// </summary>
[JsonProperty(PropertyName = "transactions")] [JsonProperty(PropertyName = "transactions")]
public IEnumerable<TransactionData> Transactions { get; set; } public ICollection<TransactionData> Transactions { get; set; }
} }
/// <summary> /// <summary>
......
...@@ -7,7 +7,9 @@ using Breeze.Wallet.Helpers; ...@@ -7,7 +7,9 @@ using Breeze.Wallet.Helpers;
using Breeze.Wallet.Models; using Breeze.Wallet.Models;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using NBitcoin.Protocol;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stratis.Bitcoin.Connection;
using Transaction = NBitcoin.Transaction; using Transaction = NBitcoin.Transaction;
namespace Breeze.Wallet namespace Breeze.Wallet
...@@ -19,10 +21,6 @@ namespace Breeze.Wallet ...@@ -19,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;
...@@ -31,12 +29,16 @@ namespace Breeze.Wallet ...@@ -31,12 +29,16 @@ namespace Breeze.Wallet
private readonly CoinType coinType; private readonly CoinType coinType;
private readonly ConnectionManager connectionManager;
private Dictionary<Script, ICollection<TransactionData>> keysLookup;
/// <summary> /// <summary>
/// Occurs when a transaction is found. /// Occurs when a transaction is found.
/// </summary> /// </summary>
public event EventHandler<TransactionFoundEventArgs> TransactionFound; public event EventHandler<TransactionFoundEventArgs> TransactionFound;
public WalletManager(ConcurrentChain chain, Network netwrok) public WalletManager(ConnectionManager connectionManager, Network netwrok)
{ {
this.Wallets = new List<Wallet>(); this.Wallets = new List<Wallet>();
...@@ -46,12 +48,13 @@ namespace Breeze.Wallet ...@@ -46,12 +48,13 @@ namespace Breeze.Wallet
this.Load(this.DeserializeWallet(path)); this.Load(this.DeserializeWallet(path));
} }
this.connectionManager = connectionManager;
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;
} }
...@@ -84,7 +87,7 @@ namespace Breeze.Wallet ...@@ -84,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;
...@@ -129,7 +132,7 @@ namespace Breeze.Wallet ...@@ -129,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;
...@@ -231,7 +234,7 @@ namespace Breeze.Wallet ...@@ -231,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;
} }
...@@ -284,14 +287,14 @@ namespace Breeze.Wallet ...@@ -284,14 +287,14 @@ namespace Breeze.Wallet
BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, i, isChange, network); BitcoinPubKeyAddress address = this.GenerateAddress(account.ExtendedPubKey, i, isChange, network);
// add address details // add address details
addresses = addresses.Concat(new[] {new HdAddress addresses.Add(new HdAddress
{ {
Index = i, Index = i,
HdPath = CreateBip44Path(account.GetCoinType(), account.Index, i, isChange), HdPath = CreateBip44Path(account.GetCoinType(), account.Index, i, isChange),
ScriptPubKey = address.ScriptPubKey, ScriptPubKey = address.ScriptPubKey,
Address = address.ToString(), Address = address.ToString(),
Transactions = new List<TransactionData>() Transactions = new List<TransactionData>()
}}); });
addressesCreated.Add(address.ToString()); addressesCreated.Add(address.ToString());
} }
...@@ -322,7 +325,7 @@ namespace Breeze.Wallet ...@@ -322,7 +325,7 @@ namespace Breeze.Wallet
} }
/// <inheritdoc /> /// <inheritdoc />
public Transaction BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed) public (string hex, Money fee) BuildTransaction(string walletName, string accountName, CoinType coinType, string password, string destinationAddress, Money amount, string feeType, bool allowUnconfirmed)
{ {
if (amount == Money.Zero) if (amount == Money.Zero)
{ {
...@@ -385,7 +388,7 @@ namespace Breeze.Wallet ...@@ -385,7 +388,7 @@ namespace Breeze.Wallet
throw new Exception("Could not build transaction, please make sure you entered the correct data."); throw new Exception("Could not build transaction, please make sure you entered the correct data.");
} }
return tx; return (tx.ToHex(), calculationResult.fee);
} }
/// <summary> /// <summary>
...@@ -411,25 +414,36 @@ namespace Breeze.Wallet ...@@ -411,25 +414,36 @@ namespace Breeze.Wallet
return (transactionsToUse, fee); return (transactionsToUse, fee);
} }
/// <inheritdoc />
public bool SendTransaction(string transactionHex) public bool SendTransaction(string transactionHex)
{ {
throw new System.NotImplementedException(); // TODO move this to a behavior on the full node
// parse transaction
Transaction transaction = Transaction.Parse(transactionHex);
TxPayload payload = new TxPayload(transaction);
foreach (var node in this.connectionManager.ConnectedNodes)
{
node.SendMessage(payload);
}
return true;
} }
/// <inheritdoc /> /// <inheritdoc />
public void ProcessBlock(CoinType coinType, 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)
{ {
this.ProcessTransaction(coinType, transaction, height, block.Header.Time); this.ProcessTransaction(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)
{ {
foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == coinType)) foreach (var accountRoot in wallet.AccountsRoot.Where(a => a.CoinType == this.coinType))
{ {
accountRoot.LastBlockSyncedHeight = height; accountRoot.LastBlockSyncedHeight = height;
} }
...@@ -437,37 +451,35 @@ namespace Breeze.Wallet ...@@ -437,37 +451,35 @@ namespace Breeze.Wallet
} }
/// <inheritdoc /> /// <inheritdoc />
public void ProcessTransaction(CoinType coinType, Transaction transaction, int? blockHeight = null, uint? blockTime = null) public void ProcessTransaction(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: {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);
if (utxo != null) if (utxo != null)
{ {
AddTransactionToWallet(coinType, 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(coinType, transaction.GetHash(), transaction.Time, null, -tTx.Amount, pubKey, blockHeight, blockTime, tTx.Hash, tTx.Index);
}
}
} }
} }
/// <summary> /// <summary>
/// Adds the transaction to the wallet. /// Adds the transaction to the wallet.
/// </summary> /// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <param name="transactionHash">The transaction hash.</param> /// <param name="transactionHash">The transaction hash.</param>
/// <param name="time">The time.</param> /// <param name="time">The time.</param>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
...@@ -477,29 +489,10 @@ namespace Breeze.Wallet ...@@ -477,29 +489,10 @@ namespace Breeze.Wallet
/// <param name="blockTime">The block time.</param> /// <param name="blockTime">The block time.</param>
/// <param name="spendingTransactionId">The id of the transaction containing the output being spent, if this is a spending transaction.</param> /// <param name="spendingTransactionId">The id of the transaction containing the output being spent, 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> /// <param name="spendingTransactionIndex">The index of the output in the transaction being referenced, if this is a spending transaction.</param>
private void AddTransactionToWallet(CoinType coinType, 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
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)) this.keysLookup.TryGetValue(script, out ICollection<TransactionData> trans);
{ trans.Add(new TransactionData
address.Transactions = address.Transactions.Concat(new[]
{
new TransactionData
{ {
Amount = amount, Amount = amount,
BlockHeight = blockHeight, BlockHeight = blockHeight,
...@@ -507,55 +500,55 @@ namespace Breeze.Wallet ...@@ -507,55 +500,55 @@ namespace Breeze.Wallet
Id = transactionHash, Id = transactionHash,
CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time), CreationTime = DateTimeOffset.FromUnixTimeMilliseconds(blockTime ?? time),
Index = index Index = index
}
}); });
// notify a transaction has been found
this.TransactionFound?.Invoke(this, new TransactionFoundEventArgs(wallet, accountRoot.CoinType, account, address, false));
}
// if this is a spending transaction, mark the spent transaction as such // if this is a spending transaction, mark the spent transaction as such
if (spendingTransactionId != null) if (spendingTransactionId != null)
{ {
var transactions = account.GetTransactionsById(spendingTransactionId); var transactions = this.keysLookup.Values.SelectMany(v => v).Where(t => t.Id == spendingTransactionId);
if (transactions.Any()) if (transactions.Any())
{ {
transactions.Single(t => t.Index == spendingTransactionIndex).SpentInTransaction = transactionHash; transactions.Single(t => t.Index == spendingTransactionIndex).SpentInTransaction = transactionHash;
} }
} }
}
} // notify a transaction has been found
this.TransactionFound?.Invoke(this, new TransactionFoundEventArgs(script, transactionHash));
} }
this.TrackedTransactions.Add(new TransactionDetails private void OnTransactionFound(object sender, TransactionFoundEventArgs a)
{ {
Hash = transactionHash, foreach (Wallet wallet in this.Wallets)
Index = index, {
Amount = amount foreach (var account in wallet.GetAccountsByCoinType(this.coinType))
}); {
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
private void OnTransactionFound(object sender, TransactionFoundEventArgs a)
{ {
Console.WriteLine("event raised"); continue;
}
var wallet = this.Wallets.Single(w => w.Name == a.WalletName);
var accountsRoot = wallet.AccountsRoot.Single(ar => ar.CoinType == a.CoinType);
var account = accountsRoot.Accounts.Single(acc => acc.Name == a.AccountName);
// 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 />
...@@ -703,39 +696,24 @@ namespace Breeze.Wallet ...@@ -703,39 +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> /// </summary>
/// <param name="coinType">Type of the coin.</param>
/// <returns></returns> /// <returns></returns>
private HashSet<Script> LoadKeys(CoinType coinType) private void LoadKeysLookup()
{ {
return new HashSet<Script>(this.Wallets. this.keysLookup = new Dictionary<Script, ICollection<TransactionData>>();
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)). foreach (var wallet in this.Wallets)
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. var accounts = wallet.GetAccountsByCoinType(this.coinType);
SelectMany(w => w.AccountsRoot.Where(a => a.CoinType == coinType)). foreach (var account in accounts)
SelectMany(a => a.Accounts).
SelectMany(a => a.ExternalAddresses).
SelectMany(t => t.Transactions).
Select(t => new TransactionDetails
{ {
Hash = t.Id, var addresses = account.ExternalAddresses.Concat(account.InternalAddresses);
Index = t.Index, foreach (var address in addresses)
Amount = t.Amount {
})); this.keysLookup.Add(address.ScriptPubKey, address.Transactions);
}
}
}
} }
/// <summary> /// <summary>
...@@ -767,19 +745,14 @@ namespace Breeze.Wallet ...@@ -767,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(Wallet wallet, CoinType coinType, HdAccount account, HdAddress address, bool isChange) public TransactionFoundEventArgs(Script script, uint256 transactionHash)
{ {
this.WalletName = wallet.Name; this.Script = script;
this.CoinType = coinType; this.TransactionHash = transactionHash;
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