// TODO: These can be more efficient, i.e. remove the wallet calls from GetHistory // And use a different model for Withdrawals. It doesn't quite map to the Withdrawal class. /// <summary> /// Get the history of successful withdrawals. /// </summary> /// <param name="maximumEntriesToReturn">The maximum number of entries to return.</param> /// <returns>A <see cref="WithdrawalModel"/> object containing a history of withdrawals.</returns> public List <WithdrawalModel> GetHistory(int maximumEntriesToReturn) { var result = new List <WithdrawalModel>(); ICrossChainTransfer[] transfers = this.crossChainTransferStore.GetTransfersByStatus(new[] { CrossChainTransferStatus.SeenInBlock }); // TODO: Need to check if this is only used for wallet UI purposes and has no consensus implications foreach (ICrossChainTransfer transfer in transfers.OrderByDescending(t => t.BlockHeight)) // t.PartialTransaction.Time { if (maximumEntriesToReturn-- <= 0) { break; } // Extract the withdrawal details from the recorded "PartialTransaction". IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transfer.PartialTransaction, transfer.BlockHash, (int)transfer.BlockHeight); var model = new WithdrawalModel(); model.withdrawal = withdrawal; model.TransferStatus = transfer?.Status.ToString(); result.Add(model); } return(result); }
/// <inheritdoc /> public List <(Transaction, TransactionData, IWithdrawal)> FindWithdrawalTransactions(uint256 depositId = null) { lock (this.lockObject) { List <(Transaction, TransactionData, IWithdrawal)> withdrawals = new List <(Transaction, TransactionData, IWithdrawal)>(); foreach (TransactionData transactionData in this.Wallet.MultiSigAddress.Transactions) { Transaction walletTran = transactionData.GetFullTransaction(this.network); IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(walletTran, transactionData.BlockHash, transactionData.BlockHeight ?? 0); if (withdrawal == null) { continue; } if (depositId != null && withdrawal.DepositId != depositId) { continue; } withdrawals.Add((walletTran, transactionData, withdrawal)); } return(withdrawals); } }
public AccountWithdrawalResponse Withdraw(string accountNumber, decimal amount) { AccountWithdrawalResponse response = new AccountWithdrawalResponse { Success = false, Account = _accountRepository.LoadAccount(accountNumber) }; if (response.Account == null) { response.Message = $"{accountNumber} is not a valid account."; } else { //var someThing = DIContainer.Kernel.GetBindings(DIContainer.Kernel.GetService().Withdraw() IWithdrawal depositRule = WithdrawalRulesFactory.Create(response.Account.Type); response = depositRule.Withdraw(response.Account, amount); } if (response.Success) { _accountRepository.SaveAccount(response.Account); } return(response); }
// TODO: These can be more efficient, i.e. remove the wallet calls from GetHistory // And use a different model for Withdrawals. It doesn't quite map to the Withdrawal class. /// <summary> /// Get the history of successful withdrawals. /// </summary> /// <param name="maximumEntriesToReturn">The maximum number of entries to return.</param> /// <returns>A <see cref="WithdrawalModel"/> object containing a history of withdrawals.</returns> public List <WithdrawalModel> GetHistory(int maximumEntriesToReturn) { var result = new List <WithdrawalModel>(); ICrossChainTransfer[] transfers = this.crossChainTransferStore.GetTransfersByStatus(new[] { CrossChainTransferStatus.SeenInBlock }); foreach (ICrossChainTransfer transfer in transfers.OrderByDescending(t => t.BlockHeight)) { if (maximumEntriesToReturn-- <= 0) { break; } // Extract the withdrawal details from the recorded "PartialTransaction". IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transfer.PartialTransaction, transfer.BlockHash, (int)transfer.BlockHeight); var model = new WithdrawalModel(); model.withdrawal = withdrawal; model.TransferStatus = transfer?.Status.ToString(); result.Add(model); } return(result); }
public WithdrawalModel(Network network, IWithdrawal withdrawal, ICrossChainTransfer transfer) { this.Id = withdrawal.Id; this.DepositId = withdrawal.DepositId; this.Amount = withdrawal.Amount; this.BlockHash = withdrawal.BlockHash; this.BlockHeight = withdrawal.BlockNumber; this.PayingTo = withdrawal.TargetAddress == network.CirrusRewardDummyAddress ? "Reward Distribution" : withdrawal.TargetAddress; this.TransferStatus = transfer?.Status.ToString(); }
public void ShouldSerialiseAsJson() { IWithdrawal withdrawal = TestingValues.GetWithdrawal(); string asJson = withdrawal.ToString(); var reconverted = JsonConvert.DeserializeObject <Withdrawal>(asJson); reconverted.BlockHash.Should().Be(withdrawal.BlockHash); reconverted.Amount.Satoshi.Should().Be(withdrawal.Amount.Satoshi); reconverted.BlockNumber.Should().Be(withdrawal.BlockNumber); reconverted.Id.Should().Be(withdrawal.Id); reconverted.DepositId.Should().Be(withdrawal.DepositId); reconverted.TargetAddress.Should().Be(withdrawal.TargetAddress); }
/// <inheritdoc /> public IEnumerable <IWithdrawal> GetWithdrawals() { foreach (TransactionData transactionData in this.Wallet.MultiSigAddress.Transactions.OrderByDescending(t => t.CreationTime)) { Transaction walletTrx = transactionData.GetFullTransaction(this.network); IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(walletTrx, transactionData.BlockHash, transactionData.BlockHeight ?? 0); if (withdrawal == null) { continue; } yield return(withdrawal); } }
[TestCase("33333", "BasicAccount", 100, AccountType.Basic, 150, -60, true)] //Pass, Overdraft public void BasicAccount_WithdrawalRuleTest(string accountNumber, string name, decimal balance, AccountType accountType, decimal amount, decimal newBalance, bool expectedResult) { IWithdrawal withdrawalRule = WithdrawalRulesFactory.Create(AccountType.Basic); Account account = new Account() { AccountNumber = accountNumber, Balance = balance, Name = name, Type = accountType }; AccountWithdrawalResponse response = withdrawalRule.Withdraw(account, amount); Assert.AreEqual(expectedResult, response.Success); }
private void VerifyWithdrawalData( IWithdrawal withdrawals, long amount, Block block, int blockHeight, Transaction validWithdrawalTransaction, uint256 opReturnDepositId, Script targetScript) { withdrawals.Amount.Satoshi.Should().Be(amount); withdrawals.BlockHash.Should().Be(block.Header.GetHash()); withdrawals.BlockNumber.Should().Be(blockHeight); withdrawals.Id.Should().Be(validWithdrawalTransaction.GetHash()); withdrawals.DepositId.Should().Be(opReturnDepositId); withdrawals.TargetAddress.Should().Be(targetScript.GetScriptAddress(this.network).ToString()); }
public void StoringDepositsWhenTargetIsMultisigIsIgnoredIffOurMultisig() { var dataFolder = new DataFolder(TestBase.CreateTestDir(this)); this.Init(dataFolder); this.AddFunding(); this.AppendBlocks(WithdrawalTransactionBuilder.MinConfirmations); MultiSigAddress multiSigAddress = this.wallet.MultiSigAddress; using (ICrossChainTransferStore crossChainTransferStore = this.CreateStore()) { crossChainTransferStore.Initialize(); crossChainTransferStore.Start(); TestBase.WaitLoopMessage(() => (this.ChainIndexer.Tip.Height == crossChainTransferStore.TipHashAndHeight.Height, $"ChainIndexer.Height:{this.ChainIndexer.Tip.Height} Store.TipHashHeight:{crossChainTransferStore.TipHashAndHeight.Height}")); Assert.Equal(this.ChainIndexer.Tip.HashBlock, crossChainTransferStore.TipHashAndHeight.HashBlock); // Forwarding money already in the multisig address to the multisig address is ignored. BitcoinAddress address1 = multiSigAddress.RedeemScript.Hash.GetAddress(this.network); BitcoinAddress address2 = new Script("").Hash.GetAddress(this.network); var deposit1 = new Deposit(0, new Money(160m, MoneyUnit.BTC), address1.ToString(), crossChainTransferStore.NextMatureDepositHeight, 1); var deposit2 = new Deposit(1, new Money(160m, MoneyUnit.BTC), address2.ToString(), crossChainTransferStore.NextMatureDepositHeight, 1); MaturedBlockDepositsModel[] blockDeposits = new[] { new MaturedBlockDepositsModel( new MaturedBlockInfoModel() { BlockHash = 1, BlockHeight = crossChainTransferStore.NextMatureDepositHeight }, new[] { deposit1, deposit2 }) }; crossChainTransferStore.RecordLatestMatureDepositsAsync(blockDeposits).GetAwaiter().GetResult(); Transaction[] partialTransactions = crossChainTransferStore.GetTransfersByStatus(new[] { CrossChainTransferStatus.Partial }).Select(x => x.PartialTransaction).ToArray(); Transaction[] suspendedTransactions = crossChainTransferStore.GetTransfersByStatus(new [] { CrossChainTransferStatus.Suspended }).Select(x => x.PartialTransaction).ToArray(); // Only the deposit going towards a different multisig address is accepted. The other is ignored. Assert.Single(partialTransactions); Assert.Empty(suspendedTransactions); IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(partialTransactions[0], null, 1); Assert.Equal((uint256)1, withdrawal.DepositId); } }
/// <inheritdoc /> public IReadOnlyList <IWithdrawal> ExtractWithdrawalsFromBlock(Block block, int blockHeight) { var withdrawals = new List <IWithdrawal>(); foreach (Transaction transaction in block.Transactions) { IWithdrawal withdrawal = this.ExtractWithdrawalFromTransaction(transaction, block.GetHash(), blockHeight); if (withdrawal != null) { withdrawals.Add(withdrawal); } } ReadOnlyCollection <IWithdrawal> withdrawalsFromBlock = withdrawals.AsReadOnly(); return(withdrawalsFromBlock); }
/// <summary> /// Get the history of successful withdrawals. /// </summary> /// <param name="crossChainTransfers">The list of transfers to report on.</param> /// <param name="maximumEntriesToReturn">The maximum number of entries to return.</param> /// <returns>A <see cref="WithdrawalModel"/> object containing a history of withdrawals.</returns> public List <WithdrawalModel> GetHistory(IEnumerable <ICrossChainTransfer> crossChainTransfers, int maximumEntriesToReturn) { var result = new List <WithdrawalModel>(); foreach (ICrossChainTransfer transfer in crossChainTransfers.OrderByDescending(t => t.BlockHeight)) { if (maximumEntriesToReturn-- <= 0) { break; } // Extract the withdrawal details from the recorded "PartialTransaction". IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transfer.PartialTransaction, transfer.BlockHash, (int)transfer.BlockHeight); var model = new WithdrawalModel(this.network, withdrawal, transfer); result.Add(model); } return(result); }
/// <inheritdoc /> public IReadOnlyList <IWithdrawal> ExtractWithdrawalsFromBlock(Block block, int blockHeight) { var withdrawals = new List <IWithdrawal>(); if (block.Transactions.Count <= 1) { return(withdrawals); } foreach (Transaction transaction in block.Transactions) { IWithdrawal withdrawal = this.ExtractWithdrawalFromTransaction(transaction, block.GetHash(), blockHeight); if (withdrawal != null) { withdrawals.Add(withdrawal); } } return(withdrawals); }
/// <inheritdoc /> public List <(Transaction, IWithdrawal)> FindWithdrawalTransactions(uint256 depositId = null) { // A withdrawal is a transaction that spends funds from the multisig wallet. lock (this.lockObject) { var withdrawals = new List <(Transaction transaction, IWithdrawal withdrawal)>(); foreach (TransactionData transactionData in this.Wallet.MultiSigAddress.Transactions) { if (transactionData.SpendingDetails == null) { continue; } Transaction walletTran = this.network.CreateTransaction(transactionData.SpendingDetails.Hex); if (withdrawals.Any(w => w.transaction.GetHash() == walletTran.GetHash())) { continue; } int? blockHeight = transactionData.SpendingDetails.BlockHeight; uint256 blockHash = transactionData.SpendingDetails.BlockHash; IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(walletTran, blockHash, blockHeight ?? 0); if (withdrawal == null) { continue; } if (depositId != null && withdrawal.DepositId != depositId) { continue; } withdrawals.Add((walletTran, withdrawal)); } return(withdrawals); } }
private static void SetupWithdrawalScheme(WithdrawalType type) { //TODO : this is poor man's injection. For now, since its a basic application we can live without have a container and DI logic //to inject the scheme. Open to extension though. switch (type) { case WithdrawalType.PreferredDenominationRules: DenominationPreferenceRules rules = new DenominationPreferenceRules(new List <DenominationType> { DenominationType.TwentyPound }); withdrawalScheme = new WithdrawalByPreferedDenomination(moneyStore, rules); break; case WithdrawalType.LeastNumberOfItems: withdrawalScheme = new WithdrawalByLeastNumberOfItems(moneyStore); break; default: throw new Exception("This schema is not supported yet"); } }
private string CollectStats() { StringBuilder benchLog = new StringBuilder(); benchLog.AppendLine(); benchLog.AppendLine("====== Federation Wallet ======"); (Money ConfirmedAmount, Money UnConfirmedAmount)balances = this.federationWalletManager.GetWallet().GetSpendableAmount(); bool isFederationActive = this.federationWalletManager.IsFederationActive(); benchLog.AppendLine("Federation Wallet: ".PadRight(LoggingConfiguration.ColumnLength) + " Confirmed balance: " + balances.ConfirmedAmount.ToString().PadRight(LoggingConfiguration.ColumnLength) + " Unconfirmed balance: " + balances.UnConfirmedAmount.ToString().PadRight(LoggingConfiguration.ColumnLength) + " Federation Status: " + (isFederationActive ? "Active" : "Inactive")); benchLog.AppendLine(); var apiSettings = (ApiSettings)this.fullNode.Services.ServiceProvider.GetService(typeof(ApiSettings)); if (!isFederationActive) { var warning = "=============================================".PadLeft(10, '=') + Environment.NewLine + Environment.NewLine + "Federation node not enabled. You will not be able to sign transactions until you enable it." + Environment.NewLine + $"If not done previously, please import your private key using " + Environment.NewLine + $"{apiSettings.ApiUri}/api/FederationWallet/{FederationWalletRouteEndPoint.ImportKey}" + Environment.NewLine + $"Then enable the wallet using " + Environment.NewLine + $"{apiSettings.ApiUri}/api/FederationWallet/{FederationWalletRouteEndPoint.EnableFederation}" + Environment.NewLine + Environment.NewLine + $"============================================".PadLeft(10, '=') + Environment.NewLine; benchLog.AppendLine(warning); } // Display recent withdrawals (if any). IWithdrawal[] withdrawals = this.federationWalletManager.GetWithdrawals().Take(5).ToArray(); if (withdrawals.Length > 0) { benchLog.AppendLine("-- Recent Withdrawals --"); ICrossChainTransfer[] transfers = this.crossChainTransferStore.GetAsync(withdrawals.Select(w => w.DepositId).ToArray()).GetAwaiter().GetResult().ToArray(); for (int i = 0; i < withdrawals.Length; i++) { ICrossChainTransfer transfer = transfers[i]; IWithdrawal withdrawal = withdrawals[i]; string line = withdrawal.GetInfo() + " Status=" + transfer?.Status; switch (transfer?.Status) { case CrossChainTransferStatus.FullySigned: if (this.mempoolManager.InfoAsync(withdrawal.Id).GetAwaiter().GetResult() != null) { line += "+InMempool"; } break; case CrossChainTransferStatus.Partial: line += " (" + transfer.GetSignatureCount(this.network) + "/" + this.federationGatewaySettings.MultiSigM + ")"; break; } benchLog.AppendLine(line); } benchLog.AppendLine(); } benchLog.AppendLine("====== NodeStore ======"); this.AddBenchmarkLine(benchLog, new (string, int)[] {
public Cashier(IWithdrawal withdrawal) { _withdrawal = withdrawal; }
/// <inheritdoc /> public bool ProcessTransaction(Transaction transaction, int?blockHeight = null, uint256 blockHash = null, Block block = null) { Guard.NotNull(transaction, nameof(transaction)); Guard.Assert(blockHash == (blockHash ?? block?.GetHash())); if (this.Wallet == null) { this.logger.LogTrace("(-)"); return(false); } bool foundReceivingTrx = false, foundSendingTrx = false; lock (this.lockObject) { // Check if we're trying to spend a utxo twice foreach (TxIn input in transaction.Inputs) { if (!this.outpointLookup.TryGetValue(input.PrevOut, out TransactionData tTx)) { continue; } // If we're trying to spend an input that is already spent, and it's not coming in a new block, don't reserve the transaction. // This would be the case when blocks are synced in between CrossChainTransferStore calling // FederationWalletTransactionHandler.BuildTransaction and FederationWalletManager.ProcessTransaction. if (blockHeight == null && tTx.SpendingDetails?.BlockHeight != null) { return(false); } } // Extract the withdrawal from the transaction (if any). IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transaction, blockHash, blockHeight ?? 0); if (withdrawal != null) { // Exit if already present and included in a block. List <(Transaction transaction, IWithdrawal withdrawal)> walletData = this.FindWithdrawalTransactions(withdrawal.DepositId); if ((walletData.Count == 1) && (walletData[0].withdrawal.BlockNumber != 0)) { this.logger.LogTrace("Deposit {0} Already included in block.", withdrawal.DepositId); return(false); } // Remove this to prevent duplicates if the transaction hash has changed. if (walletData.Count != 0) { this.logger.LogTrace("Removing duplicates for {0}", withdrawal.DepositId); this.RemoveTransientTransactions(withdrawal.DepositId); } } // Check the outputs. foreach (TxOut utxo in transaction.Outputs) { // Check if the outputs contain one of our addresses. if (this.Wallet.MultiSigAddress.ScriptPubKey == utxo.ScriptPubKey) { this.AddTransactionToWallet(transaction, utxo, blockHeight, blockHash, block); foundReceivingTrx = true; } } // Check the inputs - include those that have a reference to a transaction containing one of our scripts and the same index. foreach (TxIn input in transaction.Inputs) { if (!this.outpointLookup.TryGetValue(input.PrevOut, out TransactionData tTx)) { continue; } // Get the details of the outputs paid out. IEnumerable <TxOut> paidOutTo = transaction.Outputs.Where(o => { // If script is empty ignore it. if (o.IsEmpty) { return(false); } // Check if the destination script is one of the wallet's. // TODO fix this bool found = this.Wallet.MultiSigAddress.ScriptPubKey == o.ScriptPubKey; // Include the keys not included in our wallets (external payees). if (!found) { return(true); } // Include the keys that are in the wallet but that are for receiving // addresses (which would mean the user paid itself). // We also exclude the keys involved in a staking transaction. //return !addr.IsChangeAddress() && !transaction.IsCoinStake; return(true); }); this.AddSpendingTransactionToWallet(transaction, paidOutTo, tTx.Id, tTx.Index, blockHeight, blockHash, block); foundSendingTrx = true; } } // Figure out what to do when this transaction is found to affect the wallet. if (foundSendingTrx || foundReceivingTrx) { // Save the wallet when the transaction was not included in a block. if (blockHeight == null) { this.SaveWallet(); } } return(foundSendingTrx || foundReceivingTrx); }
/// <inheritdoc /> public bool ProcessTransaction(Transaction transaction, int?blockHeight = null, Block block = null, bool isPropagated = true) { Guard.NotNull(transaction, nameof(transaction)); uint256 hash = transaction.GetHash(); this.logger.LogTrace("({0}:'{1}',{2}:{3})", nameof(transaction), hash, nameof(blockHeight), blockHeight); if (this.Wallet == null) { this.logger.LogTrace("(-)"); return(false); } bool foundReceivingTrx = false, foundSendingTrx = false; lock (this.lockObject) { // Extract the withdrawal from the transaction (if any). IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(transaction, block?.GetHash(), blockHeight ?? 0); if (withdrawal != null) { // Exit if already present and included in a block. List <(Transaction, TransactionData, IWithdrawal)> walletData = FindWithdrawalTransactions(withdrawal.DepositId); if ((walletData.Count == 1) && (walletData[0].Item2.BlockHeight != null)) { return(false); } // Remove this to prevent duplicates if the transaction hash has changed. if (walletData.Count != 0) { this.RemoveTransientTransactions(withdrawal.DepositId); } } // Check the outputs. foreach (TxOut utxo in transaction.Outputs) { // Check if the outputs contain one of our addresses. if (this.Wallet.MultiSigAddress.ScriptPubKey == utxo.ScriptPubKey) { this.AddTransactionToWallet(transaction, utxo, blockHeight, block, isPropagated); foundReceivingTrx = true; } } // Check the inputs - include those that have a reference to a transaction containing one of our scripts and the same index. foreach (TxIn input in transaction.Inputs) { if (!this.outpointLookup.TryGetValue(input.PrevOut, out TransactionData tTx)) { continue; } // Get the details of the outputs paid out. IEnumerable <TxOut> paidOutTo = transaction.Outputs.Where(o => { // If script is empty ignore it. if (o.IsEmpty) { return(false); } // Check if the destination script is one of the wallet's. // TODO fix this bool found = this.Wallet.MultiSigAddress.ScriptPubKey == o.ScriptPubKey; // Include the keys not included in our wallets (external payees). if (!found) { return(true); } // Include the keys that are in the wallet but that are for receiving // addresses (which would mean the user paid itself). // We also exclude the keys involved in a staking transaction. //return !addr.IsChangeAddress() && !transaction.IsCoinStake; return(true); }); this.AddSpendingTransactionToWallet(transaction, paidOutTo, tTx.Id, tTx.Index, blockHeight, block); foundSendingTrx = true; } } // Figure out what to do when this transaction is found to affect the wallet. if (foundSendingTrx || foundReceivingTrx) { // Save the wallet when the transaction was not included in a block. if (blockHeight == null) { this.SaveWallet(); } } this.logger.LogTrace("(-)"); return(foundSendingTrx || foundReceivingTrx); }
/// <inheritdoc /> public IReadOnlyList <IWithdrawal> ExtractWithdrawalsFromBlock(Block block, int blockHeight) { var withdrawals = new List <IWithdrawal>(); // Check if this is the target height for a conversion transaction from wSTRAX back to STRAX. // These get returned before any other withdrawal transactions in the block to ensure consistent ordering. List <ConversionRequest> burnRequests = this.conversionRequestRepository.GetAllBurn(true); if (burnRequests != null) { foreach (ConversionRequest burnRequest in burnRequests) { // So that we don't get stuck if we miss one inadvertently, don't break out of the loop if the height is less. if (burnRequest.BlockHeight < blockHeight) { continue; } // We expect them to be ordered, so as soon as they exceed the current height, ignore the rest. if (burnRequest.BlockHeight > blockHeight) { break; } // We use the transaction ID from the Ethereum chain as the request ID for the withdrawal. // To parse it into a uint256 we need to trim the leading hex marker from the string. uint256 requestId; try { requestId = new uint256(burnRequest.RequestId.Replace("0x", "")); } catch (Exception) { continue; } withdrawals.Add(new Withdrawal(requestId, null, Money.Satoshis(burnRequest.Amount), burnRequest.DestinationAddress, burnRequest.BlockHeight, block.GetHash())); // Immediately flag it as processed & persist so that it can't be added again. burnRequest.Processed = true; burnRequest.RequestStatus = ConversionRequestStatus.Processed; this.conversionRequestRepository.Save(burnRequest); } } if (block.Transactions.Count <= 1) { return(withdrawals); } foreach (Transaction transaction in block.Transactions) { IWithdrawal withdrawal = this.ExtractWithdrawalFromTransaction(transaction, block.GetHash(), blockHeight); if (withdrawal != null) { withdrawals.Add(withdrawal); } } return(withdrawals); }