diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f12eb76c..4f34d5798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,12 +21,16 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x - + - name: Restore dependencies run: dotnet restore ./backend/CcScan.Backend.sln - + - name: Build run: dotnet build ./backend/CcScan.Backend.sln -c Release --no-restore - name: Test - run: dotnet test ./backend/CcScan.Backend.sln --filter Category!=IntegrationTests -c Release --no-build --verbosity normal + run: | + # Tests depend on docker-compose being available due to this issue https://github.com/mariotoffia/FluentDocker/issues/312. + # The soft linking should be remove when a fix is released. + ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose + dotnet test ./backend/CcScan.Backend.sln --filter Category!=IntegrationTests -c Release --no-build --verbosity normal diff --git a/.gitmodules b/.gitmodules index e69de29bb..47adfa036 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "backend/concordium-net-sdk"] + path = backend/concordium-net-sdk + url = ../concordium-net-sdk.git diff --git a/backend/Application/Api/GraphQL/Import/AccountWriter.cs b/backend/Application/Api/GraphQL/Import/AccountWriter.cs index 40347bb8d..8b64c3449 100644 --- a/backend/Application/Api/GraphQL/Import/AccountWriter.cs +++ b/backend/Application/Api/GraphQL/Import/AccountWriter.cs @@ -8,6 +8,7 @@ using Dapper; using Microsoft.EntityFrameworkCore; using Npgsql; +using Concordium.Sdk.Types; namespace Application.Api.GraphQL.Import; @@ -157,20 +158,23 @@ private static IEnumerable IterateBatchDbDataReader(DbDataReader reader, F public async Task UpdateAccount(TSource item, Func delegatorIdSelector, Action updateAction) { using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateAccount)); - + await using var context = await _dbContextFactory.CreateDbContextAsync(); var delegatorId = (long)delegatorIdSelector(item); var account = await context.Accounts.SingleAsync(x => x.Id == delegatorId); updateAction(item, account); - + await context.SaveChangesAsync(); } - + + /// + /// Update using on each account with a pending change for delegation that is effective before . + /// public async Task UpdateAccountsWithPendingDelegationChange(DateTimeOffset effectiveTimeEqualOrBefore, Action updateAction) { using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateAccountsWithPendingDelegationChange)); - + await using var context = await _dbContextFactory.CreateDbContextAsync(); var sql = $"select * from graphql_accounts where delegation_pending_change->'data'->>'EffectiveTime' <= '{effectiveTimeEqualOrBefore:O}'"; @@ -202,7 +206,31 @@ public async Task UpdateAccounts(Expression> whereClause, Ac await context.SaveChangesAsync(); } } - + + /// + /// Remove baker and move its delegators to the passive pool. + /// Returns the number of delegators which were moved. + /// + public async Task RemoveBaker(BakerId bakerId, DateTimeOffset effectiveTime) { + using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(RemoveBaker)); + await using var context = await _dbContextFactory.CreateDbContextAsync(); + + var baker = await context.Bakers.SingleAsync(x => x.Id == (long)bakerId.Id.Index); + baker.State = new Bakers.RemovedBakerState(effectiveTime); + + var target = new BakerDelegationTarget((long) bakerId.Id.Index); + var delegatorAccounts = await context.Accounts + .Where(account => account.Delegation != null && account.Delegation.DelegationTarget == target) + .ToArrayAsync(); + foreach (var delegatorAccount in delegatorAccounts) { + var delegation = delegatorAccount.Delegation ?? throw new InvalidOperationException("Account delegating to baker target being removed has no delegation attached."); + delegation.DelegationTarget = new PassiveDelegationTarget(); + } + + await context.SaveChangesAsync(); + return delegatorAccounts.Length; + } + public async Task UpdateDelegationStakeIfRestakingEarnings(AccountRewardSummary[] stakeUpdates) { using var counter = _metrics.MeasureDuration(nameof(AccountWriter), nameof(UpdateDelegationStakeIfRestakingEarnings)); diff --git a/backend/Application/Api/GraphQL/Import/BakerChangeStrategy.cs b/backend/Application/Api/GraphQL/Import/BakerChangeStrategy.cs index cc7ae6740..966d60b78 100644 --- a/backend/Application/Api/GraphQL/Import/BakerChangeStrategy.cs +++ b/backend/Application/Api/GraphQL/Import/BakerChangeStrategy.cs @@ -9,24 +9,27 @@ internal interface IBakerChangeStrategy Task UpdateBakersFromTransactionEvents( IEnumerable transactionEvents, ImportState importState, - BakerImportHandler.BakerUpdateResultsBuilder resultBuilder); - + BakerImportHandler.BakerUpdateResultsBuilder resultBuilder, + DateTimeOffset blockSlotTime); + bool MustApplyPendingChangesDue(DateTimeOffset? nextPendingBakerChangeTime); DateTimeOffset GetEffectiveTime(); + /// Whether the protocol supports pending changes. Starting from Protocol version 7 this is not the case. + bool SupportsPendingChanges(); } internal static class BakerChangeStrategyFactory { internal static IBakerChangeStrategy Create( BlockInfo blockInfo, - ChainParameters chainParameters, + ChainParameters chainParameters, BlockImportPaydayStatus importPaydayStatus, BakerWriter writer, AccountInfo[] bakersWithNewPendingChanges) { if (blockInfo.ProtocolVersion.AsInt() < 4) return new PreProtocol4Strategy(bakersWithNewPendingChanges, blockInfo, writer); - + ChainParameters.TryGetPoolOwnerCooldown(chainParameters, out var poolOwnerCooldown); return new PostProtocol4Strategy(blockInfo, poolOwnerCooldown!.Value, importPaydayStatus, writer); @@ -46,13 +49,17 @@ public PreProtocol4Strategy(AccountInfo[] accountInfos, BlockInfo blockInfo, Bak _accountInfos = accountInfos; } + public bool SupportsPendingChanges() => true; + /// /// Prior to protocol 4 isn't used. /// public async Task UpdateBakersFromTransactionEvents( IEnumerable transactionEvents, ImportState importState, - BakerImportHandler.BakerUpdateResultsBuilder resultBuilder) + BakerImportHandler.BakerUpdateResultsBuilder resultBuilder, + DateTimeOffset blockSlotTime + ) { foreach (var txEvent in transactionEvents) { @@ -184,6 +191,8 @@ public bool MustApplyPendingChangesDue(DateTimeOffset? nextPendingBakerChangeTim return false; } + public bool SupportsPendingChanges() => _blockInfo.ProtocolVersion < ProtocolVersion.P7; + public DateTimeOffset GetEffectiveTime() { if (_importPaydayStatus is FirstBlockAfterPayday firstBlockAfterPayday) @@ -195,8 +204,9 @@ public DateTimeOffset GetEffectiveTime() /// are used from protocol 4. /// public async Task UpdateBakersFromTransactionEvents(IEnumerable transactionEvents, ImportState importState, - BakerImportHandler.BakerUpdateResultsBuilder resultBuilder) + BakerImportHandler.BakerUpdateResultsBuilder resultBuilder, DateTimeOffset blockSlotTime) { + bool supportsPendingChanges = SupportsPendingChanges(); foreach (var txEvent in transactionEvents) { switch (txEvent.Effects) @@ -219,8 +229,21 @@ await _writer.AddOrUpdateBaker(bakerAdded, resultBuilder.IncrementBakersAdded(); break; case BakerRemovedEvent bakerRemovedEvent: - var pendingChange = await SetPendingChangeOnBaker(bakerRemovedEvent.BakerId, bakerRemovedEvent); - importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChange.EffectiveTime); + if (supportsPendingChanges) { + // If the protocol version (prior to 7) supports pending changes, then store a pending change. + var pendingChange = await SetPendingChangeOnBaker(bakerRemovedEvent.BakerId, bakerRemovedEvent); + importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChange.EffectiveTime); + } else { + // Otherwise update the stake immediately. + await _writer.UpdateBaker(bakerRemovedEvent, + src => src.BakerId.Id.Index, + (src, dst) => + { + var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot remove a baker that is not active!"); + dst.State = new RemovedBakerState(blockSlotTime); + resultBuilder.AddBakerRemoved((long) bakerRemovedEvent.BakerId.Id.Index); + }); + } break; case BakerRestakeEarningsUpdatedEvent bakerRestakeEarningsUpdatedEvent: await _writer.UpdateBaker(bakerRestakeEarningsUpdatedEvent, @@ -280,18 +303,36 @@ await _writer.UpdateBaker(bakerSetOpenStatusEvent, resultBuilder.AddBakerClosedForAll((long)bakerSetOpenStatusEvent.BakerId.Id.Index); break; case BakerStakeDecreasedEvent bakerStakeDecreasedEvent: - var pendingChangeStakeDecreased = await SetPendingChangeOnBaker(bakerStakeDecreasedEvent.BakerId, bakerStakeDecreasedEvent); - importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChangeStakeDecreased.EffectiveTime); + if (supportsPendingChanges) { + // If the protocol version (prior to 7) supports pending changes, then store a pending change. + var pendingChangeStakeDecreased = await SetPendingChangeOnBaker(bakerStakeDecreasedEvent.BakerId, bakerStakeDecreasedEvent); + importState.UpdateNextPendingBakerChangeTimeIfLower(pendingChangeStakeDecreased.EffectiveTime); + } else { + // From protocol version 7 and onwards stake changes are immediate. + await _writer.UpdateBaker(bakerStakeDecreasedEvent, + src => src.BakerId.Id.Index, + (src, dst) => + { + var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot decrease stake for a baker that is not active!"); + activeState.StakedAmount = src.NewStake.Value; + }); + } break; case BakerStakeIncreasedEvent bakerStakeIncreasedEvent: await _writer.UpdateBaker(bakerStakeIncreasedEvent, src => src.BakerId.Id.Index, (src, dst) => { - var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot set restake earnings for a baker that is not active!"); + var activeState = dst.State as ActiveBakerState ?? throw new InvalidOperationException("Cannot increase stake for a baker that is not active!"); activeState.StakedAmount = src.NewStake.Value; }); break; + case BakerEventDelegationRemoved delegationRemoved: + // This event was introduced as part of Concordium Protocol Version 7, + // which also removes the logic around pending changes, meaning we can + // just update the state immediately. + await _writer.RemoveDelegator(delegationRemoved.DelegatorId); + break; case BakerKeysUpdatedEvent: default: break; diff --git a/backend/Application/Api/GraphQL/Import/BakerImportHandler.cs b/backend/Application/Api/GraphQL/Import/BakerImportHandler.cs index d6411ba92..674d9ac06 100644 --- a/backend/Application/Api/GraphQL/Import/BakerImportHandler.cs +++ b/backend/Application/Api/GraphQL/Import/BakerImportHandler.cs @@ -25,11 +25,14 @@ public BakerImportHandler(IDbContextFactory dbContextFactory, _logger = Log.ForContext(GetType()); } + /// + /// Process block data related to changes for bakers/validators. + /// public async Task HandleBakerUpdates(BlockDataPayload payload, RewardsSummary rewardsSummary, ChainParametersState chainParameters, BlockImportPaydayStatus importPaydayStatus, ImportState importState) { using var counter = _metrics.MeasureDuration(nameof(BakerImportHandler), nameof(HandleBakerUpdates)); - + var changeStrategy = BakerChangeStrategyFactory.Create(payload.BlockInfo, chainParameters.Current, importPaydayStatus, _writer, payload.AccountInfos.BakersWithNewPendingChanges);; @@ -164,8 +167,8 @@ or BakerRestakeEarningsUpdated or BakerConfigured or BakerKeysUpdated ); - - await bakerChangeStrategy.UpdateBakersFromTransactionEvents(txEvents, importState, resultBuilder); + + await bakerChangeStrategy.UpdateBakersFromTransactionEvents(txEvents, importState, resultBuilder, payload.BlockInfo.BlockSlotTime); // This should happen after the bakers from current block has been added to the database if (isFirstBlockAfterPayday) @@ -224,10 +227,10 @@ await _writer.UpdateBakers( FinalizationCommission = source.PoolInfo.CommissionRates.FinalizationCommission.AsDecimal(), BakingCommission = source.PoolInfo.CommissionRates.BakingCommission.AsDecimal() }, - DelegatedStake = source.DelegatedCapital.Value, + DelegatedStake = source.DelegatedCapital!.Value.Value, DelegatorCount = 0, - DelegatedStakeCap = source.DelegatedCapitalCap.Value, - TotalStake = source.BakerEquityCapital.Value + source.DelegatedCapital.Value + DelegatedStakeCap = source.DelegatedCapitalCap!.Value.Value, + TotalStake = source.BakerEquityCapital!.Value.Value + source.DelegatedCapital.Value.Value }; pool.ApplyPaydayStatus(source.CurrentPaydayStatus, source.PoolInfo.CommissionRates); @@ -262,8 +265,8 @@ await _writer.UpdateBakers(baker => { var rates = baker.ActiveState!.Pool!.CommissionRates; rates.FinalizationCommission = AdjustValueToRange(rates.FinalizationCommission, currentFinalizationCommissionRange); - rates.BakingCommission = AdjustValueToRange(rates.BakingCommission, currentBakingCommissionRange); - rates.TransactionCommission = AdjustValueToRange(rates.TransactionCommission, currentTransactionCommissionRange); + rates.BakingCommission = AdjustValueToRange(rates.BakingCommission, currentBakingCommissionRange!); + rates.TransactionCommission = AdjustValueToRange(rates.TransactionCommission, currentTransactionCommissionRange!); }, baker => baker.ActiveState!.Pool != null); @@ -309,16 +312,22 @@ await _writer.UpdateBaker(1900UL, bakerId => bakerId, (bakerId, baker) => } } - private async Task UpdateBakersWithPendingChangesDue(IBakerChangeStrategy bakerChangeStrategy, + private async Task UpdateBakersWithPendingChangesDue(IBakerChangeStrategy bakerChangeStrategy, ImportState importState, BakerUpdateResultsBuilder resultBuilder) { - if (bakerChangeStrategy.MustApplyPendingChangesDue(importState.NextPendingBakerChangeTime)) - { - var effectiveTime = bakerChangeStrategy.GetEffectiveTime(); - await _writer.UpdateBakersWithPendingChange(effectiveTime, baker => ApplyPendingChange(baker, resultBuilder)); + // Check if this protocol supports pending changes. + if (bakerChangeStrategy.SupportsPendingChanges()) { + if (bakerChangeStrategy.MustApplyPendingChangesDue(importState.NextPendingBakerChangeTime)) + { + var effectiveTime = bakerChangeStrategy.GetEffectiveTime(); + await _writer.UpdateBakersWithPendingChange(effectiveTime, baker => ApplyPendingChange(baker, resultBuilder)); - importState.NextPendingBakerChangeTime = await _writer.GetMinPendingChangeTime(); - _logger.Information("NextPendingBakerChangeTime set to {value}", importState.NextPendingBakerChangeTime); + importState.NextPendingBakerChangeTime = await _writer.GetMinPendingChangeTime(); + _logger.Information("NextPendingBakerChangeTime set to {value}", importState.NextPendingBakerChangeTime); + } + } else { + // Starting from protocol version 7 and onwards stake changes are immediate, so we apply all of them in the first block of P7 and this is a no-op for future blocks. + await _writer.UpdateBakersWithPendingChange(DateTimeOffset.MaxValue, baker => ApplyPendingChange(baker, resultBuilder)); } } diff --git a/backend/Application/Api/GraphQL/Import/BakerWriter.cs b/backend/Application/Api/GraphQL/Import/BakerWriter.cs index 3f34bdd16..52a12b545 100644 --- a/backend/Application/Api/GraphQL/Import/BakerWriter.cs +++ b/backend/Application/Api/GraphQL/Import/BakerWriter.cs @@ -164,6 +164,33 @@ public async Task AddBakerTransactionRelations(IEnumerable + /// Removes delegator information tracked for an account. + /// Throws for accounts with no delegation information. + /// + public async Task RemoveDelegator(DelegatorId delegatorId) { + using var counter = _metrics.MeasureDuration(nameof(BakerWriter), nameof(RemoveDelegator)); + await using var context = await _dbContextFactory.CreateDbContextAsync(); + var account = await context.Accounts.SingleAsync(x => x.Id == (long) delegatorId.Id.Index); + if (account.Delegation == null) throw new InvalidOperationException("Trying to remove delegator, but account is not delegating."); + // Update the delegation counter on the target. + switch (account.Delegation.DelegationTarget) { + case PassiveDelegationTarget passiveTarget: + var passive = await context.PassiveDelegations.SingleAsync(); + passive.DelegatorCount -= 1; + break; + case BakerDelegationTarget target: + var baker = await context.Bakers.SingleAsync(baker => baker.BakerId == target.BakerId); + var activeState = baker.State as ActiveBakerState ?? throw new InvalidOperationException("Trying to remove delegator targeting a baker pool, but the baker state is not active."); + var pool = activeState.Pool ?? throw new InvalidOperationException("Trying to remove delegator targeting a baker pool, but the baker state had no pool information."); + pool.DelegatorCount -= 1; + break; + }; + // Delete the delegation information + account.Delegation = null; + await context.SaveChangesAsync(); + } + public async Task UpdateDelegatedStake() { using var counter = _metrics.MeasureDuration(nameof(BakerWriter), nameof(UpdateDelegatedStake)); diff --git a/backend/Application/Api/GraphQL/Import/DelegationImportHandler.cs b/backend/Application/Api/GraphQL/Import/DelegationImportHandler.cs index d61a409c0..85aa6d9fa 100644 --- a/backend/Application/Api/GraphQL/Import/DelegationImportHandler.cs +++ b/backend/Application/Api/GraphQL/Import/DelegationImportHandler.cs @@ -16,45 +16,68 @@ public DelegationImportHandler(AccountWriter writer) _logger = Log.ForContext(); } + /// + /// Process delegation related information from a block. + /// public async Task HandleDelegationUpdates(BlockDataPayload payload, - ChainParameters chainParameters, BakerUpdateResults bakerUpdateResults, RewardsSummary rewardsSummary, + ChainParameters chainParameters, BakerUpdateResults bakerUpdateResults, RewardsSummary rewardsSummary, BlockImportPaydayStatus importPaydayStatus) { var resultBuilder = new DelegationUpdateResultsBuilder(); - if (payload.BlockInfo.ProtocolVersion.AsInt() < 4) return resultBuilder.Build(); + // Delegation was introduced as part of Concordium Protocol Version 4, + // meaning we can just return for blocks from a protocol version prior to that. + if (payload.BlockInfo.ProtocolVersion < ProtocolVersion.P4) return resultBuilder.Build(); - if (!ChainParameters.TryGetDelegatorCooldown(chainParameters, out var delegatorCooldown)) - { - throw new InvalidOperationException("Delegator cooldown expected for protocol version 4 and above"); + // Handle effective pending changes for delegators and update the resultBuilder. + if (importPaydayStatus is FirstBlockAfterPayday firstBlockAfterPayday) { + if (payload.BlockInfo.ProtocolVersion < ProtocolVersion.P7) { + // Stake changes only take effect from the first block in each payday. + await _writer.UpdateAccountsWithPendingDelegationChange(firstBlockAfterPayday.PaydayTimestamp, + account => ApplyPendingChange(account, resultBuilder)); + } else if (payload.BlockInfo.ProtocolVersion == ProtocolVersion.P7) { + // Starting from Concordium Protocol Version 7 stake changes are immediate, + // meaning no delegators are expected to have pending changes from this point. + // Only the first reward day in P7 should this do anything, afterwards it is a + // no-op, since no accounts with pending changes are expected. + await _writer.UpdateAccountsWithPendingDelegationChange(DateTimeOffset.MaxValue, + account => ApplyPendingChange(account, resultBuilder)); + } } - if (importPaydayStatus is FirstBlockAfterPayday firstBlockAfterPayday) - await _writer.UpdateAccountsWithPendingDelegationChange(firstBlockAfterPayday.PaydayTimestamp, - account => ApplyPendingChange(account, resultBuilder)); - + // Handle delegation state changes due to pools that are either removed or closed for delegation. await HandleBakersRemovedOrClosedForAll(bakerUpdateResults, resultBuilder); - var txEvents = payload.BlockItemSummaries.Where(b => b.IsSuccess()) + var delegationConfigureEvents = payload.BlockItemSummaries.Where(b => b.IsSuccess()) .Select(b => b.Details as AccountTransactionDetails) .Where(atd => atd is not null) .Select(atd => atd!.Effects as DelegationConfigured) .Where(d => d is not null) .Select(d => d!); - await UpdateDelegationFromTransactionEvents(txEvents, payload.BlockInfo, delegatorCooldown!.Value, resultBuilder); + // Get the current cooldown parameter for delegation. + if (!ChainParameters.TryGetDelegatorCooldown(chainParameters, out var delegatorCooldownOut)) + { + throw new InvalidOperationException("Delegator cooldown expected for protocol version 4 and above"); + } + var delegatorCooldown = delegatorCooldownOut!.Value; // Safe since we throw for the failing case above. + + await UpdateDelegationFromTransactionEvents(delegationConfigureEvents, payload.BlockInfo, delegatorCooldown, resultBuilder); await _writer.UpdateDelegationStakeIfRestakingEarnings(rewardsSummary.AggregatedAccountRewards); - + resultBuilder.SetTotalAmountStaked(await _writer.GetTotalDelegationAmountStaked()); return resultBuilder.Build(); } + /// + /// Iterate removed and closed pools, moves the delegators to target the passive pool and updates the account delegation information and updates . + /// private async Task HandleBakersRemovedOrClosedForAll(BakerUpdateResults bakerUpdateResults, DelegationUpdateResultsBuilder resultBuilder) { var bakerIds = bakerUpdateResults.BakerIdsRemoved .Concat(bakerUpdateResults.BakerIdsClosedForAll); - + foreach (var bakerId in bakerIds) { var target = new BakerDelegationTarget(bakerId); @@ -68,6 +91,11 @@ await _writer.UpdateAccounts(account => account.Delegation != null && account.De } } + /// + /// Update/Apply delegation state on the with the pending change. + /// Records the stake change in . + /// Throws if is not delegating. + /// private void ApplyPendingChange(Account account, DelegationUpdateResultsBuilder resultsBuilder) { var delegation = account.Delegation ?? throw new InvalidOperationException("Apply pending delegation change to an account that has no delegation!"); @@ -108,8 +136,13 @@ await _writer.UpdateAccount(delegationRemoved, (_, dst) => { if (dst.Delegation == null) throw new InvalidOperationException("Trying to set pending change to remove delegation on an account without a delegation instance!"); - var effectiveTime = blockInfo.BlockSlotTime.AddSeconds(delegatorCooldown); - dst.Delegation.PendingChange = new PendingDelegationRemoval(effectiveTime); + if (blockInfo.ProtocolVersion < ProtocolVersion.P7) { + var effectiveTime = blockInfo.BlockSlotTime.AddSeconds(delegatorCooldown); + dst.Delegation.PendingChange = new PendingDelegationRemoval(effectiveTime); + } else { + resultBuilder.DelegationTargetRemoved(dst.Delegation.DelegationTarget); + dst.Delegation = null; + } }); break; case DelegationSetDelegationTarget delegationSetDelegationTarget: @@ -139,8 +172,12 @@ await _writer.UpdateAccount(delegationStakeDecreased, (src, dst) => { if (dst.Delegation == null) throw new InvalidOperationException("Trying to set pending change to remove delegation on an account without a delegation instance!"); - var effectiveTime = blockInfo.BlockSlotTime.AddSeconds(delegatorCooldown); - dst.Delegation.PendingChange = new PendingDelegationReduceStake(effectiveTime, delegationStakeDecreased.NewStake.Value); + if (blockInfo.ProtocolVersion < ProtocolVersion.P7) { + var effectiveTime = blockInfo.BlockSlotTime.AddSeconds(delegatorCooldown); + dst.Delegation.PendingChange = new PendingDelegationReduceStake(effectiveTime, delegationStakeDecreased.NewStake.Value); + } else { + dst.Delegation.StakedAmount = src.NewStake.Value; + } }); break; case DelegationStakeIncreased delegationStakeIncreased: @@ -152,12 +189,30 @@ await _writer.UpdateAccount(delegationStakeIncreased, dst.Delegation.StakedAmount = src.NewStake.Value; }); break; + case DelegationEventBakerRemoved bakerRemoved: + // We can update the database immediately and without tracking a pending change + // because this event was introduced as part of Protocol Version 7, where + // pending changes are also removed. + var delegationsMoveToPassive = await _writer.RemoveBaker(bakerRemoved.BakerId, blockInfo.BlockSlotTime); + // Update the resultsBuilder with the moved delegations. + var bakerTarget = new BakerDelegationTarget((long)bakerRemoved.BakerId.Id.Index); + var passiveTarget = new PassiveDelegationTarget(); + for (int i = 0; i < delegationsMoveToPassive; i++) + { + resultBuilder.DelegationTargetRemoved(bakerTarget); + resultBuilder.DelegationTargetAdded(passiveTarget); + } + break; } - + } } } + /// + /// Tracker of stake changes due to removed or reduced stake by delegators. + /// Later this will be used to update the stake of pools. + /// public class DelegationUpdateResultsBuilder { private readonly List _delegationTargetsRemoved = new (); @@ -183,7 +238,7 @@ public DelegationUpdateResults Build() }) .Where(x => x.DelegatorCountDelta != 0) .ToArray(); - + return new DelegationUpdateResults(_totalAmountStaked, all); } diff --git a/backend/Application/Api/GraphQL/Import/ImportWriteController.cs b/backend/Application/Api/GraphQL/Import/ImportWriteController.cs index d50edeaed..aff8aabea 100644 --- a/backend/Application/Api/GraphQL/Import/ImportWriteController.cs +++ b/backend/Application/Api/GraphQL/Import/ImportWriteController.cs @@ -125,7 +125,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) result.Block.BlockHeight, result.Block.BlockSlotTime.ToUniversalTime().ToString()); - await _accountBalanceValidator.PerformValidations(result.Block); + await _accountBalanceValidator.PerformValidations(result.Block, envelope.Payload.BlockInfo.ProtocolVersion); if (result.Block.BlockHeight % 5000 == 0) _metricsListener.DumpCapturedMetrics(); diff --git a/backend/Application/Api/GraphQL/Import/MetricsWriter.cs b/backend/Application/Api/GraphQL/Import/MetricsWriter.cs index 2095ecea8..8a11cfef6 100644 --- a/backend/Application/Api/GraphQL/Import/MetricsWriter.cs +++ b/backend/Application/Api/GraphQL/Import/MetricsWriter.cs @@ -262,7 +262,9 @@ insert into metrics_payday_pool_rewards (time, pool_id, transaction_fees_total_a var stakeSnapshot = poolReward.Pool switch { - BakerPoolRewardTarget baker => paydayPoolStakeSnapshot.Items.Single(x => x.BakerId == baker.BakerId), + BakerPoolRewardTarget baker => + // Find the active baker stake, otherwise the baker was removed and empty stake is used. + paydayPoolStakeSnapshot.Items.SingleOrDefault(x => x.BakerId == baker.BakerId, PaydayPoolStakeSnapshotItem.Removed(baker.BakerId)), PassiveDelegationPoolRewardTarget => new PaydayPoolStakeSnapshotItem(-1, 0, paydayPassiveDelegationStakeSnapshot.DelegatedStake), _ => throw new NotImplementedException() }; diff --git a/backend/Application/Api/GraphQL/Import/PaydayPoolStakeSnapshot.cs b/backend/Application/Api/GraphQL/Import/PaydayPoolStakeSnapshot.cs index 444198f01..9759702f8 100644 --- a/backend/Application/Api/GraphQL/Import/PaydayPoolStakeSnapshot.cs +++ b/backend/Application/Api/GraphQL/Import/PaydayPoolStakeSnapshot.cs @@ -6,4 +6,8 @@ public record PaydayPoolStakeSnapshot( public record PaydayPoolStakeSnapshotItem( long BakerId, long BakerStake, - long DelegatedStake); + long DelegatedStake) { + public static PaydayPoolStakeSnapshotItem Removed(long bakerId) { + return new(bakerId, 0, 0); + } + }; diff --git a/backend/Application/Api/GraphQL/Import/Validations/AccountValidator.cs b/backend/Application/Api/GraphQL/Import/Validations/AccountValidator.cs index ac7906b32..f62990ff7 100644 --- a/backend/Application/Api/GraphQL/Import/Validations/AccountValidator.cs +++ b/backend/Application/Api/GraphQL/Import/Validations/AccountValidator.cs @@ -24,17 +24,17 @@ public AccountValidator(ConcordiumClient nodeClient, IDbContextFactory(); } - public async Task Validate(Block block) + public async Task Validate(Block block, ProtocolVersion protocolVersion) { - await InternalValidate(block); + await InternalValidate(block, protocolVersion); } - public async Task ValidateSingle(Block block, SingleAccountValidationInfo singleAccountValidationInfo) + public async Task ValidateSingle(Block block, ProtocolVersion protocolVersion, SingleAccountValidationInfo singleAccountValidationInfo) { - await InternalValidate(block, singleAccountValidationInfo); + await InternalValidate(block, protocolVersion, singleAccountValidationInfo); } - private async Task InternalValidate(Block block, SingleAccountValidationInfo? singleAccountValidationInfo = null) + private async Task InternalValidate(Block block, ProtocolVersion protocolVersion, SingleAccountValidationInfo? singleAccountValidationInfo = null) { var blockHash = BlockHash.From(block.BlockHash); var blockHeight = (ulong)block.BlockHeight; @@ -67,7 +67,7 @@ private async Task InternalValidate(Block block, SingleAccountValidationInfo? si await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await ValidateAccounts(nodeAccountInfos, blockHeight, dbContext, singleAccountValidationInfo); - await ValidateBakers(nodeAccountBakers, block, dbContext, singleAccountValidationInfo); + await ValidateBakers(protocolVersion, nodeAccountBakers, block, dbContext, singleAccountValidationInfo); } private bool TryGetAccountDelegation(IAccountStakingInfo? info, out AccountDelegation? delegation) @@ -193,16 +193,17 @@ private static string Format(PendingDelegationChange pendingChange) }; } - private async Task ValidateBakers(List nodeAccountBakers, Block block, GraphQlDbContext dbContext, SingleAccountValidationInfo? singleAccountAddress) + private async Task ValidateBakers(ProtocolVersion protocolVersion, List nodeAccountBakers, Block block, GraphQlDbContext dbContext, SingleAccountValidationInfo? singleAccountAddress) { + _logger.Information($"Validating bakers in {block.BlockHash}: ${nodeAccountBakers.Select(a=> a.BakerInfo.BakerId).ToString()}"); + var blockHeight = (ulong)block.BlockHeight; var blockHash = BlockHash.From(block.BlockHash); var given = new Given(blockHash); var poolStatuses = new List(); - var nodeInfo = await _nodeClient.GetNodeInfoAsync(); - if (nodeInfo.Version.Major >= 4) + if (protocolVersion >= ProtocolVersion.P4) { foreach (var chunk in Chunk(nodeAccountBakers, 10)) { @@ -235,8 +236,8 @@ private async Task ValidateBakers(List nodeAccountBakers, Block bl TransactionCommission = x.BakerPoolInfo.CommissionRates.TransactionCommission.AsDecimal(), FinalizationCommission = x.BakerPoolInfo.CommissionRates.FinalizationCommission.AsDecimal(), BakingCommission = x.BakerPoolInfo.CommissionRates.BakingCommission.AsDecimal(), - DelegatedStake = bakerPoolStatus.DelegatedCapital.Value, - DelegatedStakeCap = bakerPoolStatus.DelegatedCapitalCap.Value, + DelegatedStake = bakerPoolStatus!.DelegatedCapital!.Value.Value, + DelegatedStakeCap = bakerPoolStatus.DelegatedCapitalCap!.Value.Value, PaydayStatus = bakerPoolStatus?.CurrentPaydayStatus == null ? null : new { BakerStake = bakerPoolStatus.CurrentPaydayStatus.BakerEquityCapital.Value, @@ -375,4 +376,4 @@ private IEnumerable> Chunk(IReadOnlyCollection list, int ba } } -public record SingleAccountValidationInfo(string CanonicalAccountAddress, long AccountId); \ No newline at end of file +public record SingleAccountValidationInfo(string CanonicalAccountAddress, long AccountId); diff --git a/backend/Application/Api/GraphQL/Import/Validations/BalanceStatisticsValidator.cs b/backend/Application/Api/GraphQL/Import/Validations/BalanceStatisticsValidator.cs index cb4bb7a0f..06f176052 100644 --- a/backend/Application/Api/GraphQL/Import/Validations/BalanceStatisticsValidator.cs +++ b/backend/Application/Api/GraphQL/Import/Validations/BalanceStatisticsValidator.cs @@ -20,7 +20,7 @@ public BalanceStatisticsValidator(ConcordiumClient grpcNodeClient, IDbContextFac _logger = Log.ForContext(); } - public async Task Validate(Block block) + public async Task Validate(Block block, ProtocolVersion _protocolVersion) { var nodeData = await _grpcNodeClient.GetTokenomicsInfoAsync(new Given(BlockHash.From(block.BlockHash))); if (nodeData.Response is RewardOverviewV1 rv1) @@ -44,4 +44,4 @@ public async Task Validate(Block block) } } } -} \ No newline at end of file +} diff --git a/backend/Application/Api/GraphQL/Import/Validations/IImportValidator.cs b/backend/Application/Api/GraphQL/Import/Validations/IImportValidator.cs index 7770bf08d..1ea0e46b2 100644 --- a/backend/Application/Api/GraphQL/Import/Validations/IImportValidator.cs +++ b/backend/Application/Api/GraphQL/Import/Validations/IImportValidator.cs @@ -1,9 +1,10 @@ using System.Threading.Tasks; using Application.Api.GraphQL.Blocks; +using Concordium.Sdk.Types; namespace Application.Api.GraphQL.Import.Validations; public interface IImportValidator { - Task Validate(Block block); -} \ No newline at end of file + Task Validate(Block block, ProtocolVersion protocolVersion); +} diff --git a/backend/Application/Api/GraphQL/Import/Validations/ImportValidationController.cs b/backend/Application/Api/GraphQL/Import/Validations/ImportValidationController.cs index fdd79642c..8aacae15c 100644 --- a/backend/Application/Api/GraphQL/Import/Validations/ImportValidationController.cs +++ b/backend/Application/Api/GraphQL/Import/Validations/ImportValidationController.cs @@ -5,6 +5,7 @@ using Concordium.Sdk.Client; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using Concordium.Sdk.Types; namespace Application.Api.GraphQL.Import.Validations; @@ -27,14 +28,14 @@ public ImportValidationController( }; } - public async Task PerformValidations(Block block) + public async Task PerformValidations(Block block, ProtocolVersion protocolVersion) { if (!_featureFlags.ConcordiumNodeImportValidationEnabled) return; if (block.BlockHeight % 10000 == 0) { foreach (var validator in _validators) - await validator.Validate(block); + await validator.Validate(block, protocolVersion); } } -} \ No newline at end of file +} diff --git a/backend/Application/Api/GraphQL/Import/Validations/PassiveDelegationValidator.cs b/backend/Application/Api/GraphQL/Import/Validations/PassiveDelegationValidator.cs index dd14bf29e..cc0446113 100644 --- a/backend/Application/Api/GraphQL/Import/Validations/PassiveDelegationValidator.cs +++ b/backend/Application/Api/GraphQL/Import/Validations/PassiveDelegationValidator.cs @@ -3,6 +3,7 @@ using Application.Api.GraphQL.EfCore; using Application.Api.GraphQL.PassiveDelegations; using Concordium.Sdk.Client; +using Concordium.Sdk.Types; using Dapper; using Microsoft.EntityFrameworkCore; @@ -21,10 +22,9 @@ public PassiveDelegationValidator(ConcordiumClient nodeClient, IDbContextFactory _logger = Log.ForContext(); } - public async Task Validate(Block block) + public async Task Validate(Block block, ProtocolVersion protocolVersion) { - var nodeInfo = await _nodeClient.GetNodeInfoAsync(); - if (nodeInfo.Version.Major >= 4) + if (protocolVersion >= ProtocolVersion.P4) { var target = await ReadPassiveDelegation(); @@ -76,4 +76,4 @@ private async Task ValidateDatabaseConsistent(PassiveDelegation? passiveDelegati await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.PassiveDelegations.SingleOrDefaultAsync(); } -} \ No newline at end of file +} diff --git a/backend/Application/Application.csproj b/backend/Application/Application.csproj index b575edf68..c15383d5c 100644 --- a/backend/Application/Application.csproj +++ b/backend/Application/Application.csproj @@ -4,7 +4,7 @@ net6.0 enable disable - 1.8.19 + 1.9.0 true true true @@ -12,7 +12,6 @@ - @@ -36,6 +35,7 @@ + diff --git a/backend/Application/Import/ImportChannel.cs b/backend/Application/Import/ImportChannel.cs index 17dd2ed63..2b07b031d 100644 --- a/backend/Application/Import/ImportChannel.cs +++ b/backend/Application/Import/ImportChannel.cs @@ -102,6 +102,9 @@ public GenesisBlockDataPayload( public IList GenesisIdentityProviders { get; } } +/// +/// List of results from GetAccountInfo for accounts that have changed in this block. +/// public record AccountInfosRetrieved( AccountInfo[] CreatedAccounts, AccountInfo[] BakersWithNewPendingChanges); diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md index 27d0174d0..780e85a44 100644 --- a/backend/CHANGELOG.md +++ b/backend/CHANGELOG.md @@ -1,5 +1,11 @@ ## Unreleased changes +## 1.9.0 +- Support Concordium Protocol Version 7. + - Transition between Delegation and Validating immediately. + - Stake changes are immediate. +- Fix bug in validation which was branching on the node software version instead of the protocol version. + ## 1.8.19 - Bugfix - Fix performance of account statement export, by adding an index on table `graphql_account_statement_entries`, and streaming the data. diff --git a/backend/CcScan.Backend.sln b/backend/CcScan.Backend.sln index e452e27de..3b788ae36 100644 --- a/backend/CcScan.Backend.sln +++ b/backend/CcScan.Backend.sln @@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseScripts", "DatabaseScripts\DatabaseScripts.csproj", "{F9322D66-8D55-466D-B7DF-04FBF2BD9341}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "concordium-net-sdk", "concordium-net-sdk", "{B970FDDA-8B7C-4DEE-9522-C51EB3D424B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Concordium.Sdk", "concordium-net-sdk\src\Concordium.Sdk.csproj", "{84A4662D-9D85-482F-84C3-FB37C2894053}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +28,12 @@ Global {F9322D66-8D55-466D-B7DF-04FBF2BD9341}.Debug|Any CPU.Build.0 = Debug|Any CPU {F9322D66-8D55-466D-B7DF-04FBF2BD9341}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9322D66-8D55-466D-B7DF-04FBF2BD9341}.Release|Any CPU.Build.0 = Release|Any CPU + {84A4662D-9D85-482F-84C3-FB37C2894053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84A4662D-9D85-482F-84C3-FB37C2894053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84A4662D-9D85-482F-84C3-FB37C2894053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84A4662D-9D85-482F-84C3-FB37C2894053}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {84A4662D-9D85-482F-84C3-FB37C2894053} = {B970FDDA-8B7C-4DEE-9522-C51EB3D424B4} EndGlobalSection EndGlobal diff --git a/backend/README.md b/backend/README.md index 8d5a55d9d..f322a4e48 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,7 +1,16 @@ -# Introduction +# Introduction This is the backend of Concordium Scan. # Getting Started + +This project depends on [concordium-net-sdk](https://github.com/Concordium/concordium-net-sdk) as a Git Submodule, which can be installed using: + +``` +git submodule update --init --recursive +``` + +Check out the readme of this dependency for how to build it. + ## Prerequisites The following prerequisites must be met in order to run the test suite and/or run the backend locally: diff --git a/backend/Tests/Api/GraphQL/Import/AccountImportHandlerTest.cs b/backend/Tests/Api/GraphQL/Import/AccountImportHandlerTest.cs index 08ad92757..009ea50a4 100644 --- a/backend/Tests/Api/GraphQL/Import/AccountImportHandlerTest.cs +++ b/backend/Tests/Api/GraphQL/Import/AccountImportHandlerTest.cs @@ -78,7 +78,10 @@ public async Task SameBlock_AccountCreation_InwardsTransferTest() AccountAmount: CcdAmount.FromCcd(1), AccountIndex: receiverAccountId, AccountAddress: receiverAccount, - null + null, + Schedule: ReleaseSchedule.Empty(), + Cooldowns: new List(), + AvailableBalance: CcdAmount.FromCcd(1) ) }, Array.Empty()); @@ -143,11 +146,14 @@ public async Task GenesisBlock_AccountCreation_BalanceTest() AccountAmount: CcdAmount.FromCcd(1), AccountIndex: receiverAccountId, AccountAddress: receiverAccount, - null + null, + Schedule: ReleaseSchedule.Empty(), + Cooldowns: new List(), + AvailableBalance: CcdAmount.FromCcd(1) ) }, - Array.Empty()); - + Array.Empty()); + var blockDataPayload = new BlockDataPayloadBuilder() .WithBlockInfo(blockInfo) .WithBlockItemSummaries(new List()) diff --git a/backend/Tests/Api/GraphQL/Import/AccountLookupTest.cs b/backend/Tests/Api/GraphQL/Import/AccountLookupTest.cs index 3bad5d815..33e9346c8 100644 --- a/backend/Tests/Api/GraphQL/Import/AccountLookupTest.cs +++ b/backend/Tests/Api/GraphQL/Import/AccountLookupTest.cs @@ -115,10 +115,13 @@ public async Task GivenNoAccountInCacheOrDatabase_WhenCallingNodeWhichKnowsAccou var clientMock = new Mock(); var accountInfo = new AccountInfo( AccountSequenceNumber.From(1UL), - CcdAmount.Zero, + CcdAmount.Zero, new AccountIndex(accountIndex), AccountAddress.From(uniqueAddress), - null + null, + Schedule: ReleaseSchedule.Empty(), + Cooldowns: new List(), + AvailableBalance: CcdAmount.Zero ); clientMock.Setup(m => m.GetAccountInfoAsync(It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql b/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql index c1cba8838..c862dc18c 100644 --- a/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql +++ b/backend/Tests/Api/GraphQL/__snapshots__/committed-schema.verified.graphql @@ -2117,27 +2117,49 @@ enum AccountStatementEntryType { TRANSACTION_FEE_REWARD } +"Types of account transactions." enum AccountTransactionType { + "Initialize a smart contract instance." INITIALIZE_SMART_CONTRACT_INSTANCE + "Update a smart contract instance." UPDATE_SMART_CONTRACT_INSTANCE + "Transfer CCD from an account to another." SIMPLE_TRANSFER + "Transfer encrypted amount." ENCRYPTED_TRANSFER + "Same as transfer, but with a memo field." SIMPLE_TRANSFER_WITH_MEMO + "Same as encrypted transfer, but with a memo." ENCRYPTED_TRANSFER_WITH_MEMO + "Same as transfer with schedule, but with an added memo." TRANSFER_WITH_SCHEDULE_WITH_MEMO + "Deploy a Wasm module." DEPLOY_MODULE + "Register an account as a baker." ADD_BAKER + "Remove an account as a baker." REMOVE_BAKER + "Update the staked amount." UPDATE_BAKER_STAKE + "Update whether the baker automatically restakes earnings." UPDATE_BAKER_RESTAKE_EARNINGS + "Update baker keys" UPDATE_BAKER_KEYS + "Update given credential keys" UPDATE_CREDENTIAL_KEYS + "Transfer from public to encrypted balance of the same account." TRANSFER_TO_ENCRYPTED + "Transfer from encrypted to public balance of the same account." TRANSFER_TO_PUBLIC + "Transfer a CCD with a release schedule." TRANSFER_WITH_SCHEDULE + "Update the account's credentials." UPDATE_CREDENTIALS + "Register some data on the chain." REGISTER_DATA + "Configure an account's baker." CONFIGURE_BAKER + "Configure an account's stake delegation." CONFIGURE_DELEGATION } @@ -2172,8 +2194,11 @@ enum ContractVersion { V1 } +"Enumeration of the types of credentials." enum CredentialDeploymentTransactionType { + "Initial credential is a credential that is submitted by the identity\nprovider on behalf of the user. There is at most one initial credential\nper identity." INITIAL + "A normal credential is one where the identity behind it is only known to\nthe owner of the account, unless the identity disclosure process was\nhas been initiated." NORMAL } @@ -2234,29 +2259,53 @@ enum TextDecodeType { HEX } +"The type of an update." enum UpdateTransactionType { + "Update of protocol version." UPDATE_PROTOCOL + "Update of the election difficulty." UPDATE_ELECTION_DIFFICULTY + "Update of conversion rate of Euro per energy." UPDATE_EURO_PER_ENERGY + "Update of conversion rate of CCD per Euro." UPDATE_MICRO_GTU_PER_EURO + "Update of account marked as foundation account." UPDATE_FOUNDATION_ACCOUNT + "Update of distribution of minted CCD." UPDATE_MINT_DISTRIBUTION + "Update of distribution of transaction fee." UPDATE_TRANSACTION_FEE_DISTRIBUTION + "Update of distribution of GAS rewards." UPDATE_GAS_REWARDS + "Update of minimum threshold for becoming a validator." UPDATE_BAKER_STAKE_THRESHOLD + "Introduce new Identity Disclosure Authority." UPDATE_ADD_ANONYMITY_REVOKER + "Introduce new Identity Provider." UPDATE_ADD_IDENTITY_PROVIDER + "Update of root keys." UPDATE_ROOT_KEYS + "Update of level1 keys." UPDATE_LEVEL1_KEYS + "Update of level2 keys." UPDATE_LEVEL2_KEYS + "Update of pool parameters." UPDATE_POOL_PARAMETERS + "Update of cooldown parameters." UPDATE_COOLDOWN_PARAMETERS + "Update of time parameters." UPDATE_TIME_PARAMETERS + "Update of distribution of minted CCD." MINT_DISTRIBUTION_CPV1_UPDATE + "Update of distribution of GAS rewards." GAS_REWARDS_CPV2_UPDATE + "Update of timeout parameters." TIMEOUT_PARAMETERS_UPDATE + "Update of min-block-time parameters." MIN_BLOCK_TIME_UPDATE + "Update of block energy limit parameters." BLOCK_ENERGY_LIMIT_UPDATE + "Update of finalization committee parameters." FINALIZATION_COMMITTEE_PARAMETERS_UPDATE } @@ -2284,4 +2333,4 @@ scalar TimeSpan scalar UnsignedInt "The UnsignedLong scalar type represents a unsigned 64-bit numeric non-fractional value greater than or equal to 0." -scalar UnsignedLong +scalar UnsignedLong \ No newline at end of file diff --git a/backend/Tests/TestUtilities/DatabaseFixture.cs b/backend/Tests/TestUtilities/DatabaseFixture.cs index 9ace61304..0403e4212 100644 --- a/backend/Tests/TestUtilities/DatabaseFixture.cs +++ b/backend/Tests/TestUtilities/DatabaseFixture.cs @@ -3,6 +3,7 @@ using Application.Api.GraphQL.EfCore; using Application.Database; using Dapper; +using Ductus.FluentDocker.Model.Compose; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; using Microsoft.EntityFrameworkCore; @@ -32,6 +33,7 @@ public DatabaseFixture() _service = new Builder() .UseContainer() .UseCompose() + .AssumeComposeVersion(ComposeVersion.V2) .FromFile(file) .RemoveOrphans() .WaitForPort("timescaledb-test", "5432/tcp", 30_000) diff --git a/backend/concordium-net-sdk b/backend/concordium-net-sdk new file mode 160000 index 000000000..66f608b19 --- /dev/null +++ b/backend/concordium-net-sdk @@ -0,0 +1 @@ +Subproject commit 66f608b19cc8c93a37582cf9c16056ef73a749eb