diff --git a/src/ProjectOrigin.Chronicler.Server/BlockReader/BlockReaderJob.cs b/src/ProjectOrigin.Chronicler.Server/BlockReader/BlockReaderJob.cs index a301b86..f255d0d 100644 --- a/src/ProjectOrigin.Chronicler.Server/BlockReader/BlockReaderJob.cs +++ b/src/ProjectOrigin.Chronicler.Server/BlockReader/BlockReaderJob.cs @@ -89,6 +89,10 @@ private async Task ProcessBlock(IUnitOfWork unitOfWork, string registryName, Reg { await ProcessClaimedEvent(repository, transaction); } + else if (transaction.Header.PayloadType == Electricity.V1.WithdrawnEvent.Descriptor.FullName) + { + await ProcessWithdrawnEvent(repository, transaction); + } } await repository.UpsertReadBlock(new LastReadBlock @@ -167,6 +171,7 @@ await repository.InsertClaimRecord(new ClaimRecord CertificateId = fid.StreamId, Quantity = claimIntent.Quantity, RandomR = claimIntent.RandomR, + State = ClaimRecordState.Claimed }); await repository.DeleteClaimIntent(claimIntent.Id); await repository.DeleteClaimAllocation(allocation.Id); @@ -176,4 +181,11 @@ await repository.InsertClaimRecord(new ClaimRecord _logger.LogTrace("Claim for certificate {registry}-{certificateId} not relevant", fid.RegistryName, fid.StreamId); } } + + private async Task ProcessWithdrawnEvent(IChroniclerRepository repository, Registry.V1.Transaction transaction) + { + var fid = transaction.Header.FederatedStreamId.ToModel(); + + await repository.WithdrawClaimRecord(fid); + } } diff --git a/src/ProjectOrigin.Chronicler.Server/DatabaseScripts/2.sql b/src/ProjectOrigin.Chronicler.Server/DatabaseScripts/2.sql new file mode 100644 index 0000000..d403f8e --- /dev/null +++ b/src/ProjectOrigin.Chronicler.Server/DatabaseScripts/2.sql @@ -0,0 +1 @@ +ALTER TABLE claim_records ADD COLUMN state int NOT NULL DEFAULT 0; diff --git a/src/ProjectOrigin.Chronicler.Server/Models/ClaimRecord.cs b/src/ProjectOrigin.Chronicler.Server/Models/ClaimRecord.cs index 643495b..fbb1bdc 100644 --- a/src/ProjectOrigin.Chronicler.Server/Models/ClaimRecord.cs +++ b/src/ProjectOrigin.Chronicler.Server/Models/ClaimRecord.cs @@ -9,4 +9,11 @@ public record ClaimRecord public required Guid CertificateId { get; init; } public required long Quantity { get; init; } public required byte[] RandomR { get; init; } + public required ClaimRecordState State { get; init; } +} + +public enum ClaimRecordState +{ + Claimed = 0, + Withdrawn = 3 } diff --git a/src/ProjectOrigin.Chronicler.Server/ProjectOrigin.Chronicler.Server.csproj b/src/ProjectOrigin.Chronicler.Server/ProjectOrigin.Chronicler.Server.csproj index 25f9c0d..9c6cbdf 100644 --- a/src/ProjectOrigin.Chronicler.Server/ProjectOrigin.Chronicler.Server.csproj +++ b/src/ProjectOrigin.Chronicler.Server/ProjectOrigin.Chronicler.Server.csproj @@ -29,13 +29,13 @@ - https://raw.githubusercontent.com/project-origin/registry/v1.3.0/src/Protos/registry.proto + https://raw.githubusercontent.com/project-origin/registry/v2.0.0/protos/registry.proto - https://raw.githubusercontent.com/project-origin/registry/v1.1.0/src/Protos/electricity.proto + https://raw.githubusercontent.com/project-origin/verifier_electricity/v1.2.0/protos/electricity.proto - https://raw.githubusercontent.com/project-origin/registry/v1.3.0/src/Protos/common.proto + https://raw.githubusercontent.com/project-origin/registry/v2.0.0/protos/common.proto diff --git a/src/ProjectOrigin.Chronicler.Server/Properties/launchSettings.json b/src/ProjectOrigin.Chronicler.Server/Properties/launchSettings.json new file mode 100644 index 0000000..6bba300 --- /dev/null +++ b/src/ProjectOrigin.Chronicler.Server/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ProjectOrigin.Chronicler.Server": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50753;http://localhost:50754" + } + } +} \ No newline at end of file diff --git a/src/ProjectOrigin.Chronicler.Server/Repositories/ChroniclerRepository.cs b/src/ProjectOrigin.Chronicler.Server/Repositories/ChroniclerRepository.cs index cc8cc5c..d188b6b 100644 --- a/src/ProjectOrigin.Chronicler.Server/Repositories/ChroniclerRepository.cs +++ b/src/ProjectOrigin.Chronicler.Server/Repositories/ChroniclerRepository.cs @@ -145,4 +145,22 @@ public Task InsertClaimRecord(ClaimRecord record) VALUES (@id, @registryName, @certificateId, @quantity, @randomR)", record); } + + public async Task WithdrawClaimRecord(FederatedCertificateId fid) + { + var rowsChanged = await Connection.ExecuteAsync( + @"UPDATE claim_records + SET state = @state + WHERE registry_name = @registryName + AND certificate_id = @certificateId", + new + { + registryName = fid.RegistryName, + certificateId = fid.StreamId, + state = ClaimRecordState.Withdrawn + }); + + if (rowsChanged != 1) + throw new InvalidOperationException($"ClaimRecord with registry {fid.RegistryName} and certificateId {fid.StreamId} not found"); + } } diff --git a/src/ProjectOrigin.Chronicler.Server/Repositories/IChroniclerRepository.cs b/src/ProjectOrigin.Chronicler.Server/Repositories/IChroniclerRepository.cs index e8f3ee1..922f5e6 100644 --- a/src/ProjectOrigin.Chronicler.Server/Repositories/IChroniclerRepository.cs +++ b/src/ProjectOrigin.Chronicler.Server/Repositories/IChroniclerRepository.cs @@ -23,5 +23,6 @@ public interface IChroniclerRepository Task DeleteClaimAllocation(Guid id); Task InsertClaimRecord(ClaimRecord record); + Task WithdrawClaimRecord(FederatedCertificateId fid); } diff --git a/src/ProjectOrigin.Chronicler.Test/BlockReaderJobTests.cs b/src/ProjectOrigin.Chronicler.Test/BlockReaderJobTests.cs index 09c131d..a29c06a 100644 --- a/src/ProjectOrigin.Chronicler.Test/BlockReaderJobTests.cs +++ b/src/ProjectOrigin.Chronicler.Test/BlockReaderJobTests.cs @@ -268,6 +268,27 @@ public async Task Verify_Claimed_NotFound_NotInserted() _repository.VerifyNoOtherCalls(); } + [Fact] + public async Task ProcessWithdrawnEvent_WithdrawsClaim() + { + // Arrange + var fixture = new Fixture(); + var certificateId = new FederatedCertificateId { RegistryName = RegistryName, StreamId = fixture.Create() }; + var block = new Registry.V1.Block + { + Height = 1, + }; + block.AddWithdrawn(certificateId); + _registryService.Setup(x => x.GetNextBlock(RegistryName, 0)).ReturnsAsync(block); + + // Act + await _job.ProcessRegistryBlocks(RegistryName, 0, default); + + // Assert + _repository.Verify(x => x.UpsertReadBlock(It.Is(x => x.BlockHeight == 1)), Times.Once); + _repository.Verify(x => x.WithdrawClaimRecord(certificateId), Times.Once); + _repository.VerifyNoOtherCalls(); + } [Fact] public async Task Verify_Claimed_Found_Inserted() @@ -454,5 +475,18 @@ public static void AddClaim(this Registry.V1.Block block, Guid allocationId, Fed }.ToByteString() }); } + + public static void AddWithdrawn(this Registry.V1.Block block, FederatedCertificateId id) + { + block.Transactions.Add(new Registry.V1.Transaction + { + Header = new Registry.V1.TransactionHeader + { + PayloadType = Electricity.V1.WithdrawnEvent.Descriptor.FullName, + FederatedStreamId = id.ToProto(), + }, + Payload = new Electricity.V1.WithdrawnEvent().ToByteString() + }); + } } diff --git a/src/ProjectOrigin.Chronicler.Test/ChroniclerRepositoryTests.cs b/src/ProjectOrigin.Chronicler.Test/ChroniclerRepositoryTests.cs index caeaec1..1cedd45 100644 --- a/src/ProjectOrigin.Chronicler.Test/ChroniclerRepositoryTests.cs +++ b/src/ProjectOrigin.Chronicler.Test/ChroniclerRepositoryTests.cs @@ -376,4 +376,46 @@ public async Task InsertClaimRecord_InsertsClaimRecord() // Assert _con.QuerySingle("SELECT * FROM claim_records").Should().BeEquivalentTo(record); } + + [Fact] + public async Task WithdrawClaimRecord_WithdrawsClaimRecord() + { + var record1 = new ClaimRecord() + { + RegistryName = _fixture.Create(), + State = ClaimRecordState.Claimed, + CertificateId = Guid.NewGuid(), + Id = Guid.NewGuid(), + Quantity = 123, + RandomR = _fixture.Create() + }; + var record2 = new ClaimRecord() + { + RegistryName = _fixture.Create(), + State = ClaimRecordState.Claimed, + CertificateId = Guid.NewGuid(), + Id = Guid.NewGuid(), + Quantity = 124, + RandomR = _fixture.Create() + }; + await _repository.InsertClaimRecord(record1); + await _repository.InsertClaimRecord(record2); + + await _repository.WithdrawClaimRecord(new FederatedCertificateId + { + RegistryName = record1.RegistryName, + StreamId = record1.CertificateId + }); + + var withdrawnRecord = await _con.QuerySingleAsync(@"SELECT * FROM claim_records + WHERE registry_name = @registryName + AND certificate_id = @certificateId", + new + { + registryName = record1.RegistryName, + certificateId = record1.CertificateId + }); + + withdrawnRecord.State.Should().Be(ClaimRecordState.Withdrawn); + } } diff --git a/src/Protos/electricity.proto b/src/Protos/electricity.proto index c276554..e102336 100644 --- a/src/Protos/electricity.proto +++ b/src/Protos/electricity.proto @@ -48,6 +48,14 @@ message ClaimedEvent { project_origin.common.v1.Uuid allocation_id = 2; } +message WithdrawnEvent { + +} + +message UnclaimedEvent { + project_origin.common.v1.Uuid allocation_id = 1; +} + enum GranularCertificateType { INVALID = 0; CONSUMPTION = 1;