timefrontiers / php-wallet
Blockchain-like immutable wallet and transaction system with file-based ledger
v1.0
2026-04-16 07:07 UTC
Requires
- php: >=8.1
- ext-pdo: *
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
- timefrontiers/php-core: For TimeFrontiers ecosystem integration
- timefrontiers/php-validator: For input validation
This package is auto-updated.
Last update: 2026-04-16 11:42:53 UTC
README
Blockchain-like immutable wallet and transaction system with file-based ledger.
Features
- Immutable Ledger: Append-only file-based ledger as source of truth
- Rolling Files: Monthly ledger archives prevent file bloat
- Checkpointing: Periodic snapshots for fast recovery
- Integrity Verification: Checksums detect tampering
- Batch Transactions: Efficient bulk transfers
- Payment Integration: Interface-based external payment verification
- Flexible Database: Pass credentials at initialization
Installation
composer require timefrontiers/php-wallet
Quick Start
use TimeFrontiers\Wallet\{Config, Wallet, Transaction}; // Database credentials $db_cred = [ 'server' => 'localhost', 'username' => 'wallet_user', 'password' => 'secure_password', 'database' => 'myapp_wallet', // Your database name (with any prefix) ]; // Configure ledger path Config::setLedgerPath('/var/data/.txhive/ledgers'); // Create/load wallets with credentials $alice = new Wallet('USR123456', 'NGN', $db_cred); $bob = new Wallet('USR789012', 'NGN', $db_cred); // Check balance echo $alice->balance(); // 5000.00 // Transfer $hashes = Transaction::transfer($alice, $bob, 1000.00, 'Payment for services'); // Batch transfer $result = Transaction::batch($alice) ->credit($bob, 500, 'Bonus') ->credit('219555555555555', 200, 'Refund') ->execute();
Database Setup
1. Create Your Database
Create your database manually with any required prefix:
CREATE DATABASE myprefix_wallet;
2. Run the Schema
The schema file is included in the package at schema/schema.sql:
mysql -u root -p myprefix_wallet < vendor/timefrontiers/php-wallet/schema/schema.sql
Or run directly in MySQL:
USE myprefix_wallet; SOURCE /path/to/vendor/timefrontiers/php-wallet/schema/schema.sql;
Schema Tables
| Table | Purpose |
|---|---|
wallets |
Wallet addresses and ownership |
wallet_history |
Immutable transaction log (triggers prevent UPDATE/DELETE) |
tranx_alert |
Notification queue |
wallet_batches |
Batch transaction metadata (optional) |
exchange_rates |
Dynamic rate history (optional) |
ledger_integrity |
File checksum tracking (optional) |
balance_snapshots |
Daily balance history (optional) |
Schema Views
| View | Purpose |
|---|---|
v_wallet_balances |
Quick balance lookup |
v_recent_transactions |
Last 1000 transactions |
v_pending_alerts |
Unsent notifications |
Database Connection
Option 1: Pass Credentials to Constructor
$db_cred = [ 'server' => 'localhost', 'username' => 'db_user', 'password' => 'db_pass', 'database' => 'myprefix_wallet', ]; $wallet = new Wallet('USR123456', 'NGN', $db_cred);
Option 2: Pass PDO Instance
$pdo = new PDO('mysql:host=localhost;dbname=myprefix_wallet', 'user', 'pass'); $wallet = new Wallet('USR123456', 'NGN', $pdo);
Option 3: Set Shared Connection
// Set once at application startup Wallet::setDatabase([ 'server' => 'localhost', 'username' => 'db_user', 'password' => 'db_pass', 'database' => 'myprefix_wallet', ]); // Now credentials are optional $wallet = new Wallet('USR123456', 'NGN');
Using Wallet's Connection
Each wallet instance has its own db connection:
$wallet = new Wallet('USR123456', 'NGN', $db_cred); // Access the connection $db = $wallet->db(); // Transaction methods use wallet's connection automatically Transaction::transfer($from, $to, 100, 'Payment');
Configuration
use TimeFrontiers\Wallet\Config; // Ledger storage path Config::setLedgerPath('/var/data/.txhive/ledgers'); // Decimal precision (default: 8) Config::setPrecision(8); // Wallet address format (default: prefix=219, length=15) Config::setAddressPrefix('219'); Config::setAddressLength(15); // Batch ID format (default: prefix=127, length=15) Config::setBatchPrefix('127'); Config::setBatchLength(15); // Strict integrity mode (fail on ledger/DB mismatch) Config::setStrictIntegrity(true); // Payment verifier Config::setPaymentVerifier(new MyPaymentVerifier()); // Exchange rates Config::setExchangeProvider(new FixedRateProvider([ 'NGN:DWL' => 1.0, 'USD:NGN' => 1550.0, ]));
Code Generation
Codes are generated with configurable prefix and length:
// Wallet address: 219 + 12 random digits = 219123456789012 Config::setAddressPrefix('219'); Config::setAddressLength(15); $address = Config::generateAddress(); // Batch ID: 127 + 12 random digits = 127987654321098 Config::setBatchPrefix('127'); Config::setBatchLength(15); $batch = Config::generateBatch(); // Generic code generation $code = Config::generateCode('PRE', 18); // PRE + 15 digits // Validation Config::isValidAddress('219123456789012'); // true Config::isValidBatch('127987654321098'); // true
Wallet Operations
Create/Load Wallet
// By user + currency (creates if not exists) $wallet = new Wallet('USR123456', 'NGN', $db_cred); // By address (must exist) $wallet = new Wallet('219123456789012', '', $db_cred); // Check existence Wallet::exists('219123456789012', $pdo); // Find user's wallets $wallets = Wallet::findByUser('USR123456', $pdo);
Balance
// Get balance (from ledger - source of truth) $balance = $wallet->balance(); // Verify against database (throws IntegrityException on mismatch) $balance = $wallet->balance(verify: true); // Check sufficient funds $wallet->hasSufficientBalance(1000.00); // bool
Transactions
Transfer Between Wallets
$hashes = Transaction::transfer($from, $to, 1000.00, 'Payment'); // Returns: ['credit_hash', 'debit_hash']
Credit from External Payment
use TimeFrontiers\Wallet\Payment\MockPaymentVerifier; $verifier = new MockPaymentVerifier(); $verifier->addPayment('PAY123456', 5000.00, 'NGN', 'paid'); $hash = Transaction::creditFromPayment( $wallet, 'PAY123456', 5000.00, 'Top-up via Stripe', $verifier );
Batch Transfers
$result = Transaction::batch($source_wallet) ->credit('219111111111111', 100.00, 'Bonus') ->credit('219222222222222', 50.00, 'Refund') ->credit('219333333333333', 25.00, 'Cashback') ->execute(); // Result $result->batchId(); // '127123456789012' $result->totalAmount(); // 175.00 $result->creditCount(); // 3 $result->hashes(); // All tx hashes $result->creditHashes(); // Credit hashes only $result->debitHash(); // Single debit hash
Bulk Credits
$result = Transaction::batch($source) ->creditMany([ ['address' => '219111111111111', 'amount' => 100, 'narration' => 'Bonus'], ['address' => '219222222222222', 'amount' => 50, 'narration' => 'Refund'], ]) ->execute();
Query Transactions
// Find by hash $tx = Transaction::find('abc123...', $pdo); // Find by address $txs = Transaction::findByAddress('219123456789012', type: 'credit', limit: 50, db: $pdo); // Find by batch $txs = Transaction::findByBatch('127123456789012', $pdo);
Ledger System
File Structure
/.txhive/ledgers/USR123456/
├── 219123456789012.ledger # Current active
├── 219123456789012.2024-01.ledger # January archive
├── 219123456789012.2024-02.ledger # February archive
└── 219123456789012.checksum # Integrity hash
Direct Ledger Access
$ledger = $wallet->ledger(); // Read operations $ledger->balance(); $ledger->count(); $ledger->count('credit'); $ledger->totals(); $ledger->first(); $ledger->last(); $ledger->getTransaction('abc123'); $ledger->getTransactions('credit'); // Verification $ledger->verify(); // Archives $ledger->archives();
Recovery
// Rebuild ledger from database $transactions = Transaction::findByAddress($address, db: $pdo); $wallet->ledger()->rebuild($transactions);
Payment Integration
Implement Custom Verifier
use TimeFrontiers\Wallet\Payment\PaymentVerifierInterface; class StripePaymentVerifier implements PaymentVerifierInterface { public function verify(string $reference, float $amount, string $currency): bool { $payment = \Stripe\PaymentIntent::retrieve($reference); return $payment->status === 'succeeded' && $payment->amount >= $amount * 100 && strtoupper($payment->currency) === $currency; } public function availableBalance(string $reference): float { // Return available balance } public function markSpent(string $reference, float $amount, string $tx_hash): bool { // Record the claim } public function getPayment(string $reference): ?array { // Return payment details } public function isValidReference(string $reference): bool { return preg_match('/^pi_[a-zA-Z0-9]+$/', $reference); } }
Error Handling
use TimeFrontiers\Wallet\Exception\{ WalletException, WalletNotFoundException, InsufficientBalanceException, IntegrityException, PaymentVerificationException, TransactionException }; try { Transaction::transfer($from, $to, 1000000, 'Big payment'); } catch (InsufficientBalanceException $e) { echo "Need {$e->required()}, have {$e->available()}"; echo "Shortfall: {$e->shortfall()}"; } catch (IntegrityException $e) { echo "Ledger: {$e->ledgerBalance()}, DB: {$e->dbBalance()}"; } catch (PaymentVerificationException $e) { echo "Payment {$e->reference()} failed"; }
Security
Why File Ledger is More Secure
| Aspect | File Ledger | Database |
|---|---|---|
| Attack Surface | Server access only | SQL injection, leaked creds |
| Remote Tampering | Requires server access | Multiple vectors |
| Access Control | OS permissions | App + DB users |
| Detection | Checksums | Requires audit logs |
Best Practices
- Store ledgers outside web root:
/var/data/.txhive/ - Restrict file permissions:
chmod 600on ledger files - Use strict integrity mode: Fail on mismatch
- Regular backups: Archive ledger files
- Monitor alerts table: Process pending notifications