public IActionResult OfflineColdStakingWithdrawal([FromBody] OfflineColdStakingWithdrawalRequest 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 { Money amount = Money.Parse(request.Amount); Money feeAmount = Money.Parse(request.Fees); BuildOfflineSignResponse response = this.ColdStakingManager.BuildOfflineColdStakingWithdrawalRequest(this.walletTransactionHandler, request.ReceivingAddress, request.WalletName, request.AccountName, amount, feeAmount, request.SubtractFeeFromAmount); this.logger.LogTrace("(-):'{0}'", response); return(this.Json(response)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); this.logger.LogTrace("(-)[ERROR]"); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task SignTransactionOffline() { using (NodeBuilder builder = NodeBuilder.Create(this)) { CoreNode miningNode = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); CoreNode onlineNode = builder.CreateStratisPosNode(this.network).Start(); CoreNode offlineNode = builder.CreateStratisPosNode(this.network).WithWallet().Start(); TestHelper.ConnectAndSync(miningNode, onlineNode); // Get the extpubkey from the offline node to restore on the online node. string extPubKey = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("wallet/extpubkey") .SetQueryParams(new { walletName = "mywallet", accountName = "account 0" }) .GetJsonAsync <string>(); // Load the extpubkey onto the online node. await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/recover-via-extpubkey") .PostJsonAsync(new WalletExtPubRecoveryRequest { Name = "mywallet", AccountIndex = 0, ExtPubKey = extPubKey, CreationDate = DateTime.Today }) .ReceiveJson(); TestHelper.SendCoins(miningNode, onlineNode, Money.Coins(5.0m)); TestHelper.MineBlocks(miningNode, 1); // Build the offline signing template from the online node. No password is needed. BuildOfflineSignResponse offlineTemplate = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/build-offline-sign-request") .PostJsonAsync(new BuildTransactionRequest { WalletName = "mywallet", AccountName = "account 0", FeeAmount = "0.01", ShuffleOutputs = true, AllowUnconfirmed = true, Recipients = new List <RecipientModel>() { new RecipientModel { DestinationAddress = new Key().ScriptPubKey.GetDestinationAddress(this.network).ToString(), Amount = "1" } } }) .ReceiveJson <BuildOfflineSignResponse>(); // Now build the actual transaction on the offline node. It is not synced with the others and only has the information // in the signing request and its own wallet to construct the transaction with. WalletBuildTransactionModel builtTransactionModel = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("wallet/offline-sign-request") .PostJsonAsync(new OfflineSignRequest() { WalletName = offlineTemplate.WalletName, WalletAccount = offlineTemplate.WalletAccount, WalletPassword = "******", UnsignedTransaction = offlineTemplate.UnsignedTransaction, Fee = offlineTemplate.Fee, Utxos = offlineTemplate.Utxos, Addresses = offlineTemplate.Addresses }) .ReceiveJson <WalletBuildTransactionModel>(); // Send the signed transaction from the online node (doesn't really matter, could equally be from the mining node). await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/send-transaction") .PostJsonAsync(new SendTransactionRequest { Hex = builtTransactionModel.Hex }) .ReceiveJson <WalletSendTransactionModel>(); // Check that the transaction is valid and therefore relayed, and able to be mined into a block. TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 1); TestHelper.MineBlocks(miningNode, 1); TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 0); } }
public IActionResult SetupOfflineColdStaking([FromBody] SetupOfflineColdStakingRequest 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 { Money amount = Money.Parse(request.Amount); Money feeAmount = Money.Parse(request.Fees); (Transaction transaction, TransactionBuildContext context) = this.ColdStakingManager.GetColdStakingSetupTransaction( this.walletTransactionHandler, request.ColdWalletAddress, request.HotWalletAddress, request.WalletName, request.WalletAccount, null, amount, feeAmount, request.SubtractFeeFromAmount, true, request.SplitCount, request.SegwitChangeAddress); // TODO: We use the same code in the regular wallet for offline signing request construction, perhaps it should be moved to a common method // Need to be able to look up the keypath for the UTXOs that were used. IEnumerable <UnspentOutputReference> spendableTransactions = this.ColdStakingManager.GetSpendableTransactionsInAccount( new WalletAccountReference(request.WalletName, request.WalletAccount)).ToList(); var utxos = new List <UtxoDescriptor>(); var addresses = new List <AddressDescriptor>(); foreach (ICoin coin in context.TransactionBuilder.FindSpentCoins(transaction)) { utxos.Add(new UtxoDescriptor() { Amount = coin.TxOut.Value.ToUnit(MoneyUnit.BTC).ToString(), TransactionId = coin.Outpoint.Hash.ToString(), Index = coin.Outpoint.N.ToString(), ScriptPubKey = coin.TxOut.ScriptPubKey.ToHex() }); UnspentOutputReference outputReference = spendableTransactions.FirstOrDefault(u => u.Transaction.Id == coin.Outpoint.Hash && u.Transaction.Index == coin.Outpoint.N); if (outputReference != null) { bool segwit = outputReference.Transaction.ScriptPubKey.IsScriptType(ScriptType.P2WPKH); addresses.Add(new AddressDescriptor() { Address = segwit ? outputReference.Address.Bech32Address : outputReference.Address.Address, AddressType = segwit ? "p2wpkh" : "p2pkh", KeyPath = outputReference.Address.HdPath }); } } // Return transaction hex, UTXO list, address list. The offline signer will infer from the transaction structure that a cold staking setup is being made. var model = new BuildOfflineSignResponse() { WalletName = request.WalletName, WalletAccount = request.WalletAccount, Fee = context.TransactionFee.ToUnit(MoneyUnit.BTC).ToString(), UnsignedTransaction = transaction.ToHex(), Utxos = utxos, Addresses = addresses }; this.logger.LogTrace("(-):'{0}'", model); return(this.Json(model)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); this.logger.LogTrace("(-)[ERROR]"); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task SignColdStakingSetupOffline() { using (NodeBuilder builder = NodeBuilder.Create(this)) { CoreNode miningNode = builder.CreateStratisColdStakingNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); CoreNode onlineNode = builder.CreateStratisColdStakingNode(this.network).Start(); CoreNode offlineNode = builder.CreateStratisColdStakingNode(this.network).WithWallet().Start(); // The offline node never gets connected to anything. TestHelper.ConnectAndSync(miningNode, onlineNode); // Get the extpubkey from the offline node to restore on the online node. string extPubKey = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("wallet/extpubkey") .SetQueryParams(new { walletName = "mywallet", accountName = "account 0" }) .GetJsonAsync <string>(); // Load the extpubkey onto the online node. await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/recover-via-extpubkey") .PostJsonAsync(new WalletExtPubRecoveryRequest { Name = "coldwallet", AccountIndex = 0, ExtPubKey = extPubKey, CreationDate = DateTime.Today - TimeSpan.FromDays(1) }) .ReceiveJson(); // Get a mnemonic for the hot wallet. string hotWalletMnemonic = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/mnemonic") .GetJsonAsync <string>(); // Restore the hot wallet on the online node. // This is needed because the hot address needs to have a private key available to stake with. await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/recover") .PostJsonAsync(new WalletRecoveryRequest() { Name = "hotwallet", Mnemonic = hotWalletMnemonic, Password = "******", Passphrase = "", CreationDate = DateTime.Today - TimeSpan.FromDays(1) }) .ReceiveJson(); // Get the hot address from the online node. CreateColdStakingAccountResponse hotAccount = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/cold-staking-account") .PostJsonAsync(new CreateColdStakingAccountRequest() { WalletName = "hotwallet", WalletPassword = "******", IsColdWalletAccount = false }) .ReceiveJson <CreateColdStakingAccountResponse>(); string hotAddress = (await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/cold-staking-address") .SetQueryParams(new { walletName = "hotwallet", isColdWalletAddress = "false" }) .GetJsonAsync <GetColdStakingAddressResponse>()).Address; string coldWalletUnusedAddress = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/unusedaddress") .SetQueryParams(new { walletName = "coldwallet", accountName = "account 0" }) .GetJsonAsync <string>(); // Send some funds to the cold wallet's default (non-special) account to use for the staking setup. string fundTransaction = (await $"http://localhost:{miningNode.ApiPort}/api" .AppendPathSegment("wallet/build-transaction") .PostJsonAsync(new BuildTransactionRequest { WalletName = "mywallet", Password = "******", AccountName = "account 0", FeeType = "high", Recipients = new List <RecipientModel>() { new RecipientModel() { Amount = "5", DestinationAddress = coldWalletUnusedAddress } } }) .ReceiveJson <WalletBuildTransactionModel>()).Hex; await $"http://localhost:{miningNode.ApiPort}/api" .AppendPathSegment("wallet/send-transaction") .PostJsonAsync(new SendTransactionRequest() { Hex = fundTransaction }) .ReceiveJson <WalletSendTransactionModel>(); TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length > 0); TestHelper.MineBlocks(miningNode, 1); // Set up cold staking account on offline node to get the needed cold address. CreateColdStakingAccountResponse coldAccount = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/cold-staking-account") .PostJsonAsync(new CreateColdStakingAccountRequest() { WalletName = "mywallet", WalletPassword = "******", IsColdWalletAccount = true }) .ReceiveJson <CreateColdStakingAccountResponse>(); string coldAddress = (await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/cold-staking-address") .SetQueryParams(new { walletName = "mywallet", isColdWalletAddress = "true" }) .GetJsonAsync <GetColdStakingAddressResponse>()).Address; // Build the offline cold staking template from the online node. No password is needed. BuildOfflineSignResponse offlineTemplate = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/setup-offline-cold-staking") .PostJsonAsync(new SetupOfflineColdStakingRequest() { ColdWalletAddress = coldAddress, HotWalletAddress = hotAddress, WalletName = "coldwallet", WalletAccount = "account 0", Amount = "5", // Check that we can send the entire available balance in the setup Fees = "0.01", SubtractFeeFromAmount = true, SegwitChangeAddress = false, SplitCount = 10 }) .ReceiveJson <BuildOfflineSignResponse>(); // Now build the actual transaction on the offline node. It is not synced with the others and only has the information // in the signing request and its own wallet to construct the transaction with. // Note that the wallet name and account name on the offline node may not actually match those from the online node. WalletBuildTransactionModel builtTransactionModel = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("wallet/offline-sign-request") .PostJsonAsync(new OfflineSignRequest() { WalletName = "mywallet", WalletAccount = offlineTemplate.WalletAccount, WalletPassword = "******", UnsignedTransaction = offlineTemplate.UnsignedTransaction, Fee = offlineTemplate.Fee, Utxos = offlineTemplate.Utxos, Addresses = offlineTemplate.Addresses }) .ReceiveJson <WalletBuildTransactionModel>(); Dictionary <string, int> txCountBefore = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/transactionCount") .SetQueryParams(new { walletName = "hotwallet", accountName = hotAccount.AccountName }) .GetJsonAsync <Dictionary <string, int> >(); Assert.True(txCountBefore.Values.First() == 0); // Send the signed transaction from the online node (doesn't really matter, could equally be from the mining node). await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/send-transaction") .PostJsonAsync(new SendTransactionRequest { Hex = builtTransactionModel.Hex }) .ReceiveJson <WalletSendTransactionModel>(); // Check that the transaction is valid and therefore relayed, and able to be mined into a block. TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 1); TestHelper.MineBlocks(miningNode, 1); TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 0); Dictionary <string, int> txCountAfter = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/transactionCount") .SetQueryParams(new { walletName = "hotwallet", accountName = hotAccount.AccountName }) .GetJsonAsync <Dictionary <string, int> >(); Assert.True(txCountAfter.Values.First() > 0); string onlineNodeUnusedAddress = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/unusedaddress") .SetQueryParams(new { walletName = "hotwallet", accountName = "account 0" }) .GetJsonAsync <string>(); // Now attempt a withdrawal. First get the estimated fee. Money offlineWithdrawalFee = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/estimate-offline-cold-staking-withdrawal-tx-fee") .PostJsonAsync(new OfflineColdStakingWithdrawalFeeEstimationRequest() { WalletName = "hotwallet", AccountName = hotAccount.AccountName, ReceivingAddress = onlineNodeUnusedAddress, Amount = "4", // Withdraw part of the available balance in the cold account. SubtractFeeFromAmount = true }) .ReceiveJson <Money>(); // Now generate the actual unsigned template transaction. BuildOfflineSignResponse offlineWithdrawalTemplate = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("coldstaking/offline-cold-staking-withdrawal") .PostJsonAsync(new OfflineColdStakingWithdrawalRequest() { WalletName = "hotwallet", AccountName = hotAccount.AccountName, ReceivingAddress = onlineNodeUnusedAddress, Amount = "4", // Withdraw part of the available balance in the cold account. Fees = offlineWithdrawalFee.ToString(), SubtractFeeFromAmount = true }) .ReceiveJson <BuildOfflineSignResponse>(); WalletBuildTransactionModel builtWithdrawalTransactionModel = await $"http://localhost:{offlineNode.ApiPort}/api" .AppendPathSegment("wallet/offline-sign-request") .PostJsonAsync(new OfflineSignRequest() { WalletName = "mywallet", WalletAccount = "coldStakingColdAddresses", WalletPassword = "******", UnsignedTransaction = offlineWithdrawalTemplate.UnsignedTransaction, Fee = offlineWithdrawalTemplate.Fee, Utxos = offlineWithdrawalTemplate.Utxos, Addresses = offlineWithdrawalTemplate.Addresses }) .ReceiveJson <WalletBuildTransactionModel>(); Dictionary <string, int> txCountBefore2 = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/transactionCount") .SetQueryParams(new { walletName = "hotwallet", accountName = "account 0" }) .GetJsonAsync <Dictionary <string, int> >(); Assert.True(txCountBefore2.Values.First() == 0); // Send the signed transaction from the online node (doesn't really matter, could equally be from the mining node). await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/send-transaction") .PostJsonAsync(new SendTransactionRequest { Hex = builtWithdrawalTransactionModel.Hex }) .ReceiveJson <WalletSendTransactionModel>(); // Check that the transaction is valid and therefore relayed, and able to be mined into a block. TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 1); TestHelper.MineBlocks(miningNode, 1); TestBase.WaitLoop(() => miningNode.CreateRPCClient().GetRawMempool().Length == 0); Dictionary <string, int> txCountAfter2 = await $"http://localhost:{onlineNode.ApiPort}/api" .AppendPathSegment("wallet/transactionCount") .SetQueryParams(new { walletName = "hotwallet", accountName = "account 0" }) .GetJsonAsync <Dictionary <string, int> >(); Assert.True(txCountAfter2.Values.First() > 0); } }