Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SmartContract History #596

Merged
merged 12 commits into from
Jul 12, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,6 @@ public static IFullNodeBuilder AddSmartContracts(this IFullNodeBuilder fullNodeB
services.AddSingleton<IMethodParameterStringSerializer, MethodParameterStringSerializer>();
services.AddSingleton<ICallDataSerializer, CallDataSerializer>();

// Registers the ScriptAddressReader concrete type and replaces the IScriptAddressReader implementation
// with SmartContractScriptAddressReader, which depends on the ScriptAddressReader concrete type.
services.AddSingleton<ScriptAddressReader>();
services.Replace(new ServiceDescriptor(typeof(IScriptAddressReader), typeof(SmartContractScriptAddressReader), ServiceLifetime.Singleton));

// After setting up, invoke any additional options which can replace services as required.
options?.Invoke(new SmartContractOptions(services, fullNodeBuilder.Network));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,38 +173,19 @@ public IActionResult GetHistory(GetHistoryRequest request)
HdAccount account = this.walletManager.GetAccounts(request.WalletName).First();

// Get a list of all the transactions found in an account (or in a wallet if no account is specified), with the addresses associated with them.
IEnumerable<AccountHistory> accountsHistory = this.walletManager.GetHistory(request.WalletName, account.Name, null);
IEnumerable<AccountHistory> accountsHistory = this.walletManager.GetHistory(request.WalletName, account.Name, null, offset: request.Skip ?? 0, limit: request.Take ?? int.MaxValue, accountAddress: request.Address, forSmartContracts: true);

// Wallet manager returns only 1 when an account name is specified.
AccountHistory accountHistory = accountsHistory.First();

List<FlatHistory> items = new List<FlatHistory>();// accountHistory.History.ToList();//.Where(x => x.Address.Address == request.Address).ToList();

// Represents a sublist of transactions associated with receive addresses + a sublist of already spent transactions associated with change addresses.
// In effect, we filter out 'change' transactions that are not spent, as we don't want to show these in the history.
List<FlatHistory> history = items.Where(t => !t.Address.IsChangeAddress() || (t.Address.IsChangeAddress() && t.Transaction.IsSpent())).ToList();

// TransactionData in history is confusingly named. A "TransactionData" actually represents an input, and the outputs that spend it are "SpendingDetails".
// There can be multiple "TransactionData" which have the same "SpendingDetails".
// For SCs we need to group spending details by their transaction ID, to get all the inputs related to the same outputs.
// Each group represents 1 SC transaction.
// Each item.Transaction in a group is an input.
// Each item.Transaction.SpendingDetails in the group represent the outputs, and they should all be the same so we can pick any one.
var scTransactions = history
.Where(item => item.Transaction.SpendingDetails != null)
.Where(item => item.Transaction.SpendingDetails.Payments.Any(x => x.DestinationScriptPubKey.IsSmartContractExec()))
.GroupBy(item => item.Transaction.SpendingDetails.TransactionId)
.Skip(request.Skip ?? 0)
.Take(request.Take ?? history.Count)
.Select(g => new
{
TransactionId = g.Key,
InputAmount = g.Sum(i => i.Transaction.Amount), // Sum the inputs to the SC transaction.
Outputs = g.First().Transaction.SpendingDetails.Payments, // Each item in the group will have the same outputs.
OutputAmount = g.First().Transaction.SpendingDetails.Payments.Sum(o => o.Amount),
BlockHeight = g.First().Transaction.SpendingDetails.BlockHeight // Each item in the group will have the same block height.
})
.ToList();
var scTransactions = accountHistory.History.Select(h => new
{
TransactionId = uint256.Parse(h.Id),
Fee = h.Fee,
SendToScriptPubKey = Script.FromHex(h.SendToScriptPubkey),
OutputAmount = h.Amount,
BlockHeight = h.BlockHeight
}).ToList();

// Get all receipts in one transaction
IList<Receipt> receipts = this.receiptRepository.RetrieveMany(scTransactions.Select(x => x.TransactionId).ToList());
Expand All @@ -214,36 +195,33 @@ public IActionResult GetHistory(GetHistoryRequest request)
var scTransaction = scTransactions[i];
Receipt receipt = receipts[i];

// Consensus rules state that each transaction can have only one smart contract exec output.
PaymentDetails scPayment = scTransaction.Outputs.First(x => x.DestinationScriptPubKey.IsSmartContractExec());

// This will always give us a value - the transaction has to be serializable to get past consensus.
Result<ContractTxData> txDataResult = this.callDataSerializer.Deserialize(scPayment.DestinationScriptPubKey.ToBytes());
Result<ContractTxData> txDataResult = this.callDataSerializer.Deserialize(scTransaction.SendToScriptPubKey.ToBytes());
ContractTxData txData = txDataResult.Value;

// If the receipt is not available yet, we don't know how much gas was consumed so use the full gas budget.
ulong gasFee = receipt != null
? receipt.GasUsed * receipt.GasPrice
: txData.GasCostBudget;

long totalFees = scTransaction.InputAmount - scTransaction.OutputAmount;
long totalFees = scTransaction.Fee;
Money transactionFee = Money.FromUnit(totalFees, MoneyUnit.Satoshi) - Money.FromUnit(txData.GasCostBudget, MoneyUnit.Satoshi);

var result = new ContractTransactionItem
{
Amount = scPayment.Amount.ToUnit(MoneyUnit.Satoshi),
Amount = new Money(scTransaction.OutputAmount).ToUnit(MoneyUnit.Satoshi),
BlockHeight = scTransaction.BlockHeight,
Hash = scTransaction.TransactionId,
TransactionFee = transactionFee.ToUnit(MoneyUnit.Satoshi),
GasFee = gasFee
};

if (scPayment.DestinationScriptPubKey.IsSmartContractCreate())
if (scTransaction.SendToScriptPubKey.IsSmartContractCreate())
{
result.Type = ContractTransactionItemType.ContractCreate;
result.To = receipt?.NewContractAddress?.ToBase58Address(this.network) ?? string.Empty;
}
else if (scPayment.DestinationScriptPubKey.IsSmartContractCall())
else if (scTransaction.SendToScriptPubKey.IsSmartContractCall())
{
result.Type = ContractTransactionItemType.ContractCall;
result.To = txData.ContractAddress.ToBase58Address(this.network);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
using Stratis.Bitcoin.Builder;
using Stratis.Bitcoin.Builder.Feature;
using Stratis.Bitcoin.Configuration.Logging;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Interfaces;

namespace Stratis.Bitcoin.Features.SmartContracts.Wallet
{
Expand Down Expand Up @@ -52,6 +54,11 @@ public static IFullNodeBuilder UseSmartContractWallet(this IFullNodeBuilder full
.DependOn<BaseWalletFeature>()
.FeatureServices(services =>
{
// Registers the ScriptAddressReader concrete type and replaces the IScriptAddressReader implementation
// with SmartContractScriptAddressReader, which depends on the ScriptAddressReader concrete type.
services.AddSingleton<ScriptAddressReader>();
services.Replace(new ServiceDescriptor(typeof(IScriptAddressReader), typeof(SmartContractScriptAddressReader), ServiceLifetime.Singleton));

services.RemoveAll(typeof(StandardTransactionPolicy));
services.AddSingleton<StandardTransactionPolicy, SmartContractTransactionPolicy>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ public void GetHistoryWithExceptionReturnsBadRequest()
{
string walletName = "myWallet";
var mockWalletManager = this.ConfigureMock<IWalletManager>(mock =>
mock.Setup(w => w.GetHistory("myWallet", WalletManager.DefaultAccount, null, 100, 0))
mock.Setup(w => w.GetHistory("myWallet", WalletManager.DefaultAccount, null, 100, 0, null, false))
.Throws(new InvalidOperationException("Issue retrieving wallets.")));
mockWalletManager.Setup(w => w.GetWallet(walletName)).Returns(new Wallet());

Expand Down
2 changes: 2 additions & 0 deletions src/Stratis.Bitcoin.Features.Wallet/FlatHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public FlattenedHistoryItem()

public sealed class FlattenedHistoryItemPayment
{
public Script DestinationScriptPubKey { get; set; }

public string DestinationAddress { get; set; }

public Money Amount { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public interface IWalletManager
/// <param name="limit">Limit the result set by this amount (primarily used for pagination).</param>
/// <param name="offset">Skip (offset) the result set by this amount (primarily used for pagination).</param>
/// <returns>Collection of address history and transaction pairs.</returns>
IEnumerable<AccountHistory> GetHistory(string walletName, string accountName = null, string searchQuery = null, int limit = int.MaxValue, int offset = 0);
IEnumerable<AccountHistory> GetHistory(string walletName, string accountName = null, string searchQuery = null, int limit = int.MaxValue, int offset = 0, string accountAddress = null, bool forSmartContracts = false);

/// <summary>
/// Gets the balance of transactions contained in an account.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,10 @@ public interface IWalletRepository
/// <param name="limit">Limit the result set by this amount of records (used with paging).</param>
/// <param name="offset">Offset the result set start point by this amount of records (used with paging).</param>
/// <param name="txId">Optional transaction filter.</param>
/// <param name="address">An optional account address filter to limit the results to a particular address.</param>
/// <param name="forSmartContracts">If set, gets the smart contract history.</param>
/// <returns>A history of all transactions in the wallet.</returns>
AccountHistory GetHistory(HdAccount account, int limit, int offset, string txId = null);
AccountHistory GetHistory(HdAccount account, int limit, int offset, string txId = null, string address = null, bool forSmartContracts = false);

/// <summary>
/// Allows an unconfirmed transaction to be removed.
Expand Down
4 changes: 2 additions & 2 deletions src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ public WalletHistoryModel GetHistory(WalletHistoryRequest request)
IEnumerable<AccountHistory> accountsHistory;

if (request.Skip.HasValue && request.Take.HasValue)
accountsHistory = this.walletManager.GetHistory(request.WalletName, request.AccountName, request.SearchQuery, request.Take.Value, request.Skip.Value);
accountsHistory = this.walletManager.GetHistory(request.WalletName, request.AccountName, request.SearchQuery, request.Take.Value, request.Skip.Value, accountAddress: request.Address);
else
accountsHistory = this.walletManager.GetHistory(request.WalletName, request.AccountName, request.SearchQuery);
accountsHistory = this.walletManager.GetHistory(request.WalletName, request.AccountName, request.SearchQuery, accountAddress: request.Address);

var model = new WalletHistoryModel();

Expand Down
8 changes: 4 additions & 4 deletions src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ public IEnumerable<HdAddress> GetNewAddresses(WalletAccountReference accountRefe
}

/// <inheritdoc />
public IEnumerable<AccountHistory> GetHistory(string walletName, string accountName = null, string searchQuery = null, int limit = int.MaxValue, int offset = 0)
public IEnumerable<AccountHistory> GetHistory(string walletName, string accountName = null, string searchQuery = null, int limit = int.MaxValue, int offset = 0, string accountAddress = null, bool forSmartContracts = false)
{
Guard.NotEmpty(walletName, nameof(walletName));

Expand All @@ -866,22 +866,22 @@ public IEnumerable<AccountHistory> GetHistory(string walletName, string accountN

foreach (HdAccount account in accounts)
{
accountsHistory.Add(this.GetHistoryForAccount(account, limit, offset, searchQuery));
accountsHistory.Add(this.GetHistoryForAccount(account, limit, offset, searchQuery, accountAddress, forSmartContracts));
}
}

return accountsHistory;
}

protected AccountHistory GetHistoryForAccount(HdAccount account, int limit, int offset, string searchQuery = null)
protected AccountHistory GetHistoryForAccount(HdAccount account, int limit, int offset, string searchQuery = null, string accountAddress = null, bool forSmartContracts = false)
{
Guard.NotNull(account, nameof(account));

var accountHistory = new AccountHistory();

lock (this.lockObject)
{
return this.WalletRepository.GetHistory(account, limit, offset, searchQuery);
return this.WalletRepository.GetHistory(account, limit, offset, searchQuery, accountAddress, forSmartContracts);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1504,13 +1504,13 @@ public AddressIdentifier GetAddressIdentifier(string walletName, string accountN
}

/// <inheritdoc />
public AccountHistory GetHistory(HdAccount account, int limit, int offset, string txId = null)
public AccountHistory GetHistory(HdAccount account, int limit, int offset, string txId = null, string accountAddress = null, bool forSmartContracts = false)
{
Wallet wallet = account.AccountRoot.Wallet;
WalletContainer walletContainer = this.GetWalletContainer(wallet.Name);
(HDWallet HDWallet, DBConnection conn) = (walletContainer.Wallet, walletContainer.Conn);

var result = HDTransactionData.GetHistory(conn, HDWallet.WalletId, account.Index, limit, offset, txId);
var result = HDTransactionData.GetHistory(conn, HDWallet.WalletId, account.Index, limit, offset, txId, accountAddress, forSmartContracts);

// Filter ColdstakeUtxos
result = result.Where(r =>
Expand All @@ -1528,7 +1528,7 @@ public AccountHistory GetHistory(HdAccount account, int limit, int offset, strin

foreach (var group in grouped)
{
result.First().Payments.Add(new FlattenedHistoryItemPayment() { Amount = group.First().Amount, DestinationAddress = group.First().DestinationAddress, IsChange = group.First().IsChange });
result.First().Payments.Add(new FlattenedHistoryItemPayment() { Amount = group.First().Amount, DestinationScriptPubKey = group.First().DestinationScriptPubKey, DestinationAddress = group.First().DestinationAddress, IsChange = group.First().IsChange });
}
}
}
Expand Down
Loading