Skip to content

Commit

Permalink
Implement importpubkey and watchonly flag for gettransaction (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
zeptin authored and fassadlr committed Nov 30, 2020
1 parent bbffc2a commit 50e3c29
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ public class ColdStakingWalletRPCController : WalletRPCController
StoreSettings storeSettings,
IWalletManager walletManager,
WalletSettings walletSettings,
IWalletTransactionHandler walletTransactionHandler) :
base(blockStore, broadcasterManager, chainIndexer, consensusManager, fullNode, loggerFactory, network, scriptAddressReader, storeSettings, walletManager, walletSettings, walletTransactionHandler)
IWalletTransactionHandler walletTransactionHandler,
IWalletSyncManager walletSyncManager) :
base(blockStore, broadcasterManager, chainIndexer, consensusManager, fullNode, loggerFactory, network, scriptAddressReader, storeSettings, walletManager, walletSettings, walletTransactionHandler, walletSyncManager)
{
}
}
}
}
5 changes: 5 additions & 0 deletions src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ public Money GetReceivedByAddress(BitcoinAddress address, int confirmations)
return Money.Coins(response.Result.Value<decimal>());
}

public void ImportPubKey(string pubkey, bool rescan = true)
{
SendCommand(RPCOperations.importpubkey, pubkey, "", rescan);
}

// importprivkey

public void ImportPrivKey(BitcoinSecret secret)
Expand Down
1 change: 1 addition & 0 deletions src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum RPCOperations
dumpprivkey,
importprivkey,
importaddress,
importpubkey,
dumpwallet,
importwallet,

Expand Down
2 changes: 1 addition & 1 deletion src/Stratis.Bitcoin.Features.Wallet/HdAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public HdAddress AsPaginated(long? prevOutputTxTime, int? prevOutputIndex, int l
public Script Pubkey { get; set; }

/// <summary>
/// The base32 representation of a segwit (P2WPH) address.
/// The bech32 representation of a segwit (P2WPKH) address.
/// </summary>
[JsonProperty(PropertyName = "bech32Address")]
public string Bech32Address { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ public interface IWalletManager
/// <returns>The list of all accounts.</returns>
IEnumerable<HdAccount> GetAllAccounts();

HdAccount GetAccount(string walletName, string accountName);

HdAccount GetAccount(WalletAccountReference accountReference);

HdAccount GetOrCreateWatchOnlyAccount(string walletName);

void AddWatchOnlyAddress(string walletName, string accountName, Script p2pkScriptPubKey, Script p2pkhScriptPubKey);

/// <summary>
/// Gets the last block height.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1390,4 +1390,4 @@ public async Task<WalletBuildTransactionModel> OfflineSignRequest(OfflineSignReq
i.ConfirmedInBlock == transaction.BlockHeight);
}
}
}
}
11 changes: 11 additions & 0 deletions src/Stratis.Bitcoin.Features.Wallet/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,20 @@ public class Wallet
/// <summary>Account numbers greater or equal to this number are reserved for special purpose account indexes.</summary>
public const int SpecialPurposeAccountIndexesStart = 100_000_000;

/// <summary>
/// The wallet account used for storing watched addresses that the wallet does not possess a private key for.
/// This is intended to supersede the WatchOnlyWallet feature going forwards.
/// <remarks>Note that indices 0 and 1 are used for cold staking.</remarks>
/// </summary>
public const int WatchOnlyAccountIndex = SpecialPurposeAccountIndexesStart + 2;

public const string WatchOnlyAccountName = "watchOnly";

/// <summary>Filter for identifying normal wallet accounts.</summary>
public static Func<HdAccount, bool> NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart;

public static Func<HdAccount, bool> WatchOnlyAccount = a => a.Index == WatchOnlyAccountIndex;

/// <summary>Filter for all wallet accounts.</summary>
public static Func<HdAccount, bool> AllAccounts = a => true;

Expand Down
59 changes: 59 additions & 0 deletions src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -994,9 +994,68 @@ public IEnumerable<HdAccount> GetAccounts(string walletName)
{
res = wallet.GetAccounts().ToArray();
}

return res;
}

public HdAccount GetAccount(string walletName, string accountName)
{
Guard.NotEmpty(walletName, nameof(walletName));

Wallet wallet = this.GetWallet(walletName);

HdAccount res = null;
lock (this.lockObject)
{
res = wallet.GetAccounts().FirstOrDefault(a => a.Name == accountName);
}

return res;
}

public HdAccount GetAccount(WalletAccountReference accountReference)
{
return GetAccount(accountReference.WalletName, accountReference.AccountName);
}

public HdAccount GetOrCreateWatchOnlyAccount(string walletName)
{
Guard.NotEmpty(walletName, nameof(walletName));

Wallet wallet = this.GetWallet(walletName);

HdAccount[] res = null;
lock (this.lockObject)
{
res = wallet.GetAccounts(Wallet.WatchOnlyAccount).ToArray();
}

HdAccount watchOnlyAccount = res.FirstOrDefault(a => a.Index == Wallet.WatchOnlyAccountIndex);

if (watchOnlyAccount == null)
{
watchOnlyAccount = this.WalletRepository.CreateAccount(walletName, Wallet.WatchOnlyAccountIndex, Wallet.WatchOnlyAccountName, null);
}

return watchOnlyAccount;
}

