Documentation Index Fetch the complete documentation index at: https://docs.vultlocal.com/llms.txt
Use this file to discover all available pages before exploring further.
Ledger
The Ledger is the core transaction processing engine in Wallet-Core, responsible for atomic payments, account management, and transaction integrity.
Core Principles
Atomicity All operations complete entirely or not at all
Idempotency Duplicate requests return existing results
Consistency Balances always reflect actual state
Auditability Every operation is logged for traceability
Payment Flow
Idempotency
Every payment request requires a unique request_id. If the same request_id is submitted again, the ledger returns the existing transaction without re-executing.
Implementation
func ( l * Service ) ExecutePayment ( ctx context . Context , req * PaymentRequest ) ( * Transaction , error ) {
// Check for existing transaction with same request_id
existing , err := l . txRepo . GetByRequestID ( ctx , req . RequestID )
if err == nil && existing != nil {
// Return cached result
return existing , nil
}
// Proceed with new transaction
// ...
}
Client Usage
# First request
curl -X POST /api/v1/payments \
-H "X-Request-ID: uuid-12345" \
-d '{"user_id": "u1", "recipient": "u2", "amount": 1000}'
# Returns: transaction_id = txn_abc
# Duplicate request (same X-Request-ID)
curl -X POST /api/v1/payments \
-H "X-Request-ID: uuid-12345" \
-d '{"user_id": "u1", "recipient": "u2", "amount": 1000}'
# Returns: same transaction_id = txn_abc (no new transaction created)
Account Model
Accounts are created per user and currency combination.
Schema
CREATE TABLE accounts (
id TEXT PRIMARY KEY ,
user_id TEXT NOT NULL ,
currency TEXT NOT NULL ,
balance BIGINT NOT NULL DEFAULT 0 ,
created_at TIMESTAMP DEFAULT NOW (),
updated_at TIMESTAMP DEFAULT NOW (),
UNIQUE (user_id, currency)
);
CREATE INDEX idx_accounts_user_id ON accounts(user_id);
CREATE INDEX idx_accounts_user_currency ON accounts(user_id, currency);
Lazy Creation
Accounts are created automatically when needed:
func ( l * Service ) getOrCreateAccount ( ctx context . Context , tx * sql . Tx , userID , currency string ) ( * Account , error ) {
// Try to get existing
acc , err := l . accRepo . GetByUserAndCurrency ( ctx , userID , currency )
if err == nil {
return acc , nil
}
// Create new account
newAcc := & Account {
ID : generateID (),
UserID : userID ,
Currency : currency ,
Balance : 0 ,
CreatedAt : time . Now (),
}
if err := l . accRepo . Create ( ctx , tx , newAcc ); err != nil {
return nil , err
}
return newAcc , nil
}
Transaction Model
Schema
CREATE TABLE transactions (
id TEXT PRIMARY KEY ,
request_id TEXT UNIQUE NOT NULL ,
user_id TEXT NOT NULL ,
recipient_id TEXT NOT NULL ,
amount BIGINT NOT NULL ,
currency TEXT NOT NULL ,
status TEXT NOT NULL ,
memo TEXT ,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW (),
completed_at TIMESTAMP
);
CREATE INDEX idx_transactions_user_id ON transactions(user_id);
CREATE INDEX idx_transactions_request_id ON transactions(request_id);
CREATE INDEX idx_transactions_created_at ON transactions(created_at);
Transaction Statuses
Status Description PENDINGTransaction initiated, not yet processed COMPLETEDSuccessfully executed FAILEDProcessing failed REVERSEDTransaction was reversed EXPIREDPending transaction expired
Balance Operations
Get Balance
func ( l * Service ) GetBalance ( ctx context . Context , userID , currency string ) ( * Balance , error ) {
acc , err := l . accRepo . GetByUserAndCurrency ( ctx , userID , currency )
if err != nil {
if errors . Is ( err , ErrAccountNotFound ) {
// Return zero balance for non-existent account
return & Balance {
UserID : userID ,
Currency : currency ,
Balance : 0 ,
}, nil
}
return nil , err
}
return & Balance {
UserID : acc . UserID ,
Currency : acc . Currency ,
Balance : acc . Balance ,
AsOf : time . Now (),
}, nil
}
Debit/Credit
func ( l * Service ) debit ( ctx context . Context , tx * sql . Tx , accountID string , amount int64 ) error {
query := `
UPDATE accounts
SET balance = balance - $1, updated_at = NOW()
WHERE id = $2 AND balance >= $1
`
result , err := tx . ExecContext ( ctx , query , amount , accountID )
if err != nil {
return err
}
rows , _ := result . RowsAffected ()
if rows == 0 {
return ErrInsufficientBalance
}
return nil
}
func ( l * Service ) credit ( ctx context . Context , tx * sql . Tx , accountID string , amount int64 ) error {
query := `
UPDATE accounts
SET balance = balance + $1, updated_at = NOW()
WHERE id = $2
`
_ , err := tx . ExecContext ( ctx , query , amount , accountID )
return err
}
Audit Logging
Every transaction creates an audit log entry:
func ( l * Service ) auditLog ( ctx context . Context , tx * sql . Tx , txn * Transaction ) error {
entry := & AuditLog {
ID : generateID (),
TransactionID : txn . ID ,
EventType : "PAYMENT_EXECUTED" ,
Actor : txn . UserID ,
Details : map [ string ] interface {}{
"amount" : txn . Amount ,
"currency" : txn . Currency ,
"recipient" : txn . RecipientID ,
},
Timestamp : time . Now (),
}
return l . auditRepo . Create ( ctx , tx , entry )
}
Audit Log Schema
CREATE TABLE audit_log (
id TEXT PRIMARY KEY ,
transaction_id TEXT ,
event_type TEXT NOT NULL ,
actor TEXT ,
details JSONB,
timestamp TIMESTAMP DEFAULT NOW ()
);
CREATE INDEX idx_audit_log_transaction_id ON audit_log(transaction_id);
CREATE INDEX idx_audit_log_event_type ON audit_log(event_type);
CREATE INDEX idx_audit_log_timestamp ON audit_log( timestamp );
Transaction Queries
List Transactions
func ( l * Service ) ListTransactions ( ctx context . Context , req * ListRequest ) ( * TransactionList , error ) {
query := `
SELECT id, request_id, user_id, recipient_id, amount, currency, status, memo, metadata, created_at, completed_at
FROM transactions
WHERE user_id = $1 OR recipient_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
rows , err := l . db . QueryContext ( ctx , query , req . UserID , req . Limit , req . Offset )
// ... process rows
}
Account Statement
func ( l * Service ) GetAccountStatement ( ctx context . Context , req * StatementRequest ) ( * Statement , error ) {
query := `
SELECT
id,
CASE WHEN user_id = $1 THEN -amount ELSE amount END as delta,
created_at,
memo
FROM transactions
WHERE (user_id = $1 OR recipient_id = $1)
AND currency = $2
AND created_at BETWEEN $3 AND $4
ORDER BY created_at
`
// Execute and build statement with running balance
// ...
}
Reconciliation
Wallet-Core includes VULT reconciliation for external processor integration:
// services/vult_reconciliation.go
func ( s * VULTReconciliationService ) Reconcile ( ctx context . Context ) ( * ReconciliationResult , error ) {
// Fetch external transactions from VULT
external , err := s . vult . GetTransactions ( ctx , lastReconcileTime )
if err != nil {
return nil , err
}
// Compare with internal records
internal , err := s . txRepo . GetByTimeRange ( ctx , lastReconcileTime , time . Now ())
if err != nil {
return nil , err
}
// Find discrepancies
mismatches := findMismatches ( external , internal )
// Create alerts for mismatches
for _ , m := range mismatches {
s . compliance . CreateAlert ( ctx , "RECONCILIATION_MISMATCH" , m )
}
return & ReconciliationResult {
Processed : len ( external ),
Matched : len ( external ) - len ( mismatches ),
Mismatches : len ( mismatches ),
}, nil
}
Error Handling
Common ledger errors:
Error Code Description ErrInsufficientBalanceINSUFFICIENT_BALANCESender balance too low ErrAccountNotFoundACCOUNT_NOT_FOUNDAccount doesn’t exist ErrDuplicateRequestDUPLICATE_REQUESTRequest ID already used ErrInvalidAmountINVALID_AMOUNTAmount must be positive ErrCurrencyMismatchCURRENCY_MISMATCHSender/recipient currency mismatch
Next Steps
Configuration Database and server configuration
API Reference Payment API documentation