Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,11 @@ private void Initialize([System.Runtime.CompilerServices.CallerMemberName] strin
this.loggerFactory, DateTimeProvider.Default, walletRepository);

var reserveUtxoService = new ReserveUtxoService(this.loggerFactory, new Mock<ISignals>().Object);
var walletFeePolicy = new Mock<IWalletFeePolicy>().Object;
var broadcasterManager = new Mock<IBroadcasterManager>().Object;
var walletTransactionHandler = new WalletTransactionHandler(this.loggerFactory, this.coldStakingManager, walletFeePolicy, this.Network, new StandardTransactionPolicy(this.Network), reserveUtxoService);

var walletTransactionHandler = new WalletTransactionHandler(this.loggerFactory, this.coldStakingManager, new Mock<IWalletFeePolicy>().Object, this.Network, new StandardTransactionPolicy(this.Network), reserveUtxoService);

this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler);
this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler, walletFeePolicy, broadcasterManager);

this.asyncProvider = new AsyncProvider(this.loggerFactory, new Mock<ISignals>().Object);

Expand Down
46 changes: 46 additions & 0 deletions src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -692,5 +692,51 @@ public IEnumerable<UnspentOutputReference> GetSpendableTransactionsInColdWallet(
this.logger.LogTrace("(-):*.Count={0}", res.Count());
return res;
}

public List<Transaction> RetrieveFilteredUtxos(string walletName, string walletPassword, string transactionHex, FeeRate feeRate, string walletAccount = null)
{
var retrievalTransactions = new List<Transaction>();

Transaction transactionToReclaim = this.network.Consensus.ConsensusFactory.CreateTransaction(transactionHex);

foreach (TxOut output in transactionToReclaim.Outputs)
{
Wallet.Wallet wallet = this.GetWallet(walletName);

HdAddress address = wallet.GetAllAddresses(Wallet.Wallet.AllAccounts).FirstOrDefault(a => a.ScriptPubKey == output.ScriptPubKey);

// The address is not in the wallet so ignore this output.
if (address == null)
continue;

HdAccount destinationAccount = wallet.GetAccounts(Wallet.Wallet.NormalAccounts).First();

// This shouldn't really happen unless the user has no proper accounts in the wallet.
if (destinationAccount == null)
continue;

Script destination = destinationAccount.GetFirstUnusedReceivingAddress().ScriptPubKey;

ISecret extendedPrivateKey = wallet.GetExtendedPrivateKeyForAddress(walletPassword, address);

Key privateKey = extendedPrivateKey.PrivateKey;

var builder = new TransactionBuilder(this.network);

var coin = new Coin(transactionToReclaim, output);

builder.AddCoins(coin);
builder.AddKeys(privateKey);
builder.Send(destination, output.Value);
builder.SubtractFees();
builder.SendEstimatedFees(feeRate);

Transaction builtTransaction = builder.BuildTransaction(true);

retrievalTransactions.Add(builtTransaction);
}

return retrievalTransactions;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,33 @@ namespace Stratis.Bitcoin.Features.ColdStaking.Controllers
public class ColdStakingController : Controller
{
public ColdStakingManager ColdStakingManager { get; private set; }

private readonly IWalletTransactionHandler walletTransactionHandler;
private readonly IWalletFeePolicy walletFeePolicy;
private readonly IBroadcasterManager broadcasterManager;

/// <summary>Instance logger.</summary>
private readonly ILogger logger;

public ColdStakingController(
ILoggerFactory loggerFactory,
IWalletManager walletManager,
IWalletTransactionHandler walletTransactionHandler)
IWalletTransactionHandler walletTransactionHandler,
IWalletFeePolicy walletFeePolicy,
IBroadcasterManager broadcasterManager)
{
Guard.NotNull(loggerFactory, nameof(loggerFactory));
Guard.NotNull(walletManager, nameof(walletManager));
Guard.NotNull(walletTransactionHandler, nameof(walletTransactionHandler));
Guard.NotNull(walletFeePolicy, nameof(walletFeePolicy));
Guard.NotNull(broadcasterManager, nameof(broadcasterManager));

this.ColdStakingManager = walletManager as ColdStakingManager;
Guard.NotNull(this.ColdStakingManager, nameof(this.ColdStakingManager));

this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.walletTransactionHandler = walletTransactionHandler;
this.walletFeePolicy = walletFeePolicy;
this.broadcasterManager = broadcasterManager;
}

/// <summary>
Expand Down Expand Up @@ -589,5 +597,45 @@ public IActionResult EstimateColdStakingWithdrawalFee([FromBody] ColdStakingWith
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}

[Route("retrieve-filtered-utxos")]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public IActionResult RetrieveFilteredUtxos([FromBody] RetrieveFilteredUtxosRequest request)
{
Guard.NotNull(request, nameof(request));

// Checks the request is valid.
if (!this.ModelState.IsValid)
{
this.logger.LogTrace("(-)[MODEL_STATE_INVALID]");
return ModelStateErrors.BuildErrorResponse(this.ModelState);
}

try
{
FeeRate feeRate = this.walletFeePolicy.GetFeeRate(FeeType.High.ToConfirmations());

List<Transaction> retrievalTransactions = this.ColdStakingManager.RetrieveFilteredUtxos(request.WalletName, request.WalletPassword, request.Hex, feeRate, request.WalletAccount);

if (request.Broadcast)
{
foreach (Transaction transaction in retrievalTransactions)
{
this.broadcasterManager.BroadcastTransactionAsync(transaction);
}
}

return this.Json(retrievalTransactions.Select(t => t.ToHex()));
}
catch (Exception e)
{
this.logger.LogError("Exception occurred: {0}", e.ToString());
this.logger.LogTrace("(-)[ERROR]");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,38 @@ public override string ToString()
return $"{nameof(this.TransactionHex)}={this.TransactionHex}";
}
}

public class RetrieveFilteredUtxosRequest
{
/// <summary>The wallet name.</summary>
[Required]
[JsonProperty(PropertyName = "walletName")]
public string WalletName { get; set; }

/// <summary>The wallet password.</summary>
[Required]
[JsonProperty(PropertyName = "walletPassword")]
public string WalletPassword { get; set; }

/// <summary>
/// The (optional) account for the retrieved UTXOs to be sent back to.
/// If this is not specified, the first available non-coldstaking account will be used.
/// </summary>
[JsonProperty(PropertyName = "walletAccount")]
public string WalletAccount { get; set; }

/// <summary>
/// The hex of the transaction to retrieve the UTXOs for.
/// Only UTXOs sent to addresses within the supplied wallet can be reclaimed.
/// </summary>
[Required]
[JsonProperty(PropertyName = "hex")]
public string Hex { get; set; }

/// <summary>
/// Indicate whether the built transactions should be sent to the network immediately after being built.
/// </summary>
[JsonProperty(PropertyName = "broadcast")]
public bool Broadcast { get; set; }
}
}
1 change: 0 additions & 1 deletion src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using NBitcoin.BuilderExtensions;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Features.Wallet.Models;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.Extensions;
using TracerAttributes;
Expand Down
52 changes: 52 additions & 0 deletions src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Stratis.Bitcoin.Features.Api;
using Stratis.Bitcoin.Features.BlockStore;
using Stratis.Bitcoin.Features.ColdStaking;
using Stratis.Bitcoin.Features.ColdStaking.Controllers;
using Stratis.Bitcoin.Features.ColdStaking.Models;
using Stratis.Bitcoin.Features.Consensus;
using Stratis.Bitcoin.Features.MemoryPool;
using Stratis.Bitcoin.Features.Miner;
Expand Down Expand Up @@ -199,5 +201,55 @@ public async Task WalletCanMineWithColdWalletCoinsAsync()
}, cancellationToken: cancellationToken);
}
}

