Files
GL/application/ledger/authority.go
T
nfel 88e227f80c
CI/CD Stage / stage (push) Failing after 1m12s
feat(gl): add standalone ledger authority mode
2026-10-06 22:40:07 +03:30

80 lines
3.4 KiB
Go

package ledger
import (
"context"
"strings"
domain "gl/domain/ledger"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
type AuthorityRepository interface {
OperatingMode(context.Context) (domain.OperatingState, error)
SetOperatingMode(context.Context, domain.OperatingMode, int64, string, string, bool) (domain.OperatingState, error)
UserBalances(context.Context, string, string, *int64) ([]domain.UserAssetBalance, error)
}
func (s *Service) OperatingMode(ctx context.Context) (domain.OperatingState, error) {
repository, ok := s.repository.(AuthorityRepository)
if !ok {
return domain.OperatingState{}, domain.ErrModeRequired
}
return repository.OperatingMode(ctx)
}
func (s *Service) SetOperatingMode(ctx context.Context, mode domain.OperatingMode, revision int64, actor, reason string, reconciled bool) (domain.OperatingState, error) {
if (mode != domain.ModeNormal && mode != domain.ModeGLOnly) || revision < 0 || strings.TrimSpace(actor) == "" || strings.TrimSpace(reason) == "" {
return domain.OperatingState{}, invalid("mode, nonnegative revision, actor, and reason are required")
}
repository, ok := s.repository.(AuthorityRepository)
if !ok {
return domain.OperatingState{}, domain.ErrModeRequired
}
ctx, span := otel.Tracer("gl/ledger").Start(ctx, "ledger.set_operating_mode")
defer span.End()
span.SetAttributes(attribute.String("ledger.mode", string(mode)), attribute.Int64("ledger.expected_revision", revision), attribute.String("ledger.actor_id", actor))
state, err := repository.SetOperatingMode(ctx, mode, revision, actor, reason, reconciled)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "mode change failed")
return domain.OperatingState{}, err
}
if state.Revision != revision {
span.AddEvent("ledger.operating_mode_changed")
}
span.SetAttributes(attribute.Int64("ledger.revision", state.Revision))
return state, nil
}
func (s *Service) UserBalances(ctx context.Context, ownerType, ownerID string, assetID *int64) ([]domain.UserAssetBalance, error) {
if strings.TrimSpace(ownerType) == "" || strings.TrimSpace(ownerID) == "" || (assetID != nil && *assetID <= 0) {
return nil, invalid("owner type, owner id, and a positive optional asset id are required")
}
repository, ok := s.repository.(AuthorityRepository)
if !ok {
return nil, domain.ErrNotFound
}
return repository.UserBalances(ctx, ownerType, ownerID, assetID)
}
// PostInternalJournal executes exact balanced entries without network settlement.
// PostgreSQL rechecks mode and funds under locks in the same journal transaction.
func (s *Service) PostInternalJournal(ctx context.Context, command AppendJournalCommand) (AppendJournalResult, error) {
if strings.TrimSpace(command.ActorID) == "" {
return AppendJournalResult{}, invalid("actor id is required for internal execution")
}
if command.Blockchain != (domain.BlockchainReference{}) {
return AppendJournalResult{}, invalid("internal execution cannot claim blockchain settlement")
}
for _, entry := range command.Entries {
if entry.Account.Class == domain.AccountClassExternalBlockchain {
return AppendJournalResult{}, invalid("external blockchain movements cannot execute internally")
}
}
command.Metadata = cloneMetadata(command.Metadata)
command.Metadata[domain.InternalExecutionMetadata] = string(domain.ModeGLOnly)
return s.appendJournal(ctx, command)
}