// TODO: Perhaps this shouldn't be in the WalletManager itself, although it doesn't fit well with HdAccount either
public void AddWatchOnlyAddress(string walletName, string accountName, Script p2pkScriptPubKey, Script p2pkhScriptPubKey)
{
string address = p2pkhScriptPubKey.GetDestinationAddress(this.network).ToString();

// TODO: Is it sufficient to only define these fields here, or do we need all the other available fields?
var hdAddress = new HdAddress()
{
ScriptPubKey = p2pkhScriptPubKey,
Pubkey = p2pkScriptPubKey,
Address = address
};

this.WalletRepository.AddWatchOnlyAddresses(walletName, accountName, 0, new List<HdAddress>() { hdAddress });
}

public IEnumerable<HdAccount> GetAllAccounts()
{
HdAccount[] res = null;
Expand Down
84 changes: 82 additions & 2 deletions src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class WalletRPCController : FeatureController

private readonly IWalletTransactionHandler walletTransactionHandler;

private readonly IWalletSyncManager walletSyncManager;

private readonly IReserveUtxoService reserveUtxoService;

private readonly WalletSettings walletSettings;
Expand All @@ -64,6 +66,7 @@ public class WalletRPCController : FeatureController
IWalletManager walletManager,
WalletSettings walletSettings,
IWalletTransactionHandler walletTransactionHandler,
IWalletSyncManager walletSyncManager,
IReserveUtxoService reserveUtxoService = null) : base(fullNode: fullNode, consensusManager: consensusManager, chainIndexer: chainIndexer, network: network)
{
this.blockStore = blockStore;
Expand All @@ -74,6 +77,7 @@ public class WalletRPCController : FeatureController
this.walletManager = walletManager;
this.walletSettings = walletSettings;
this.walletTransactionHandler = walletTransactionHandler;
this.walletSyncManager = walletSyncManager;
this.reserveUtxoService = reserveUtxoService;
}

Expand Down Expand Up @@ -453,11 +457,12 @@ public decimal GetBalance(string accountName, int minConfirmations = 0)
/// <returns>Transaction information.</returns>
[ActionName("gettransaction")]
[ActionDescription("Get detailed information about an in-wallet transaction.")]
public GetTransactionModel GetTransaction(string txid)
public GetTransactionModel GetTransaction(string txid, bool include_watchonly = false)
{
if (!uint256.TryParse(txid, out uint256 trxid))
throw new ArgumentException(nameof(txid));

// First check the regular wallet accounts.
WalletAccountReference accountReference = this.GetWalletAccountReference();

Wallet hdWallet = this.walletManager.WalletRepository.GetWallet(accountReference.WalletName);
Expand All @@ -477,6 +482,24 @@ bool IsChangeAddress(Script scriptPubKey)

TransactionData firstReceivedTransaction = receivedTransactions.FirstOrDefault();
TransactionData firstSendTransaction = sentTransactions.FirstOrDefault();

if (firstReceivedTransaction == null && firstSendTransaction == null && include_watchonly)
{
accountReference = this.GetWatchOnlyWalletAccountReference();

hdAccount = this.walletManager.GetOrCreateWatchOnlyAccount(accountReference.WalletName);

addressLookup = this.walletManager.WalletRepository.GetWalletAddressLookup(accountReference.WalletName);

// Get the transaction from the wallet by looking into received and send transactions.
receivedTransactions = this.walletManager.WalletRepository.GetTransactionOutputs(hdAccount, null, trxid, true)
.Where(td => !IsChangeAddress(td.ScriptPubKey)).ToList();
sentTransactions = this.walletManager.WalletRepository.GetTransactionInputs(hdAccount, null, trxid, true).ToList();

firstReceivedTransaction = receivedTransactions.FirstOrDefault();
firstSendTransaction = sentTransactions.FirstOrDefault();
}

if (firstReceivedTransaction == null && firstSendTransaction == null)
throw new RPCServerException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id.");

Expand Down Expand Up @@ -619,6 +642,27 @@ bool IsChangeAddress(Script scriptPubKey)
return model;
}

[ActionName("importpubkey")]
public bool ImportPubkey(string pubkey, string label = "", bool rescan = true)
{
WalletAccountReference walletAccountReference = this.GetWatchOnlyWalletAccountReference();

// As we are not sure whether the P2PK or P2PKH was desired, we have to add both to the watch only account simultaneously.
// We would not be able to infer the P2PK from the P2PKH later anyhow.
Script p2pkScriptPubKey = new PubKey(pubkey).ScriptPubKey;
Script p2pkhScriptPubKey = new PubKey(pubkey).Hash.ScriptPubKey;

this.walletManager.AddWatchOnlyAddress(walletAccountReference.WalletName, walletAccountReference.AccountName, p2pkScriptPubKey, p2pkhScriptPubKey);

// As we cannot be sure when an imported pubkey was transacted against, we have to rescan from genesis if requested.
if (rescan)
{
this.walletSyncManager.SyncFromHeight(0, walletAccountReference.WalletName);
}

return true;
}

