Skip to main content

Wallet-Core Architecture

This document covers the internal architecture of Wallet-Core, including the service layer, ledger, and data access patterns.

Layer Overview

Service Layer

Services Overview

ServiceFileResponsibility
SubscriberServicesubscriber_service.goUser registration, KYC, PIN management
CardServicecard_service.goCard linking, blocking, child cards
AgentServiceagent_service.goAgent registration, float, cashin
WalletServicewallet_service.goPayments, balances, transfers
ComplianceServicecompliance_service.goMonitoring rules, alerts
POSServicepos_service.goTerminal payments, verification
ProcessorServiceprocessor_service.goExternal processor integration
FeeServicefee_calculator.goFee calculation
AuditServiceaudit_service.goAudit log management
APIKeyServiceapikey_service.goAPI key CRUD
PEPAccessServicepep_access_service.goPrivileged access flows

Service Pattern

// Standard service structure
type SubscriberService struct {
    repo   *repository.SubscriberRepository
    ledger *ledger.Service
    audit  *AuditService
}

func NewSubscriberService(
    repo *repository.SubscriberRepository,
    ledger *ledger.Service,
    audit *AuditService,
) *SubscriberService {
    return &SubscriberService{
        repo:   repo,
        ledger: ledger,
        audit:  audit,
    }
}

func (s *SubscriberService) Register(ctx context.Context, req *RegisterRequest) (*Subscriber, error) {
    // Validate
    if err := validateRequest(req); err != nil {
        return nil, err
    }
    
    // Create subscriber
    subscriber, err := s.repo.Create(ctx, req)
    if err != nil {
        return nil, err
    }
    
    // Create account
    _, err = s.ledger.CreateAccount(ctx, subscriber.ID, "SLE")
    if err != nil {
        // Rollback or handle
    }
    
    // Audit
    s.audit.Log(ctx, "subscriber_registered", subscriber.ID)
    
    return subscriber, nil
}

Ledger

The ledger is the core transaction engine, handling all financial operations.

Core Operations

OperationDescription
ExecutePaymentAtomic payment between accounts
GetBalanceRetrieve account balance
CreateAccountCreate new account for user+currency
GetTransactionRetrieve transaction by ID
ListTransactionsQuery transaction history
GetAccountStatementGenerate account statements

Payment Execution

// ledger/service.go
func (l *Service) ExecutePayment(ctx context.Context, req *PaymentRequest) (*Transaction, error) {
    // Check idempotency
    existing, err := l.txRepo.GetByRequestID(ctx, req.RequestID)
    if err == nil && existing != nil {
        return existing, nil  // Return existing for duplicate
    }
    
    // Start transaction
    tx, err := l.db.BeginTx(ctx, nil)
    if err != nil {
        return nil, err
    }
    defer tx.Rollback()
    
    // Get or create accounts
    senderAcc, err := l.getOrCreateAccount(ctx, tx, req.UserID, req.Currency)
    if err != nil {
        return nil, err
    }
    
    recipientAcc, err := l.getOrCreateAccount(ctx, tx, req.RecipientID, req.Currency)
    if err != nil {
        return nil, err
    }
    
    // Validate balance
    if senderAcc.Balance < req.Amount {
        return nil, ErrInsufficientBalance
    }
    
    // Debit sender
    if err := l.debit(ctx, tx, senderAcc.ID, req.Amount); err != nil {
        return nil, err
    }
    
    // Credit recipient
    if err := l.credit(ctx, tx, recipientAcc.ID, req.Amount); err != nil {
        return nil, err
    }
    
    // Create transaction record
    txn := &Transaction{
        ID:          generateID(),
        RequestID:   req.RequestID,
        UserID:      req.UserID,
        RecipientID: req.RecipientID,
        Amount:      req.Amount,
        Currency:    req.Currency,
        Status:      "COMPLETED",
        CreatedAt:   time.Now(),
        CompletedAt: time.Now(),
    }
    
    if err := l.txRepo.Create(ctx, tx, txn); err != nil {
        return nil, err
    }
    
    // Audit log
    if err := l.auditLog(ctx, tx, txn); err != nil {
        return nil, err
    }
    
    // Commit
    if err := tx.Commit(); err != nil {
        return nil, err
    }
    
    return txn, nil
}

