diff --git a/backend/Application/Aggregates/Contract/Configurations/ContractEntityTypeConfigurations.cs b/backend/Application/Aggregates/Contract/Configurations/ContractEntityTypeConfigurations.cs index e507169f2..a01c4ff14 100644 --- a/backend/Application/Aggregates/Contract/Configurations/ContractEntityTypeConfigurations.cs +++ b/backend/Application/Aggregates/Contract/Configurations/ContractEntityTypeConfigurations.cs @@ -41,5 +41,10 @@ public void Configure(EntityTypeBuilder builder) .HasMany(sm => sm.ContractEvents) .WithOne() .HasForeignKey(sme => new { sme.ContractAddressIndex, sme.ContractAddressSubIndex }); + + builder + .HasMany(c => c.ModuleReferenceContractLinkEvents) + .WithOne() + .HasForeignKey(link => new { link.ContractAddressIndex, link.ContractAddressSubIndex }); } } \ No newline at end of file diff --git a/backend/Application/Aggregates/Contract/Entities/Contract.cs b/backend/Application/Aggregates/Contract/Entities/Contract.cs index beb0193a8..41b6c0db0 100644 --- a/backend/Application/Aggregates/Contract/Entities/Contract.cs +++ b/backend/Application/Aggregates/Contract/Entities/Contract.cs @@ -26,7 +26,8 @@ public sealed class Contract public ImportSource Source { get; init; } public DateTimeOffset BlockSlotTime { get; init; } public DateTimeOffset CreatedAt { get; init; } = DateTime.UtcNow; - public ICollection ContractEvents { get; set; } + public ICollection ContractEvents { get; set; } = null!; + public ICollection ModuleReferenceContractLinkEvents { get; set; } = null!; /// /// Needed for EF Core @@ -58,13 +59,27 @@ internal Contract( [ExtendObjectType(typeof(Query))] public class ContractQuery { + /// + /// Get contracts with pagination support. + /// + /// Currently contracts module reference are not updated for the lifetime of the contract. Hence often there will + /// be only one module link event for each contract. + /// + /// Because of this we are currently not using . + /// If performance issues on this query is seen and module reference links increases then look into using above splitting technique. + /// + /// + /// See EF Core split queries for more information. + /// [UsePaging] public IQueryable GetContracts( GraphQlDbContext context) { return context.Contract .AsNoTracking() - .Include(s => s.ContractEvents); + .Include(s => s.ContractEvents) + .Include(s => s.ModuleReferenceContractLinkEvents) + .OrderByDescending(c => c.ContractAddressIndex); } } @@ -77,6 +92,20 @@ public sealed class ContractExtensions public ContractAddress GetContractAddress([Parent] Contract contract) => new(contract.ContractAddressIndex, contract.ContractAddressSubIndex); + /// + /// Returns the current linked module reference which is the latest added . + /// + public string GetModuleReference([Parent] Contract contract) + { + var link = contract.ModuleReferenceContractLinkEvents + .Where(link => link.LinkAction == ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Added) + .OrderByDescending(link => link.BlockHeight) + .ThenByDescending(link => link.TransactionIndex) + .ThenByDescending(link => link.EventIndex) + .First(); + return link.ModuleReference; + } + /// /// Returns aggregated amount from events on contract. /// diff --git a/backend/Application/Common/Logging/TraceEnricher.cs b/backend/Application/Common/Logging/TraceEnricher.cs index ac10e01e4..86079a053 100644 --- a/backend/Application/Common/Logging/TraceEnricher.cs +++ b/backend/Application/Common/Logging/TraceEnricher.cs @@ -18,7 +18,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Id, Activity.Current.Id)); - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(TraceId, Activity.Current.Id)); - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(SpanId, Activity.Current.Id)); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(TraceId, Activity.Current.TraceId)); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(SpanId, Activity.Current.SpanId)); } -} \ No newline at end of file +} diff --git a/backend/Application/Observability/ObservabilityExecutionDiagnosticEventListener.cs b/backend/Application/Observability/ObservabilityExecutionDiagnosticEventListener.cs index 3db6ac011..680e33b86 100644 --- a/backend/Application/Observability/ObservabilityExecutionDiagnosticEventListener.cs +++ b/backend/Application/Observability/ObservabilityExecutionDiagnosticEventListener.cs @@ -170,4 +170,4 @@ private static bool TryGetQuery(IHasContextData context, out string? query) return false; } -} \ No newline at end of file +} diff --git a/backend/Tests/Aggregates/Contract/Entities/ContractTests.cs b/backend/Tests/Aggregates/Contract/Entities/ContractTests.cs new file mode 100644 index 000000000..79a785e69 --- /dev/null +++ b/backend/Tests/Aggregates/Contract/Entities/ContractTests.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Application.Aggregates.Contract.Entities; +using Application.Api.GraphQL; +using Application.Api.GraphQL.Transactions; +using FluentAssertions; +using Tests.TestUtilities.Builders; + +namespace Tests.Aggregates.Contract.Entities; + +public sealed class ContractTests +{ + private readonly Application.Aggregates.Contract.Entities.Contract.ContractExtensions _contractExtensions; + + public ContractTests() + { + _contractExtensions = new Application.Aggregates.Contract.Entities.Contract.ContractExtensions(); + } + + [Fact] + public void WhenGetModuleReference_ThenReturnLatestAdded() + { + // Arrange + const string expectedModuleReference = "foobar"; + var a = ModuleReferenceContractLinkEventBuilder.Create() + .WithBlockHeight(6) + .WithTransactionIndex(5) + .WithEventIndex(4) + .WithModuleReference(expectedModuleReference) + .WithLinkAction(ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Added) + .Build(); + var b = ModuleReferenceContractLinkEventBuilder.Create() + .WithBlockHeight(7) + .WithTransactionIndex(5) + .WithEventIndex(4) + .WithLinkAction(ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Removed) + .Build(); + var c = ModuleReferenceContractLinkEventBuilder.Create() + .WithBlockHeight(6) + .WithTransactionIndex(4) + .WithLinkAction(ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Added) + .Build(); + var d = ModuleReferenceContractLinkEventBuilder.Create() + .WithBlockHeight(5) + .WithLinkAction(ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Added) + .Build(); + var contract = ContractBuilder + .Create() + .WithModuleReferenceContractLinkEvents(new List + { + a, b, c, d + }) + .Build(); + + // Act + var moduleReference = _contractExtensions.GetModuleReference(contract); + + // Assert + moduleReference.Should().Be(expectedModuleReference); + } + + [Fact] + public void WhenGetAmount_ThenReturnCorrectAmount() + { + // Arrange + var from = new ContractAddress(1, 1); + var to = new ContractAddress(2, 1); + var contractEvents = new List{ + ContractEventBuilder.Create().WithEvent(new Transferred(42, from, to)).Build(), + ContractEventBuilder.Create().WithEvent(new Transferred(2, to, from)).Build(), + ContractEventBuilder.Create().WithEvent(new ContractInitialized("", new ContractAddress(1,0), 10, "", ContractVersion.V0, Array.Empty())).Build(), + ContractEventBuilder.Create().WithEvent(new ContractUpdated(new ContractAddress(1,0), new ContractAddress(1,0), 7, "", "", ContractVersion.V0, Array.Empty())).Build() + }; + var contract = ContractBuilder + .Create() + .WithContractAddress(to) + .WithContractEvents(contractEvents) + .Build(); + + // Act + var amount = _contractExtensions.GetAmount(contract); + + // Assert + amount.Should().Be(57); + } +} \ No newline at end of file diff --git a/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql b/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql index 080299d9a..073d0f6b7 100644 --- a/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql +++ b/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql @@ -722,7 +722,9 @@ type Contract { blockSlotTime: DateTime! createdAt: DateTime! contractEvents: [ContractEvent!]! + moduleReferenceContractLinkEvents: [ModuleReferenceContractLinkEvent!]! contractAddress: ContractAddress! + moduleReference: String! amount: Float! } @@ -1193,6 +1195,21 @@ type ModuleNotWf { _: Boolean! @deprecated(reason: "Don't use! This field is only in the schema to make sure reject reasons without any fields are valid types in GraphQL (which does not allow types without any fields)") } +type ModuleReferenceContractLinkEvent { + blockHeight: UnsignedLong! + transactionHash: String! + transactionIndex: UnsignedLong! + eventIndex: UnsignedInt! + moduleReference: String! + contractAddressIndex: UnsignedLong! + contractAddressSubIndex: UnsignedLong! + sender: AccountAddress! + source: ImportSource! + linkAction: ModuleReferenceContractLinkAction! + blockSlotTime: DateTime! + createdAt: DateTime! +} + type NewEncryptedAmount { accountAddress: AccountAddress! newIndex: UnsignedLong! @@ -1939,6 +1956,11 @@ enum MetricsPeriod { LAST_YEAR } +enum ModuleReferenceContractLinkAction { + ADDED + REMOVED +} + enum NodeSortDirection { ASC DSC diff --git a/backend/Tests/TestUtilities/Builders/ContractBuilder.cs b/backend/Tests/TestUtilities/Builders/ContractBuilder.cs new file mode 100644 index 000000000..77d13e8ab --- /dev/null +++ b/backend/Tests/TestUtilities/Builders/ContractBuilder.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Application.Aggregates.Contract.Entities; +using Application.Aggregates.Contract.Types; +using Application.Api.GraphQL; +using Application.Api.GraphQL.Accounts; + +namespace Tests.TestUtilities.Builders; + +internal sealed class ContractBuilder +{ + private readonly ulong _blockHeight = 1; + private readonly string _transactionHash = ""; + private readonly ulong _transactionIndex = 1; + private readonly uint _eventIndex = 1; + private ContractAddress _contractAddress = new(1, 0); + private readonly AccountAddress _accountAddress = new(""); + private readonly ImportSource _source = ImportSource.DatabaseImport; + private readonly DateTimeOffset _dateTimeOffset = DateTimeOffset.UtcNow; + private IList _moduleReferenceContractLinkEvents = new List(); + private IList _contractEvents = new List(); + + private ContractBuilder() {} + + internal static ContractBuilder Create() + { + return new ContractBuilder(); + } + + internal Contract Build() + { + return new Contract( + _blockHeight, + _transactionHash, + _transactionIndex, + _eventIndex, + _contractAddress, + _accountAddress, + _source, + _dateTimeOffset + ) + { + ModuleReferenceContractLinkEvents = _moduleReferenceContractLinkEvents, + ContractEvents = _contractEvents + }; + } + + internal ContractBuilder WithContractEvents(IList events) + { + _contractEvents = events; + return this; + } + + internal ContractBuilder WithModuleReferenceContractLinkEvents(IList events) + { + _moduleReferenceContractLinkEvents = events; + return this; + } + + internal ContractBuilder WithContractAddress(ContractAddress contractAddress) + { + _contractAddress = contractAddress; + return this; + } + +} \ No newline at end of file diff --git a/backend/Tests/TestUtilities/Builders/ContractEventBuilder.cs b/backend/Tests/TestUtilities/Builders/ContractEventBuilder.cs new file mode 100644 index 000000000..de5895bb0 --- /dev/null +++ b/backend/Tests/TestUtilities/Builders/ContractEventBuilder.cs @@ -0,0 +1,53 @@ +using Application.Aggregates.Contract.Entities; +using Application.Aggregates.Contract.Types; +using Application.Api.GraphQL; +using Application.Api.GraphQL.Accounts; +using Application.Api.GraphQL.Transactions; + +namespace Tests.TestUtilities.Builders; + +internal sealed class ContractEventBuilder +{ + private readonly ulong _blockHeight = 1; + private readonly string _transactionHash = ""; + private readonly ulong _transactionIndex = 1; + private readonly uint _eventIndex = 1; + private readonly ContractAddress _contractAddress = new(1, 0); + private readonly AccountAddress _accountAddress = new(""); + private TransactionResultEvent _event = + new Transferred(1, new ContractAddress(1, 0), new ContractAddress(2, 0)); + + private readonly ImportSource _source = ImportSource.DatabaseImport; + private readonly DateTimeOffset _dateTimeOffset = DateTimeOffset.UtcNow; + + private ContractEventBuilder() + { + } + + internal static ContractEventBuilder Create() + { + return new ContractEventBuilder(); + } + + internal ContractEvent Build() + { + return new ContractEvent( + _blockHeight, + _transactionHash, + _transactionIndex, + _eventIndex, + _contractAddress, + _accountAddress, + _event, + _source, + _dateTimeOffset + ); + } + + internal ContractEventBuilder WithEvent(TransactionResultEvent @event) + { + _event = @event; + return this; + } + +} \ No newline at end of file diff --git a/backend/Tests/TestUtilities/Builders/ModuleReferenceContractLinkEventBuilder.cs b/backend/Tests/TestUtilities/Builders/ModuleReferenceContractLinkEventBuilder.cs new file mode 100644 index 000000000..847b70d60 --- /dev/null +++ b/backend/Tests/TestUtilities/Builders/ModuleReferenceContractLinkEventBuilder.cs @@ -0,0 +1,79 @@ +using Application.Aggregates.Contract.Entities; +using Application.Aggregates.Contract.Types; +using Application.Api.GraphQL; +using Application.Api.GraphQL.Accounts; + +namespace Tests.TestUtilities.Builders; + +internal class ModuleReferenceContractLinkEventBuilder +{ + private ulong _blockHeight = 1; + private readonly string _transactionHash = ""; + private ulong _transactionIndex = 1; + private uint _eventIndex = 1; + private string _moduleReference = ""; + private ContractAddress _contractAddress = new(1, 0); + private readonly AccountAddress _accountAddress = new(""); + private readonly ImportSource _source = ImportSource.DatabaseImport; + private ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction _action = + ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction.Added; + private readonly DateTimeOffset _dateTimeOffset = DateTimeOffset.UtcNow; + + private ModuleReferenceContractLinkEventBuilder() {} + + internal static ModuleReferenceContractLinkEventBuilder Create() + { + return new ModuleReferenceContractLinkEventBuilder(); + } + + internal ModuleReferenceContractLinkEvent Build() + { + return new ModuleReferenceContractLinkEvent( + _blockHeight, + _transactionHash, + _transactionIndex, + _eventIndex, + _moduleReference, + _contractAddress, + _accountAddress, + _source, + _action, + _dateTimeOffset + ); + } + + internal ModuleReferenceContractLinkEventBuilder WithModuleReference( + string moduleReference) + { + _moduleReference = moduleReference; + return this; + } + + internal ModuleReferenceContractLinkEventBuilder WithBlockHeight( + ulong blockHeight) + { + _blockHeight = blockHeight; + return this; + } + + internal ModuleReferenceContractLinkEventBuilder WithTransactionIndex( + ulong transactionIndex) + { + _transactionIndex = transactionIndex; + return this; + } + + internal ModuleReferenceContractLinkEventBuilder WithEventIndex( + uint eventIndex) + { + _eventIndex = eventIndex; + return this; + } + + internal ModuleReferenceContractLinkEventBuilder WithLinkAction( + ModuleReferenceContractLinkEvent.ModuleReferenceContractLinkAction action) + { + _action = action; + return this; + } +} \ No newline at end of file diff --git a/frontend/src/components/Navigation/Navigation.vue b/frontend/src/components/Navigation/Navigation.vue index f8350b1b7..752186555 100644 --- a/frontend/src/components/Navigation/Navigation.vue +++ b/frontend/src/components/Navigation/Navigation.vue @@ -36,5 +36,9 @@ const routes: Route[] = [ title: 'Nodes', path: '/nodes', }, + { + title: 'Contracts', + path: '/contracts', + }, ] diff --git a/frontend/src/components/atoms/TextCopy.vue b/frontend/src/components/atoms/TextCopy.vue index 6c6e69319..ea8f599fd 100644 --- a/frontend/src/components/atoms/TextCopy.vue +++ b/frontend/src/components/atoms/TextCopy.vue @@ -7,6 +7,7 @@ >