[ActionName("listaddressgroupings")]
[ActionDescription("Returns a list of grouped addresses which have had their common ownership made public by common use as inputs or as the resulting change in past transactions.")]
public List<object> ListAddressGroupings()
Expand Down Expand Up @@ -874,7 +918,7 @@ private int GetConfirmationCount(TransactionData transaction)
}

/// <summary>
/// Gets the first account from the "default" wallet if it specified,
/// Gets the first account from the "default" wallet if it is specified,
/// otherwise returns the first available account in the existing wallets.
/// </summary>
/// <returns>Reference to the default wallet account, or the first available if no default wallet is specified.</returns>
Expand Down Expand Up @@ -908,5 +952,41 @@ private WalletAccountReference GetWalletAccountReference()

return new WalletAccountReference(walletName, account.Name);
}

/// <summary>
/// Gets the first watch only account from the "default" wallet if it is specified,
/// otherwise returns the first available watch only account in the existing wallets.
/// </summary>
/// <returns>Reference to the default wallet watch only account, or the first available if no default wallet is specified.</returns>
private WalletAccountReference GetWatchOnlyWalletAccountReference()
{
string walletName = null;

if (string.IsNullOrWhiteSpace(WalletRPCController.CurrentWalletName))
{
if (this.walletSettings.IsDefaultWalletEnabled())
walletName = this.walletManager.GetWalletsNames().FirstOrDefault(w => w == this.walletSettings.DefaultWalletName);
else
{
// TODO: Support multi wallet like core by mapping passed RPC credentials to a wallet/account
walletName = this.walletManager.GetWalletsNames().FirstOrDefault();
}
}
else
{
// Read the wallet name from the class instance.
walletName = WalletRPCController.CurrentWalletName;
}

if (walletName == null)
throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found");

HdAccount account = this.walletManager.GetOrCreateWatchOnlyAccount(walletName);

if (account == null)
throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "Unable to retrieve watch only account");

return new WalletAccountReference(walletName, account.Name);
}
}
}
44 changes: 44 additions & 0 deletions src/Stratis.Bitcoin.IntegrationTests/RPC/RPCTestsMutable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stratis.Bitcoin.Base;
using Stratis.Bitcoin.Features.RPC;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.IntegrationTests.Common;
using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers;
using Stratis.Bitcoin.IntegrationTests.Common.ReadyData;
Expand Down Expand Up @@ -61,6 +63,48 @@ public void TestRpcGetTransactionIsSuccessful()
}
}

[Fact]
public void TestRpcImportPubkeyIsSuccessful()
{
using (NodeBuilder builder = NodeBuilder.Create(this))
{
CoreNode node = builder.CreateStratisPowNode(new BitcoinRegTest()).AlwaysFlushBlocks().WithWallet().Start();
CoreNode node2 = builder.CreateStratisPowNode(new BitcoinRegTest()).WithReadyBlockchainData(ReadyBlockchain.BitcoinRegTest10Miner).Start();

TestHelper.ConnectAndSync(node, node2);

UnspentOutputReference tx = node2.FullNode.WalletManager().GetUnspentTransactionsInWallet("mywallet", 0, Features.Wallet.Wallet.NormalAccounts).First();

RPCClient rpc = node.CreateRPCClient();

PubKey pubKey = PayToPubkeyTemplate.Instance.ExtractScriptPubKeyParameters(tx.Address.Pubkey);

Assert.Throws<RPCException>(() => rpc.SendCommand(RPCOperations.gettransaction, tx.Transaction.Id.ToString(), true));

rpc.ImportPubKey(pubKey.ToHex());

TestBase.WaitLoop(() => node.FullNode.WalletManager().WalletTipHeight == node2.FullNode.WalletManager().WalletTipHeight);

TestBase.WaitLoop(() =>
{
try
{
// Check if gettransaction can now find the transaction in the watch only account.
RPCResponse walletTx = rpc.SendCommand(RPCOperations.gettransaction, tx.Transaction.Id.ToString(), true);
return walletTx != null;
}
catch (RPCException e)
{
return false;
}
});

// Check that when include_watchonly is not set, the watched transaction cannot be located in the normal wallet accounts.
Assert.Throws<RPCException>(() => rpc.SendCommand(RPCOperations.gettransaction, tx.Transaction.Id.ToString(), false));
}
}

[Fact]
public void TestRpcGetBlockWithValidHashIsSuccessful()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ public bool ProcessTransactions(IEnumerable<Transaction> transactions, HashHeigh
{
// This tests the converse.
// Don't allow special-purpose accounts (e.g. coldstaking) to be used like normal accounts.
if (address.AccountIndex >= Wallet.SpecialPurposeAccountIndexesStart)
// However, for the purposes of recording transactions to watch-only accounts we need to make an allowance.
if (address.AccountIndex >= Wallet.SpecialPurposeAccountIndexesStart && address.AccountIndex != Wallet.WatchOnlyAccountIndex)
continue;
}

Expand Down

0 comments on commit 50e3c29

Please sign in to comment.