package grpcadapter import ( "context" "errors" "time" applicationledger "gl/application/ledger" domain "gl/domain/ledger" ledgerv1 "gl/gen/ledger/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) func (h *Handler) AppendJournal(ctx context.Context, request *ledgerv1.AppendJournalRequest) (*ledgerv1.AppendJournalResponse, error) { command, err := appendJournalCommand(request) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } result, err := h.ledger.AppendJournal(ctx, command) if err != nil { return nil, rpcError(err) } return &ledgerv1.AppendJournalResponse{ Journal: journalMessage(result.Journal), AlreadyExisted: result.AlreadyExisted, }, nil } func (h *Handler) AppendTransactionEvent(ctx context.Context, request *ledgerv1.AppendTransactionEventRequest) (*ledgerv1.AppendTransactionEventResponse, error) { occurredAt, err := requiredTime(request.GetOccurredAt(), "occurred_at") if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } event, alreadyExisted, err := h.ledger.AppendEvent(ctx, applicationledger.AppendEventCommand{ SourceService: request.GetSourceService(), IdempotencyKey: request.GetIdempotencyKey(), SourceTransactionID: request.GetSourceTransactionId(), TrackingCode: request.GetTrackingCode(), EventVersion: request.GetEventVersion(), State: transactionState(request.GetState()), ErrorCode: request.GetErrorCode(), ErrorMessage: request.GetErrorMessage(), OccurredAt: occurredAt, CorrelationID: request.GetCorrelationId(), ActorID: request.GetActorId(), Blockchain: blockchainReference(request.GetBlockchain()), Metadata: request.GetMetadata(), }) if err != nil { return nil, rpcError(err) } return &ledgerv1.AppendTransactionEventResponse{ Event: eventMessage(event), AlreadyExisted: alreadyExisted, }, nil } func (h *Handler) GetJournal(ctx context.Context, request *ledgerv1.GetJournalRequest) (*ledgerv1.Journal, error) { var journalID, idempotencyKey string switch lookup := request.GetLookup().(type) { case *ledgerv1.GetJournalRequest_JournalId: journalID = lookup.JournalId case *ledgerv1.GetJournalRequest_IdempotencyKey: idempotencyKey = lookup.IdempotencyKey } journal, err := h.ledger.GetJournal(ctx, journalID, idempotencyKey) if err != nil { return nil, rpcError(err) } return journalMessage(journal), nil } func (h *Handler) ListEntries(ctx context.Context, request *ledgerv1.ListEntriesRequest) (*ledgerv1.ListEntriesResponse, error) { command := applicationledger.ListCommand{ PageSize: request.GetPageSize(), PageToken: request.GetPageToken(), } if request.Account != nil { account := accountReference(request.Account) command.Account = &account } if request.AssetId != nil { assetID := request.GetAssetId() command.AssetID = &assetID } var err error if request.RecordedFrom != nil { value, parseErr := requiredTime(request.RecordedFrom, "recorded_from") if parseErr != nil { return nil, status.Error(codes.InvalidArgument, parseErr.Error()) } command.RecordedFrom = &value } if request.RecordedTo != nil { value, parseErr := requiredTime(request.RecordedTo, "recorded_to") if parseErr != nil { return nil, status.Error(codes.InvalidArgument, parseErr.Error()) } command.RecordedTo = &value } journals, nextPageToken, err := h.ledger.List(ctx, command) if err != nil { return nil, rpcError(err) } response := &ledgerv1.ListEntriesResponse{ Journals: make([]*ledgerv1.Journal, 0, len(journals)), NextPageToken: nextPageToken, } for _, journal := range journals { response.Journals = append(response.Journals, journalMessage(journal)) } return response, nil } func (h *Handler) GetBalance(ctx context.Context, request *ledgerv1.GetBalanceRequest) (*ledgerv1.GetBalanceResponse, error) { if request.Account == nil { return nil, status.Error(codes.InvalidArgument, "account is required") } asOf := time.Now().UTC() if request.AsOf != nil { value, err := requiredTime(request.AsOf, "as_of") if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } asOf = value } account := accountReference(request.Account) balance, err := h.ledger.Balance(ctx, account, asOf) if err != nil { return nil, rpcError(err) } return &ledgerv1.GetBalanceResponse{ Account: accountMessage(account), Balance: balance.String(), AsOf: timestamppb.New(asOf), }, nil } func (h *Handler) ReplayJournals(ctx context.Context, request *ledgerv1.ReplayJournalsRequest) (*ledgerv1.ReplayJournalsResponse, error) { commands := make([]applicationledger.AppendJournalCommand, 0, len(request.GetJournals())) for _, journal := range request.GetJournals() { command, err := appendJournalCommand(journal) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } commands = append(commands, command) } results, err := h.ledger.Replay(ctx, commands) if err != nil { return nil, rpcError(err) } response := &ledgerv1.ReplayJournalsResponse{Results: make([]*ledgerv1.ReplayJournalResult, 0, len(results))} for _, result := range results { response.Results = append(response.Results, &ledgerv1.ReplayJournalResult{ IdempotencyKey: result.Journal.IdempotencyKey, Journal: journalMessage(result.Journal), AlreadyExisted: result.AlreadyExisted, }) } return response, nil } func appendJournalCommand(request *ledgerv1.AppendJournalRequest) (applicationledger.AppendJournalCommand, error) { occurredAt, err := requiredTime(request.GetOccurredAt(), "occurred_at") if err != nil { return applicationledger.AppendJournalCommand{}, err } command := applicationledger.AppendJournalCommand{ SourceService: request.GetSourceService(), IdempotencyKey: request.GetIdempotencyKey(), SourceTransactionID: request.GetSourceTransactionId(), TrackingCode: request.GetTrackingCode(), EffectKind: request.GetEffectKind(), EventVersion: request.GetEventVersion(), ReversalOfJournalID: request.GetReversalOfJournalId(), OccurredAt: occurredAt, CorrelationID: request.GetCorrelationId(), ActorID: request.GetActorId(), Blockchain: blockchainReference(request.GetBlockchain()), Metadata: request.GetMetadata(), Entries: make([]applicationledger.EntryCommand, 0, len(request.GetEntries())), } for _, entry := range request.GetEntries() { if entry.Account == nil { return applicationledger.AppendJournalCommand{}, errors.New("entry account is required") } command.Entries = append(command.Entries, applicationledger.EntryCommand{ LineNumber: entry.GetLineNumber(), Account: accountReference(entry.Account), Amount: entry.GetAmount(), Description: entry.GetDescription(), }) } return command, nil } func requiredTime(value *timestamppb.Timestamp, field string) (time.Time, error) { if value == nil { return time.Time{}, errors.New(field + " is required") } if err := value.CheckValid(); err != nil { return time.Time{}, errors.New(field + " is invalid: " + err.Error()) } return value.AsTime().UTC(), nil } func accountReference(value *ledgerv1.AccountReference) domain.AccountReference { return domain.AccountReference{ Class: accountClass(value.GetAccountClass()), OwnerType: value.GetOwnerType(), OwnerID: value.GetOwnerId(), AssetID: value.GetAssetId(), } } func accountClass(value ledgerv1.AccountClass) domain.AccountClass { switch value { case ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE: return domain.AccountClassUserAvailable case ledgerv1.AccountClass_ACCOUNT_CLASS_USER_FROZEN: return domain.AccountClassUserFrozen case ledgerv1.AccountClass_ACCOUNT_CLASS_EXTERNAL_BLOCKCHAIN: return domain.AccountClassExternalBlockchain case ledgerv1.AccountClass_ACCOUNT_CLASS_TREASURY: return domain.AccountClassTreasury case ledgerv1.AccountClass_ACCOUNT_CLASS_MARKET_CLEARING: return domain.AccountClassMarketClearing case ledgerv1.AccountClass_ACCOUNT_CLASS_IPG_CLEARING: return domain.AccountClassIPGClearing case ledgerv1.AccountClass_ACCOUNT_CLASS_COMMISSION_REVENUE: return domain.AccountClassCommissionRevenue default: return "" } } func accountClassMessage(value domain.AccountClass) ledgerv1.AccountClass { switch value { case domain.AccountClassUserAvailable: return ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE case domain.AccountClassUserFrozen: return ledgerv1.AccountClass_ACCOUNT_CLASS_USER_FROZEN case domain.AccountClassExternalBlockchain: return ledgerv1.AccountClass_ACCOUNT_CLASS_EXTERNAL_BLOCKCHAIN case domain.AccountClassTreasury: return ledgerv1.AccountClass_ACCOUNT_CLASS_TREASURY case domain.AccountClassMarketClearing: return ledgerv1.AccountClass_ACCOUNT_CLASS_MARKET_CLEARING case domain.AccountClassIPGClearing: return ledgerv1.AccountClass_ACCOUNT_CLASS_IPG_CLEARING case domain.AccountClassCommissionRevenue: return ledgerv1.AccountClass_ACCOUNT_CLASS_COMMISSION_REVENUE default: return ledgerv1.AccountClass_ACCOUNT_CLASS_UNSPECIFIED } } func transactionState(value ledgerv1.TransactionState) domain.TransactionState { switch value { case ledgerv1.TransactionState_TRANSACTION_STATE_CREATED: return domain.TransactionStateCreated case ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_TRANSACTION: return domain.TransactionStatePendingTransaction case ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_ADMIN: return domain.TransactionStatePendingAdmin case ledgerv1.TransactionState_TRANSACTION_STATE_SUCCESSFUL: return domain.TransactionStateSuccessful case ledgerv1.TransactionState_TRANSACTION_STATE_FAILED: return domain.TransactionStateFailed case ledgerv1.TransactionState_TRANSACTION_STATE_SUSPENDED: return domain.TransactionStateSuspended default: return "" } } func transactionStateMessage(value domain.TransactionState) ledgerv1.TransactionState { switch value { case domain.TransactionStateCreated: return ledgerv1.TransactionState_TRANSACTION_STATE_CREATED case domain.TransactionStatePendingTransaction: return ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_TRANSACTION case domain.TransactionStatePendingAdmin: return ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_ADMIN case domain.TransactionStateSuccessful: return ledgerv1.TransactionState_TRANSACTION_STATE_SUCCESSFUL case domain.TransactionStateFailed: return ledgerv1.TransactionState_TRANSACTION_STATE_FAILED case domain.TransactionStateSuspended: return ledgerv1.TransactionState_TRANSACTION_STATE_SUSPENDED default: return ledgerv1.TransactionState_TRANSACTION_STATE_UNSPECIFIED } } func blockchainReference(value *ledgerv1.BlockchainReference) domain.BlockchainReference { if value == nil { return domain.BlockchainReference{} } return domain.BlockchainReference{ Network: value.GetNetwork(), TransactionHash: value.GetTransactionHash(), LedgerSequence: value.GetLedgerSequence(), } } func journalMessage(value domain.Journal) *ledgerv1.Journal { message := &ledgerv1.Journal{ JournalId: value.ID, SourceService: value.SourceService, IdempotencyKey: value.IdempotencyKey, SourceTransactionId: value.SourceTransactionID, TrackingCode: value.TrackingCode, EffectKind: value.EffectKind, EventVersion: value.EventVersion, OccurredAt: timestampMessage(value.OccurredAt), RecordedAt: timestampMessage(value.RecordedAt), CorrelationId: value.CorrelationID, ActorId: value.ActorID, Blockchain: blockchainMessage(value.Blockchain), Metadata: value.Metadata, PayloadHash: value.PayloadHash, Entries: make([]*ledgerv1.JournalEntry, 0, len(value.Entries)), } if value.ReversalOfJournalID != "" { message.ReversalOfJournalId = &value.ReversalOfJournalID } for _, entry := range value.Entries { message.Entries = append(message.Entries, &ledgerv1.JournalEntry{ LineNumber: entry.LineNumber, Account: accountMessage(entry.Account), Amount: entry.Amount.String(), Description: entry.Description, }) } return message } func eventMessage(value domain.TransactionEvent) *ledgerv1.TransactionEvent { return &ledgerv1.TransactionEvent{ EventId: value.ID, Event: &ledgerv1.AppendTransactionEventRequest{ SourceService: value.SourceService, IdempotencyKey: value.IdempotencyKey, SourceTransactionId: value.SourceTransactionID, TrackingCode: value.TrackingCode, EventVersion: value.EventVersion, State: transactionStateMessage(value.State), ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, OccurredAt: timestampMessage(value.OccurredAt), CorrelationId: value.CorrelationID, ActorId: value.ActorID, Blockchain: blockchainMessage(value.Blockchain), Metadata: value.Metadata, }, RecordedAt: timestampMessage(value.RecordedAt), PayloadHash: value.PayloadHash, } } func accountMessage(value domain.AccountReference) *ledgerv1.AccountReference { return &ledgerv1.AccountReference{ AccountClass: accountClassMessage(value.Class), OwnerType: value.OwnerType, OwnerId: value.OwnerID, AssetId: value.AssetID, } } func blockchainMessage(value domain.BlockchainReference) *ledgerv1.BlockchainReference { return &ledgerv1.BlockchainReference{ Network: value.Network, TransactionHash: value.TransactionHash, LedgerSequence: value.LedgerSequence, } } func timestampMessage(value time.Time) *timestamppb.Timestamp { if value.IsZero() { return nil } return timestamppb.New(value) } func rpcError(err error) error { switch { case errors.Is(err, applicationledger.ErrInvalidArgument): return status.Error(codes.InvalidArgument, err.Error()) case errors.Is(err, domain.ErrNotFound): return status.Error(codes.NotFound, err.Error()) case errors.Is(err, domain.ErrIdempotencyConflict): return status.Error(codes.AlreadyExists, err.Error()) case errors.Is(err, domain.ErrIncompleteJournal): return status.Error(codes.FailedPrecondition, err.Error()) case errors.Is(err, domain.ErrAlreadyReversed): return status.Error(codes.FailedPrecondition, err.Error()) case errors.Is(err, context.Canceled): return status.Error(codes.Canceled, err.Error()) case errors.Is(err, context.DeadlineExceeded): return status.Error(codes.DeadlineExceeded, err.Error()) default: return status.Error(codes.Internal, "internal ledger error") } }