diff --git a/src/Features/Blockcore.Features.ColdStaking/ColdStakingFeature.cs b/src/Features/Blockcore.Features.ColdStaking/ColdStakingFeature.cs index f3f6e28c4..279fb9a23 100644 --- a/src/Features/Blockcore.Features.ColdStaking/ColdStakingFeature.cs +++ b/src/Features/Blockcore.Features.ColdStaking/ColdStakingFeature.cs @@ -164,7 +164,7 @@ private void AddComponentStats(StringBuilder benchLog) { // Get all the accounts, including the ones used for cold staking. // TODO: change GetAccounts to accept a filter. - foreach (HdAccount account in this.coldStakingManager.GetAccounts(walletName)) + foreach (IHdAccount account in this.coldStakingManager.GetAccounts(walletName)) { AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, account.Name).Single(); benchLog.AppendLine(($"{walletName}/{account.Name}" + ",").PadRight(LoggingConfiguration.ColumnLength + 20) @@ -172,7 +172,7 @@ private void AddComponentStats(StringBuilder benchLog) + " Unconfirmed balance: " + accountBalance.AmountUnconfirmed.ToString()); } - HdAccount coldStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), true); + IHdAccount coldStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), true); if (coldStakingAccount != null) { AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, coldStakingAccount.Name).Single(); @@ -181,7 +181,7 @@ private void AddComponentStats(StringBuilder benchLog) + " Unconfirmed balance: " + accountBalance.AmountUnconfirmed.ToString()); } - HdAccount hotStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), false); + IHdAccount hotStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), false); if (hotStakingAccount != null) { AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, hotStakingAccount.Name).Single(); diff --git a/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs b/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs index 37949af40..0d6c4aadb 100644 --- a/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs +++ b/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs @@ -183,10 +183,10 @@ public GetColdStakingInfoResponse GetColdStakingInfo(string walletName) /// The wallet where we wish to create the account. /// Indicates whether we need the cold wallet account (versus the hot wallet account). /// The cold staking account or null if the account does not exist. - public HdAccount GetColdStakingAccount(Wallet.Types.Wallet wallet, bool isColdWalletAccount) + public IHdAccount GetColdStakingAccount(Wallet.Types.Wallet wallet, bool isColdWalletAccount) { var coinType = wallet.Network.Consensus.CoinType; - HdAccount account = wallet.GetAccount(isColdWalletAccount ? ColdWalletAccountName : HotWalletAccountName); + IHdAccount account = wallet.GetAccount(isColdWalletAccount ? ColdWalletAccountName : HotWalletAccountName); if (account == null) { this.logger.LogTrace("(-)[ACCOUNT_DOES_NOT_EXIST]:null"); @@ -213,11 +213,11 @@ public HdAccount GetColdStakingAccount(Wallet.Types.Wallet wallet, bool isColdWa /// Indicates whether we need the cold wallet account (versus the hot wallet account). /// The wallet password which will be used to create the account. /// The new or existing cold staking account. - public HdAccount GetOrCreateColdStakingAccount(string walletName, bool isColdWalletAccount, string walletPassword) + public IHdAccount GetOrCreateColdStakingAccount(string walletName, bool isColdWalletAccount, string walletPassword) { Wallet.Types.Wallet wallet = this.GetWalletByName(walletName); - HdAccount account = this.GetColdStakingAccount(wallet, isColdWalletAccount); + IHdAccount account = this.GetColdStakingAccount(wallet, isColdWalletAccount); if (account != null) { this.logger.LogTrace("(-)[ACCOUNT_ALREADY_EXIST]:'{0}'", account.Name); @@ -279,7 +279,7 @@ internal HdAddress GetFirstUnusedColdStakingAddress(string walletName, bool isCo Guard.NotNull(walletName, nameof(walletName)); Wallet.Types.Wallet wallet = this.GetWalletByName(walletName); - HdAccount account = this.GetColdStakingAccount(wallet, isColdWalletAddress); + IHdAccount account = this.GetColdStakingAccount(wallet, isColdWalletAddress); if (account == null) { this.logger.LogTrace("(-)[ACCOUNT_DOES_NOT_EXIST]:null"); @@ -340,8 +340,8 @@ internal Transaction GetColdStakingSetupTransaction(IWalletTransactionHandler wa Wallet.Types.Wallet wallet = this.GetWalletByName(walletName); // Get/create the cold staking accounts. - HdAccount coldAccount = this.GetOrCreateColdStakingAccount(walletName, true, walletPassword); - HdAccount hotAccount = this.GetOrCreateColdStakingAccount(walletName, false, walletPassword); + IHdAccount coldAccount = this.GetOrCreateColdStakingAccount(walletName, true, walletPassword); + IHdAccount hotAccount = this.GetOrCreateColdStakingAccount(walletName, false, walletPassword); HdAddress coldAddress = coldAccount?.ExternalAddresses.FirstOrDefault(s => s.Address == coldWalletAddress || s.Bech32Address == coldWalletAddress); HdAddress hotAddress = hotAccount?.ExternalAddresses.FirstOrDefault(s => s.Address == hotWalletAddress || s.Bech32Address == hotWalletAddress); @@ -474,7 +474,7 @@ internal Transaction GetColdStakingWithdrawalTransaction(IWalletTransactionHandl Wallet.Types.Wallet wallet = this.GetWalletByName(walletName); // Get the cold staking account. - HdAccount coldAccount = this.GetColdStakingAccount(wallet, true); + IHdAccount coldAccount = this.GetColdStakingAccount(wallet, true); if (coldAccount == null) { this.logger.LogTrace("(-)[COLDSTAKE_ACCOUNT_DOES_NOT_EXIST]"); @@ -488,7 +488,7 @@ internal Transaction GetColdStakingWithdrawalTransaction(IWalletTransactionHandl throw new WalletException("You can't send the money to a cold staking cold wallet account."); } - HdAccount hotAccount = this.GetColdStakingAccount(wallet, false); + IHdAccount hotAccount = this.GetColdStakingAccount(wallet, false); if (hotAccount != null && hotAccount.ExternalAddresses.Concat(hotAccount.InternalAddresses).Any(s => s.Address == receivingAddress || s.Bech32Address == receivingAddress)) { this.logger.LogTrace("(-)[COLDSTAKE_INVALID_HOT_WALLET_ADDRESS_USAGE]"); @@ -599,7 +599,7 @@ public IEnumerable GetSpendableTransactionsInColdWallet( /// /// The script (possibly a cold staking script) to check. /// The account filter. - public override void TransactionFoundInternal(Wallet.Types.Wallet wallet, Script script, Func accountFilter = null) + public override void TransactionFoundInternal(Wallet.Types.Wallet wallet, Script script, Func accountFilter = null) { if (ColdStakingScriptTemplate.Instance.ExtractScriptPubKeyParameters(script, out KeyId hotPubKeyHash, out KeyId coldPubKeyHash)) { diff --git a/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningController.cs b/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningController.cs index 4679f4894..f7212611b 100644 --- a/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningController.cs +++ b/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningController.cs @@ -145,7 +145,7 @@ internal WalletAccountReference GetAccount() throw new Exception(noWalletMessage); } - HdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); + IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); if (account == null) { this.logger.LogError(ExceptionOccurredMessage, noAccountMessage); diff --git a/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningRPCController.cs b/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningRPCController.cs index 5efaf60b0..45a64f514 100644 --- a/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningRPCController.cs +++ b/src/Features/Blockcore.Features.Miner/Api/Controllers/MiningRPCController.cs @@ -106,7 +106,7 @@ private WalletAccountReference GetAccount() if (walletName == null) throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found"); - HdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); + IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); if (account == null) throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No account found on wallet"); diff --git a/src/Features/Blockcore.Features.PoA/PoAMiner.cs b/src/Features/Blockcore.Features.PoA/PoAMiner.cs index d94359317..a8c7662ea 100644 --- a/src/Features/Blockcore.Features.PoA/PoAMiner.cs +++ b/src/Features/Blockcore.Features.PoA/PoAMiner.cs @@ -310,7 +310,7 @@ private Script GetScriptPubKeyFromWallet() if (walletName == null) return null; - HdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); + IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); if (account == null) return null; diff --git a/src/Features/Blockcore.Features.RPC/RPCMiddleware.cs b/src/Features/Blockcore.Features.RPC/RPCMiddleware.cs index 691432cd2..49157a045 100644 --- a/src/Features/Blockcore.Features.RPC/RPCMiddleware.cs +++ b/src/Features/Blockcore.Features.RPC/RPCMiddleware.cs @@ -84,7 +84,7 @@ public async Task InvokeAsync(HttpContext httpContext) throw new NotImplementedException("The request is not supported."); } - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; // Write the response body. using (StreamWriter streamWriter = new StreamWriter(httpContext.Response.Body, Encoding.Default, 1024, true)) @@ -134,44 +134,44 @@ private async Task HandleRpcInvokeExceptionAsync(HttpContext httpContext, Except if (ex is ArgumentException || ex is FormatException) { JObject response = CreateError(RPCErrorCode.RPC_MISC_ERROR, "Argument error: " + ex.Message); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (ex is BlockNotFoundException) { JObject response = CreateError(RPCErrorCode.RPC_INVALID_REQUEST, "Argument error: " + ex.Message); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (ex is ConfigurationException) { JObject response = CreateError(RPCErrorCode.RPC_INTERNAL_ERROR, ex.Message); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (ex is RPCServerException) { var rpcEx = (RPCServerException)ex; JObject response = CreateError(rpcEx.ErrorCode, ex.Message); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (httpContext.Response?.StatusCode == 404) { JObject response = CreateError(RPCErrorCode.RPC_METHOD_NOT_FOUND, "Method not found"); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (this.IsDependencyFailure(ex)) { JObject response = CreateError(RPCErrorCode.RPC_METHOD_NOT_FOUND, ex.Message); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } else if (httpContext.Response?.StatusCode == 500 || ex != null) { JObject response = CreateError(RPCErrorCode.RPC_INTERNAL_ERROR, "Internal error"); - httpContext.Response.ContentType = ContentType; + httpContext.Response.ContentType = this.ContentType; this.logger.LogError(new EventId(0), ex, "Internal error while calling RPC Method"); await httpContext.Response.WriteAsync(response.ToString(Formatting.Indented)); } diff --git a/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletController.cs b/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletController.cs index 07cfb8a02..54fd5c9cd 100644 --- a/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletController.cs +++ b/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletController.cs @@ -719,7 +719,7 @@ public IActionResult BuildTransaction([FromBody] BuildTransactionRequest request if (!string.IsNullOrWhiteSpace(request.ChangeAddress)) { Types.Wallet wallet = this.walletManager.GetWallet(request.WalletName); - HdAccount account = wallet.GetAccount(request.AccountName); + IHdAccount account = wallet.GetAccount(request.AccountName); if (account == null) { return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Account not found.", $"No account with the name '{request.AccountName}' could be found in wallet {wallet.Name}."); @@ -952,7 +952,7 @@ public IActionResult CreateNewAccount([FromBody] GetUnusedAccountModel request) try { - HdAccount result = this.walletManager.GetUnusedAccount(request.WalletName, request.Password); + IHdAccount result = this.walletManager.GetUnusedAccount(request.WalletName, request.Password); return this.Json(result.Name); } catch (CannotAddAccountToXpubKeyWalletException e) @@ -986,7 +986,7 @@ public IActionResult ListAccounts([FromQuery] ListAccountsModel request) try { - IEnumerable result = this.walletManager.GetAccounts(request.WalletName); + IEnumerable result = this.walletManager.GetAccounts(request.WalletName); return this.Json(result.Select(a => a.Name)); } catch (Exception e) @@ -1081,7 +1081,7 @@ public IActionResult GetAllAddresses([FromQuery] GetAllAddressesModel request) try { Types.Wallet wallet = this.walletManager.GetWallet(request.WalletName); - HdAccount account = wallet.GetAccount(request.AccountName); + IHdAccount account = wallet.GetAccount(request.AccountName); if (account == null) throw new WalletException($"No account with the name '{request.AccountName}' could be found."); @@ -1436,7 +1436,7 @@ public IActionResult DistributeUtxos([FromBody] DistributeUtxosRequest request) var walletReference = new WalletAccountReference(request.WalletName, request.AccountName); Types.Wallet wallet = this.walletManager.GetWallet(request.WalletName); - HdAccount account = wallet.GetAccount(request.AccountName); + IHdAccount account = wallet.GetAccount(request.AccountName); var addresses = new List(); diff --git a/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletRPCController.cs b/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletRPCController.cs index 1c6020365..3b6e2c3b4 100644 --- a/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletRPCController.cs +++ b/src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletRPCController.cs @@ -15,6 +15,7 @@ using Blockcore.Features.Wallet.Api.Models; using Blockcore.Features.Wallet.Database; using Blockcore.Features.Wallet.Exceptions; +using Blockcore.Features.Wallet.Helpers; using Blockcore.Features.Wallet.Interfaces; using Blockcore.Features.Wallet.Types; using Blockcore.Interfaces; @@ -24,6 +25,8 @@ using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Script = Blockcore.Consensus.ScriptInfo.Script; namespace Blockcore.Features.Wallet.Api.Controllers { @@ -151,6 +154,239 @@ public async Task SendToAddressAsync(BitcoinAddress address, decimal am } } + /// + /// Create a transaction spending the given inputs and creating new outputs. Outputs can be addresses or data. Returns hex - encoded raw transaction. Note that the transaction's inputs are not signed, and it is not stored in the wallet or transmitted to the network. + /// + /// A json array of json objects. + /// A json object with outputs. + /// (string) Hex string of the transaction + [ActionName("createrawtransaction")] + [ActionDescription("Create a transaction spending the given inputs and creating new outputs. Outputs can be addresses or data. Returns hex - encoded raw transaction. Note that the transaction's inputs are not signed, and it is not stored in the wallet or transmitted to the network.")] + public IActionResult CreateRawTransaction(string inputs, string outputs) + { + try + { + if (string.IsNullOrEmpty(inputs)) + { + throw new ArgumentNullException("inputs"); + } + if (string.IsNullOrEmpty(outputs)) + { + throw new ArgumentNullException("outputs"); + } + Transaction transaction = new Transaction(); + + dynamic txIns = JsonConvert.DeserializeObject(inputs); + foreach (var input in txIns) + { + var txIn = new TxIn(new OutPoint(uint256.Parse((string)input.txid), (uint)input.vout)); + if (input.sequence != null) + { + txIn.Sequence = (uint)input.sequence; + } + transaction.AddInput(txIn); + } + + Dictionary parsedOutputs = JsonConvert.DeserializeObject>(outputs); + foreach (KeyValuePair entry in parsedOutputs) + { + var isValid = false; + try + { + // P2PKH + if (BitcoinPubKeyAddress.IsValid(entry.Key, this.Network)) + { + isValid = true; + } + else if (BitcoinScriptAddress.IsValid(entry.Key, this.Network)) + { + isValid = true; + } + } + catch (Exception) + { + isValid = false; + } + + if (!isValid) throw new Exception(string.Format("Output address {0} is invalid.", entry.Key)); + + var destination = BitcoinAddress.Create(entry.Key, this.Network).ScriptPubKey; + transaction.AddOutput(new TxOut(new Money(entry.Value, MoneyUnit.BTC), destination)); + } + + var response = new TransactionHexModel() + { + TransactionHex = transaction.ToHex() + }; + + return this.Json(response); + + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, e.Message); + } + } + + /// + /// Creates mutisig wallet with 1 private key and list of xPub keys + /// + /// Wallet name + /// Number of signatures required for transaction to be valid. + /// Extended pubkeys for other cosigners. + /// Coin type as per https://github.com/satoshilabs/slips/blob/master/slip-0044.md + /// Mnemonic wallet recovery seed. + /// A wallet encryption password. + /// Passphrase as seed extension word. + /// + [ActionName("createmutisigwallet")] + [ActionDescription("Creates a multisig wallet.")] + public IActionResult CreateMutisigWallet(string walletName, int threashold, string cosignerXPubs, string mnemonicSeed, string password, string passphrase) + { + List cosigners = new List(); + + foreach (var item in JsonConvert.DeserializeObject(cosignerXPubs) as JArray) + { + cosigners.Add(item.ToString()); + } + var wallet = this.walletManager.CreateMutisigWallet(walletName, threashold, cosigners, this.Network.Consensus.CoinType, mnemonicSeed, password, passphrase); + return this.Json(wallet); + } + + [ActionName("combinemultisigsignatures")] + [ActionDescription("Combines multiple signed (same) transctions into 1 properly signed transaction")] + public IActionResult CombineMultisigSignatures(string transactions) + { + try + { + List transactionsParsed = new List(); + List coins = new List(); + + foreach (var item in JsonConvert.DeserializeObject(transactions) as JArray) + { + transactionsParsed.Add(this.FullNode.Network.CreateTransaction(item.ToString())); + } + + WalletAccountReference accountReference = this.GetWalletAccountReference(); + var wallet = this.walletManager.GetWallet(accountReference.WalletName); + if (wallet.IsMultisig) + { + // Add keys for signing inputs. This takes time so only add keys for distinct addresses. + foreach (UnspentOutputReference unspentOutput in this.walletManager.GetSpendableTransactionsInWallet(accountReference.WalletName, 1)) + { + Script prevscript = unspentOutput.Transaction.ScriptPubKey; + + if (prevscript.IsScriptType(ScriptType.P2SH) || prevscript.IsScriptType(ScriptType.P2WSH)) + { + if (unspentOutput.Address.RedeemScript == null) + throw new WalletException("Missing redeem script"); + + // Provide the redeem script to the builder + var scriptCoin = ScriptCoin.Create(this.Network, unspentOutput.ToOutPoint(), new TxOut(unspentOutput.Transaction.Amount, prevscript), unspentOutput.Address.RedeemScript); + coins.Add(scriptCoin); + } + } + } + + Transaction fullySigned = new TransactionBuilder(this.FullNode.Network) + .AddCoins(coins) + .CombineSignatures(transactionsParsed.ToArray()); + + var isOk = new TransactionBuilder(this.FullNode.Network) + .AddCoins(coins) + .Verify(fullySigned); + + var response = new TransactionHexModel() + { + TransactionHex = fullySigned.ToHex() + }; + return this.Json(response); + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, e.Message); + } + } + + + /// + /// Add inputs to a transaction until it has enough in value to meet its out value. This will not accept inputs specified in raw tranaction.It will add at most one change output to the outputs. No existing outputs will be modified unless "subtractFeeFromOutputs" is specified. Note that inputs which were signed may need to be resigned after completion since in/ outputs have been added. The inputs added will not be signed, use signrawtransaction for that. Note that all existing inputs must have their previous output transaction be in the wallet." + /// + /// HD Account Name - Example: "WalletName/WalletAccount" + /// The hex string of the raw transaction. + /// Transaction password. + /// hex of funded transaction + [ActionName("fundandsignmultisigtransaction")] + [ActionDescription("Add inputs to a transaction until it has enough in value to meet its out value.")] + public IActionResult CreateMultisigTransaction(string account, string hex, string password) + { + try + { + List coins = new List(); + List secrets = new List(); + + var decodedTx = this.FullNode.Network.CreateTransaction(hex); + + if (!string.IsNullOrEmpty(account)) + throw new RPCServerException(RPCErrorCode.RPC_METHOD_DEPRECATED, "Use of 'account' parameter has been deprecated"); + + WalletAccountReference accountReference = this.GetWalletAccountReference(); + var wallet = this.walletManager.GetWallet(accountReference.WalletName); + if (wallet.IsMultisig) + { + + var msAccount = (HdAccountMultisig)wallet.GetAccounts().First(); + // get extended private key + Key privateKey = HdOperations.DecryptSeed(wallet.EncryptedSeed, password, this.Network); + + // Add keys for signing inputs. This takes time so only add keys for distinct addresses. + foreach (UnspentOutputReference unspentOutput in this.walletManager.GetSpendableTransactionsInWallet(accountReference.WalletName, 1)) + { + Script prevscript = unspentOutput.Transaction.ScriptPubKey; + + if (prevscript.IsScriptType(ScriptType.P2SH) || prevscript.IsScriptType(ScriptType.P2WSH)) + { + if (unspentOutput.Address.RedeemScript == null) + throw new WalletException("Missing redeem script"); + + // Provide the redeem script to the builder + var scriptCoin = ScriptCoin.Create(this.Network, unspentOutput.ToOutPoint(), new TxOut(unspentOutput.Transaction.Amount, prevscript), unspentOutput.Address.RedeemScript); + coins.Add(scriptCoin); + } + + secrets.Add(HdOperations.GetExtendedPrivateKey(privateKey, wallet.ChainCode, unspentOutput.Address.HdPath, this.Network)); + //this is much slower as wallet seed is decrypted multiple times for each unspent output. + //secrets.Add(wallet.GetExtendedPrivateKeyForAddress(password, unspentOutput.Address)); + } + } + + + //.ContinueToBuild(decodedTx)? + var built = new TransactionBuilder(this.FullNode.Network) + .SetChange(this.walletManager.GetUnusedChangeAddress(accountReference).ScriptPubKey) + .AddCoins(coins) + .AddKeys(secrets.ToArray()) + .SendFees(new Money(10000)) + .Send(decodedTx.Outputs.First().ScriptPubKey, decodedTx.Outputs.First().Value) + .BuildTransaction(true); + + var response = new TransactionHexModel() + { + TransactionHex = built.ToHex() + }; + return this.Json(response); + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, e.Message); + } + } + + + /// /// Broadcasts a raw transaction from hex to local node and network. /// @@ -340,7 +576,7 @@ public GetTransactionModel GetTransaction(string txid) WalletAccountReference accountReference = this.GetWalletAccountReference(); Types.Wallet wallet = this.walletManager.GetWalletByName(accountReference.WalletName); - HdAccount account = this.walletManager.GetAccounts(accountReference.WalletName).Single(a => a.Name == accountReference.AccountName); + IHdAccount account = this.walletManager.GetAccounts(accountReference.WalletName).Single(a => a.Name == accountReference.AccountName); // Get the transaction from the wallet by looking into received and send transactions. List addresses = account.GetCombinedAddresses().ToList(); @@ -901,7 +1137,7 @@ private WalletAccountReference GetWalletAccountReference() throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found"); } - HdAccount account = this.walletManager.GetAccounts(walletName).First(); + IHdAccount account = this.walletManager.GetAccounts(walletName).First(); return new WalletAccountReference(walletName, account.Name); } } diff --git a/src/Features/Blockcore.Features.Wallet/Api/Models/TransactionHexModel.cs b/src/Features/Blockcore.Features.Wallet/Api/Models/TransactionHexModel.cs new file mode 100644 index 000000000..3cf24d43f --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Api/Models/TransactionHexModel.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Blockcore.Features.Wallet.Api.Models +{ + + public class TransactionHexModel + { + /// The transaction bytes as a hexadecimal string. + [JsonProperty(PropertyName = "transactionHex")] + public string TransactionHex { get; set; } + + + public override string ToString() + { + return this.TransactionHex; + } + } +} diff --git a/src/Features/Blockcore.Features.Wallet/Helpers/HdOperations.cs b/src/Features/Blockcore.Features.Wallet/Helpers/HdOperations.cs index 01cc1ff9c..c3212fec1 100644 --- a/src/Features/Blockcore.Features.Wallet/Helpers/HdOperations.cs +++ b/src/Features/Blockcore.Features.Wallet/Helpers/HdOperations.cs @@ -17,17 +17,18 @@ public class HdOperations /// The extended public key used to generate child keys. /// The index of the child key to generate. /// A value indicating whether the public key to generate corresponds to a change address. + /// Network /// /// An HD public key derived from an extended public key. /// - public static PubKey GeneratePublicKey(string accountExtPubKey, int index, bool isChange) + public static PubKey GeneratePublicKey(string accountExtPubKey, int index, bool isChange, Network network = null) { Guard.NotEmpty(accountExtPubKey, nameof(accountExtPubKey)); int change = isChange ? 1 : 0; var keyPath = new KeyPath($"{change}/{index}"); // TODO: Should probably explicitly be passing the network into Parse - ExtPubKey extPubKey = ExtPubKey.Parse(accountExtPubKey).Derive(keyPath); + ExtPubKey extPubKey = ExtPubKey.Parse(accountExtPubKey, network).Derive(keyPath); return extPubKey.PubKey; } diff --git a/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs b/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs new file mode 100644 index 000000000..b50e7a303 --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Blockcore.Features.Wallet.Database; +using Blockcore.Networks; +using NBitcoin; + +namespace Blockcore.Features.Wallet.Interfaces +{ + public interface IAccountRoot + { + ICollection Accounts { get; set; } + int? CoinType { get; set; } + uint256 LastBlockSyncedHash { get; set; } + int? LastBlockSyncedHeight { get; set; } + + IHdAccount AddNewAccount(ExtPubKey accountExtPubKey, int accountIndex, Network network, DateTimeOffset accountCreationTime); + IHdAccount AddNewAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime, int? accountIndex = null, string accountName = null); + IHdAccount CreateAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime, int newAccountIndex, string newAccountName = null); + IHdAccount GetAccountByName(string accountName); + IHdAccount GetFirstUnusedAccount(IWalletStore walletStore); + } +} \ No newline at end of file diff --git a/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs b/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs new file mode 100644 index 000000000..1750c591c --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Blockcore.Features.Wallet.Database; +using Blockcore.Features.Wallet.Types; +using Blockcore.Networks; +using NBitcoin; + +namespace Blockcore.Features.Wallet.Interfaces +{ + public interface IHdAccount + { + DateTimeOffset CreationTime { get; set; } + string ExtendedPubKey { get; set; } + ICollection ExternalAddresses { get; set; } + string HdPath { get; set; } + int Index { get; set; } + ICollection InternalAddresses { get; set; } + string Name { get; set; } + + IEnumerable CreateAddresses(Network network, int addressesQuantity, bool isChange = false); + (Money ConfirmedAmount, Money UnConfirmedAmount) GetBalances(IWalletStore walletStore, bool excludeColdStakeUtxo); + int GetCoinType(); + IEnumerable GetCombinedAddresses(); + HdAddress GetFirstUnusedChangeAddress(IWalletStore walletStore); + HdAddress GetFirstUnusedReceivingAddress(IWalletStore walletStore); + HdAddress GetLastUsedAddress(IWalletStore walletStore, bool isChange); + IEnumerable GetSpendableTransactions(IWalletStore walletStore, int currentChainHeight, long coinbaseMaturity, int confirmations = 0); + bool IsNormalAccount(); + } +} \ No newline at end of file diff --git a/src/Features/Blockcore.Features.Wallet/Interfaces/IWalletManager.cs b/src/Features/Blockcore.Features.Wallet/Interfaces/IWalletManager.cs index 7ae361253..7ed510fd0 100644 --- a/src/Features/Blockcore.Features.Wallet/Interfaces/IWalletManager.cs +++ b/src/Features/Blockcore.Features.Wallet/Interfaces/IWalletManager.cs @@ -54,7 +54,7 @@ public interface IWalletManager /// This is distinct from the list of spendable transactions. A transaction can be unspent but not yet spendable due to coinbase/stake maturity, for example. /// /// A collection of unspent outputs - IEnumerable GetUnspentTransactionsInWallet(string walletName, int confirmations, Func accountFilter); + IEnumerable GetUnspentTransactionsInWallet(string walletName, int confirmations, Func accountFilter); /// /// Helps identify UTXO's that can participate in staking. @@ -174,7 +174,7 @@ public interface IWalletManager /// at index (i - 1) contains transactions. /// /// An unused account. - HdAccount GetUnusedAccount(string walletName, string password); + IHdAccount GetUnusedAccount(string walletName, string password); /// /// Gets an account that contains no transactions. @@ -186,7 +186,7 @@ public interface IWalletManager /// at index (i - 1) contains transactions. /// /// An unused account. - HdAccount GetUnusedAccount(Types.Wallet wallet, string password); + IHdAccount GetUnusedAccount(Types.Wallet wallet, string password); /// /// Gets an address that contains no transaction. @@ -227,7 +227,7 @@ public interface IWalletManager /// The wallet instance. /// The account for which to get history. /// The history for this account. - AccountHistory GetHistory(Types.Wallet wallet, HdAccount account); + AccountHistory GetHistory(Types.Wallet wallet, IHdAccount account); /// /// Gets the history of transactions contained in an account. @@ -248,7 +248,7 @@ public interface IWalletManager /// Items to skip. /// Items to take. /// The history for this account. - AccountHistorySlim GetHistorySlim(Types.Wallet wallet, HdAccount account, int skip = 0, int take = 100); + AccountHistorySlim GetHistorySlim(Types.Wallet wallet, IHdAccount account, int skip = 0, int take = 100); /// /// Gets the balance of transactions contained in an account. @@ -279,7 +279,7 @@ public interface IWalletManager /// /// The name of the wallet to look into. /// - IEnumerable GetAccounts(string walletName); + IEnumerable GetAccounts(string walletName); /// /// Gets the last block height. @@ -429,5 +429,18 @@ public interface IWalletManager /// Broadcast the transaction to the network. /// List of sweep transactions. IEnumerable Sweep(IEnumerable privateKeys, string destAddress, bool broadcast); + + /// + /// Creates mutisig wallet with 1 private key and list of xPub keys + /// + /// Wallet name + /// Number of signatures required for transaction to be valid. + /// Extended pubkeys for other cosigners. + /// Coin type as per https://github.com/satoshilabs/slips/blob/master/slip-0044.md + /// Mnemonic wallet recovery seed. + /// A wallet encryption password. + /// Passphrase as seed extension word. + /// + WalletMultisig CreateMutisigWallet(string walletName, int threashold, List cosignerXPubs, int coinType, string mnemonic, string password, string passphrase); } } \ No newline at end of file diff --git a/src/Features/Blockcore.Features.Wallet/Types/FlatHistory.cs b/src/Features/Blockcore.Features.Wallet/Types/FlatHistory.cs index 24d456c66..19ab57da0 100644 --- a/src/Features/Blockcore.Features.Wallet/Types/FlatHistory.cs +++ b/src/Features/Blockcore.Features.Wallet/Types/FlatHistory.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Blockcore.Features.Wallet.Database; +using Blockcore.Features.Wallet.Interfaces; namespace Blockcore.Features.Wallet.Types { @@ -8,7 +9,7 @@ public class AccountHistory /// /// The account for which the history is retrieved. /// - public HdAccount Account { get; set; } + public IHdAccount Account { get; set; } /// /// The collection of history items. @@ -37,7 +38,7 @@ public class AccountHistorySlim /// /// The account for which the history is retrieved. /// - public HdAccount Account { get; set; } + public IHdAccount Account { get; set; } /// /// The collection of history items. diff --git a/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs b/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs new file mode 100644 index 000000000..beb5dda2d --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Blockcore.Features.Wallet.Types +{ + public class MultisigScheme + { + /// + /// How many signatures will be suffient to move the funds. + /// + [JsonProperty(PropertyName = "threashold")] + public int Threashold { get; set; } + + /// + /// Cosigner extended pubkeys. Intentionally not including any xPriv at this stage as such model is simplest to start with. + /// + [JsonProperty(PropertyName = "xPubs")] + public string[] XPubs { get; set; } + } +} diff --git a/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs b/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs index 3554c7231..0423e95b7 100644 --- a/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs +++ b/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs @@ -7,6 +7,7 @@ using Blockcore.Features.Wallet.Database; using Blockcore.Features.Wallet.Exceptions; using Blockcore.Features.Wallet.Helpers; +using Blockcore.Features.Wallet.Interfaces; using Blockcore.Networks; using Blockcore.Utilities; using Blockcore.Utilities.JsonConverters; @@ -27,17 +28,17 @@ public class Wallet public const int SpecialPurposeAccountIndexesStart = 100_000_000; /// Filter for identifying normal wallet accounts. - public static Func NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart; + public static Func NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart; /// Filter for all wallet accounts. - public static Func AllAccounts = a => true; + public static Func AllAccounts = a => true; /// /// Initializes a new instance of the wallet. /// public Wallet() { - this.AccountsRoot = new List(); + this.AccountsRoot = new List(); } [JsonIgnore] @@ -92,15 +93,19 @@ public Wallet() /// /// The root of the accounts tree. /// + [JsonConverter(typeof(AccountRootConverter))] [JsonProperty(PropertyName = "accountsRoot")] - public ICollection AccountsRoot { get; set; } + public virtual ICollection AccountsRoot { get; set; } + + [JsonProperty(PropertyName = "isMultisig")] + public bool IsMultisig { get; set; } /// /// Gets the accounts in the wallet. /// /// An optional filter for filtering the accounts being returned. /// The accounts in the wallet. - public IEnumerable GetAccounts(Func accountFilter = null) + public IEnumerable GetAccounts(Func accountFilter = null) { return this.AccountsRoot.SelectMany(a => a.Accounts).Where(accountFilter ?? NormalAccounts); } @@ -110,7 +115,7 @@ public IEnumerable GetAccounts(Func accountFilter = /// /// The name of the account to retrieve. /// The requested account or null if the account does not exist. - public HdAccount GetAccount(string accountName) + public IHdAccount GetAccount(string accountName) { return this.AccountsRoot.SingleOrDefault()?.GetAccountByName(accountName); } @@ -121,7 +126,7 @@ public HdAccount GetAccount(string accountName) /// The block whose details are used to update the wallet. public void SetLastBlockDetails(ChainedHeader block) { - AccountRoot accountRoot = this.AccountsRoot.SingleOrDefault(); + IAccountRoot accountRoot = this.AccountsRoot.SingleOrDefault(); if (accountRoot == null) { @@ -136,9 +141,9 @@ public void SetLastBlockDetails(ChainedHeader block) /// Gets all the transactions in the wallet. /// /// A list of all the transactions in the wallet. - public IEnumerable GetAllTransactions(Func accountFilter = null) + public IEnumerable GetAllTransactions(Func accountFilter = null) { - List accounts = this.GetAccounts(accountFilter).ToList(); + List accounts = this.GetAccounts(accountFilter).ToList(); // First we iterate normal accounts foreach (TransactionOutputData txData in accounts.Where(a => a.IsNormalAccount()).SelectMany(x => x.ExternalAddresses).SelectMany(x => this.walletStore.GetForAddress(x.Address))) @@ -181,7 +186,7 @@ public IEnumerable GetAllTransactions(FuncA list of all the public keys contained in the wallet. public IEnumerable