[Fact]
[Trait("Unstable", "True")]
public async Task CanRetrieveFilteredUtxosAsync()
{
using (var builder = NodeBuilder.Create(this))
{
var network = new StraxRegTest();

CoreNode stratisSender = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: false);
CoreNode stratisColdStake = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: true);

stratisSender.WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start();
stratisColdStake.WithWallet().Start();

var coldWalletManager = stratisColdStake.FullNode.WalletManager() as ColdStakingManager;

// Set up cold staking account on cold wallet.
coldWalletManager.GetOrCreateColdStakingAccount(WalletName, true, Password, null);
HdAddress coldWalletAddress = coldWalletManager.GetFirstUnusedColdStakingAddress(WalletName, true);

var walletAccountReference = new WalletAccountReference(WalletName, Account);
long total2 = stratisSender.FullNode.WalletManager().GetSpendableTransactionsInAccount(walletAccountReference, 1).Sum(s => s.Transaction.Amount);

// Sync nodes.
TestHelper.Connect(stratisSender, stratisColdStake);

// Send coins to cold address.
Money amountToSend = total2 - network.Consensus.ProofOfWorkReward;
Transaction transaction1 = stratisSender.FullNode.WalletTransactionHandler().BuildTransaction(CreateContext(stratisSender.FullNode.Network, new WalletAccountReference(WalletName, Account), Password, coldWalletAddress.ScriptPubKey, amountToSend, FeeType.Medium, 1));

// Broadcast to the other nodes.
await stratisSender.FullNode.NodeController<WalletController>().SendTransaction(new SendTransactionRequest(transaction1.ToHex()));

// Wait for the transaction to arrive.
TestBase.WaitLoop(() => stratisColdStake.CreateRPCClient().GetRawMempool().Length > 0);

// Despite the funds being sent to an address in the cold account, the wallet does not recognise the output as funds belonging to it.
Assert.True(stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) == 0);

uint256[] mempoolTransactionId = stratisColdStake.CreateRPCClient().GetRawMempool();

Transaction misspentTransaction = stratisColdStake.CreateRPCClient().GetRawTransaction(mempoolTransactionId[0]);

// Now retrieve the funds sent to the cold address. They will reappear in a normal account on the cold staking node.
stratisColdStake.FullNode.NodeController<ColdStakingController>().RetrieveFilteredUtxos(new RetrieveFilteredUtxosRequest() { WalletName = stratisColdStake.WalletName, WalletPassword = stratisColdStake.WalletPassword, Hex = misspentTransaction.ToHex(), WalletAccount = null, Broadcast = true});

TestBase.WaitLoop(() => stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) > 0);
}
}
}
}