Repository Layer

Repositories provide data access and encapsulate SQL queries.

Repository Pattern

// repository/account_repository.go
type AccountRepository struct {
    db *sql.DB
}

func (r *AccountRepository) GetByUserAndCurrency(
    ctx context.Context,
    userID, currency string,
) (*models.Account, error) {
    query := `
        SELECT id, user_id, currency, balance, created_at, updated_at
        FROM accounts
        WHERE user_id = $1 AND currency = $2
    `
    
    var acc models.Account
    err := r.db.QueryRowContext(ctx, query, userID, currency).Scan(
        &acc.ID, &acc.UserID, &acc.Currency, 
        &acc.Balance, &acc.CreatedAt, &acc.UpdatedAt,
    )
    
    if err == sql.ErrNoRows {
        return nil, ErrAccountNotFound
    }
    
    return &acc, err
}

func (r *AccountRepository) UpdateBalance(
    ctx context.Context,
    tx *sql.Tx,
    accountID string,
    delta int64,
) error {
    query := `
        UPDATE accounts 
        SET balance = balance + $1, updated_at = NOW()
        WHERE id = $2
    `
    
    _, err := tx.ExecContext(ctx, query, delta, accountID)
    return err
}

Available Repositories

RepositoryTableOperations
AccountRepositoryaccountsCRUD, balance updates
TransactionRepositorytransactionsCreate, query, list
SubscriberRepositorysubscribersCRUD, lookup by phone
CardRepositorynfc_cardsLink, block, list
AgentRepositoryagentsCRUD, float management
AuditLogRepositoryaudit_logCreate, query
APIKeyRepositoryapi_keysCRUD
ComplianceAlertRepositorycompliance_alertsCreate, resolve
MonitoringRuleRepositorymonitoring_rulesCRUD

gRPC Server

The server layer maps gRPC requests to service calls:
// server/server.go
type WalletServer struct {
    wallet.UnimplementedWalletServiceServer
    ledger *ledger.Service
}

func (s *WalletServer) ExecutePayment(
    ctx context.Context,
    req *wallet.PaymentRequest,
) (*wallet.PaymentResponse, error) {
    // Map proto to domain
    payment := &ledger.PaymentRequest{
        RequestID:   req.RequestId,
        UserID:      req.UserId,
        RecipientID: req.RecipientId,
        Amount:      req.Amount,
        Currency:    req.Currency,
        Memo:        req.Memo,
    }
    
    // Execute
    txn, err := s.ledger.ExecutePayment(ctx, payment)
    if err != nil {
        return nil, mapToGRPCError(err)
    }
    
    // Map to proto response
    return &wallet.PaymentResponse{
        TransactionId: txn.ID,
        Status:        txn.Status,
        NewBalance:    txn.NewBalance,
    }, nil
}

Database Migrations

Migrations are run at startup:
// database/database.go
func Migrate(db *sql.DB) error {
    migrations := []string{
        `CREATE TABLE IF NOT EXISTS accounts (...)`,
        `CREATE TABLE IF NOT EXISTS transactions (...)`,
        `CREATE TABLE IF NOT EXISTS subscribers (...)`,
        // ... more tables
    }
    
    for _, m := range migrations {
        if _, err := db.Exec(m); err != nil {
            return err
        }
    }
    
    return nil
}

Error Handling

Domain errors are defined and mapped:
// errors.go
var (
    ErrAccountNotFound     = errors.New("account not found")
    ErrInsufficientBalance = errors.New("insufficient balance")
    ErrDuplicateRequest    = errors.New("duplicate request")
    ErrCardAlreadyLinked   = errors.New("card already linked")
)

// Map to gRPC status codes
func mapToGRPCError(err error) error {
    switch {
    case errors.Is(err, ErrAccountNotFound):
        return status.Error(codes.NotFound, err.Error())
    case errors.Is(err, ErrInsufficientBalance):
        return status.Error(codes.FailedPrecondition, err.Error())
    case errors.Is(err, ErrDuplicateRequest):
        return status.Error(codes.AlreadyExists, err.Error())
    default:
        return status.Error(codes.Internal, "internal error")
    }
}

Next Steps