/// <summary>
/// Begins synchronization of the client with the remote wallet process.
/// A delegate must be passed to be connected to the wallet's ChangesProcessed event to avoid
/// a race where additional notifications are processed in the sync task before the caller
/// can connect the event. The caller is responsible for disconnecting the delegate from the
/// event handler when finished.
/// </summary>
/// <param name="walletEventHandler">Event handler for changes to wallet as new transactions are processed.</param>
/// <returns>The synced Wallet and the Task that is keeping the wallet in sync.</returns>
public async Task<Tuple<Mutex<Wallet>, Task>> Synchronize(EventHandler<Wallet.ChangesProcessedEventArgs> walletEventHandler)
{
if (walletEventHandler == null)
throw new ArgumentNullException(nameof(walletEventHandler));
TransactionNotifications notifications;
Task notificationsTask;
// TODO: Initialization requests need timeouts.
// Loop until synchronization did not race on a reorg.
while (true)
{
// Begin receiving notifications for new and removed wallet transactions before
// old transactions are downloaded. Any received notifications are saved to
// a buffer and are processed after GetAllTransactionsAsync is awaited.
notifications = new TransactionNotifications(_channel, _tokenSource.Token);
notificationsTask = notifications.ListenAndBuffer();
var networkTask = NetworkAsync();
var accountsTask = AccountsAsync();
var networkResp = await networkTask;
var activeBlockChain = BlockChainIdentity.FromNetworkBits(networkResp.ActiveNetwork);
var txSetTask = GetTransactionsAsync(Wallet.MinRecentTransactions, Wallet.NumRecentBlocks(activeBlockChain));
var txSet = await txSetTask;
var rpcAccounts = await accountsTask;
var lastAccountBlockHeight = rpcAccounts.CurrentBlockHeight;
var lastAccountBlockHash = new Blake256Hash(rpcAccounts.CurrentBlockHash.ToByteArray());
var lastTxBlock = txSet.MinedTransactions.LastOrDefault();
if (lastTxBlock != null)
{
var lastTxBlockHeight = lastTxBlock.Height;
var lastTxBlockHash = lastTxBlock.Hash;
if (lastTxBlockHeight > lastAccountBlockHeight ||
(lastTxBlockHeight == lastAccountBlockHeight && !lastTxBlockHash.Equals(lastAccountBlockHash)))
{
_tokenSource.Cancel();
continue;
}
}
// Read all received notifications thus far and determine if synchronization raced
// on a chain reorganize. Try again if so.
IList<WalletChanges> transactionNotifications;
if (notifications.Buffer.TryReceiveAll(out transactionNotifications))
{
if (transactionNotifications.Any(r => r.DetachedBlocks.Count != 0))
{
_tokenSource.Cancel();
continue;
}
// Skip all attached block notifications that are in blocks lower than the
// block accounts notification. If blocks exist at or past that height,
// the first's hash should equal that from the accounts notification.
//
// This ensures that both notifications contain data that is valid at this
// block.
var remainingNotifications = transactionNotifications
.SelectMany(r => r.AttachedBlocks)
.SkipWhile(b => b.Height < lastAccountBlockHeight)
.ToList();
if (remainingNotifications.Count != 0)
{
if (!remainingNotifications[0].Hash.Equals(lastAccountBlockHash))
{
_tokenSource.Cancel();
continue;
}
}
// TODO: Merge remaining notifications with the transaction set.
// For now, be lazy and start the whole sync over.
if (remainingNotifications.Count > 1)
{
_tokenSource.Cancel();
continue;
}
}
var accounts = rpcAccounts.Accounts.ToDictionary(
a => new Account(a.AccountNumber),
a => new AccountProperties
{
AccountName = a.AccountName,
TotalBalance = a.TotalBalance,
// TODO: uncomment when added to protospec and implemented by wallet.
//ImmatureCoinbaseReward = a.ImmatureBalance,
ExternalKeyCount = a.ExternalKeyCount,
InternalKeyCount = a.InternalKeyCount,
ImportedKeyCount = a.ImportedKeyCount,
});
Func<AccountsResponse.Types.Account, AccountProperties> createProperties = a => new AccountProperties
{
AccountName = a.AccountName,
TotalBalance = a.TotalBalance,
// TODO: uncomment when added to protospec and implemented by wallet.
//ImmatureCoinbaseReward = a.ImmatureBalance,
ExternalKeyCount = a.ExternalKeyCount,
InternalKeyCount = a.InternalKeyCount,
ImportedKeyCount = a.ImportedKeyCount,
};
// This assumes that all but the last account listed in the RPC response are
// BIP0032 accounts, with the same account number as their List index.
var bip0032Accounts = rpcAccounts.Accounts.Take(rpcAccounts.Accounts.Count - 1).Select(createProperties).ToList();
var importedAccount = createProperties(rpcAccounts.Accounts.Last());
var chainTip = new BlockIdentity(lastAccountBlockHash, lastAccountBlockHeight);
var wallet = new Wallet(activeBlockChain, txSet, bip0032Accounts, importedAccount, chainTip);
wallet.ChangesProcessed += walletEventHandler;
var walletMutex = new Mutex<Wallet>(wallet);
var syncTask = Task.Run(async () =>
{
var client = new WalletService.WalletServiceClient(_channel);
var accountsStream = client.AccountNotifications(new AccountNotificationsRequest(), cancellationToken: _tokenSource.Token);
var accountChangesTask = accountsStream.ResponseStream.MoveNext();
var txChangesTask = notifications.Buffer.OutputAvailableAsync();
while (true)
{
var completedTask = await Task.WhenAny(accountChangesTask, txChangesTask);
if (!await completedTask)
{
break;
}
using (var walletGuard = await walletMutex.LockAsync())
{
var w = walletGuard.Instance;
if (completedTask == accountChangesTask)
{
var accountProperties = accountsStream.ResponseStream.Current;
var account = new Account(accountProperties.AccountNumber);
w.UpdateAccountProperties(account, accountProperties.AccountName,
accountProperties.ExternalKeyCount, accountProperties.InternalKeyCount,
accountProperties.ImportedKeyCount);
accountChangesTask = accountsStream.ResponseStream.MoveNext();
}
else if (completedTask == txChangesTask)
{
var changes = notifications.Buffer.Receive();
w.ApplyTransactionChanges(changes);
txChangesTask = notifications.Buffer.OutputAvailableAsync();
}
}
}
await notificationsTask;
});
return Tuple.Create(walletMutex, syncTask);
}
}