yannxtrem / katchika
Package Laravel pour intégrer SHKeeper Crypto Multi-Wallet
Requires
- php: ^8.1
- illuminate/database: ^12.0
- illuminate/events: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- orchestra/testbench: ^10.11
- phpunit/phpunit: ^11.5.3
README
A robust, production-ready Laravel package that seamlessly integrates SHKeeper as a multi-wallet backend for processing non-custodial cryptocurrency payments. Designed for SaaS platforms that need to accept crypto without KYC requirements while managing multiple user wallets, commissions, and payment workflows.
Table of Contents
- Features
- Requirements
- Installation
- Configuration
- Usage
- Architecture
- API Reference
- Security
- Testing
- Events
- Extending
- Troubleshooting
Features
✨ Multi-Wallet Management - Create and manage system-level wallets per crypto for SHKeeper integration
✨ Webhook Integration - Handle SHKeeper payment confirmations via secure HMAC-verified webhooks
✨ Transaction Tracking - Monitor all payment lifecycle states (pending → paid → confirmed)
✨ Commission Handling - Built-in support for platform fee deductions via SHKeeper or post-processing
✨ Event-Driven Architecture - Extensible event system for custom business logic integration
✨ Dashboard Components - Pre-built Livewire/Blade components for user dashboards
✨ Status API - Public API endpoint for real-time transaction status polling
✨ Offline-First Testing - Full test suite runs without SHKeeper instance
✨ Zero KYC - Non-custodial architecture; SHKeeper handles blockchain directly
Requirements
- PHP 8.1+
- Laravel 10.0+
- SQLite, MySQL, PostgreSQL, or compatible database
- SHKeeper instance (for production)
Optional Dependencies
- Orchestra/Testbench 8.0+ (for development/testing)
Installation
1. Composer Installation
composer require yannxtrem/katchika
Local installation without Packagist
If you want to install this package from a local Laravel project without publishing it on Packagist, use Composer's path repository feature.
In your Laravel application's composer.json, add:
"repositories": [ { "type": "path", "url": "../katchika" } ],
Then require the package locally:
composer require yannxtrem/katchika:@dev --prefer-source
The package source will be symlinked into your project and can be developed locally without Packagist.
2. Publish Configuration
php artisan vendor:publish --provider="Katchika\KatchikaServiceProvider" --tag=config
This creates config/shkeeper.php in your Laravel application.
3. Run Migrations
php artisan migrate
Creates two tables:
shkeeper_wallets- Stores wallet metadata and API keysshkeeper_transactions- Tracks payment transactions
4. Environment Configuration
Add to your .env:
SHKEEPER_API_URL=https://api.shkeeper.example.com SHKEEPER_API_KEY=fs9GYTyNdkjPHX36 SHKEEPER_WEBHOOK_SECRET=your_webhook_secret
Configuration
Edit config/shkeeper.php to customize the package behavior:
return [ // SHKeeper API endpoint 'api_base_url' => env('SHKEEPER_API_URL', 'https://api.shkeeper.example.com'), // Admin API key for wallet operations 'api_key' => env('SHKEEPER_API_KEY', null), // Secret for webhook signature verification 'webhook_secret' => env('SHKEEPER_WEBHOOK_SECRET', null), // Whitelist allowed IPs for webhook (empty = allow all) 'webhook_ips' => [], // Cache TTL for exchange rates (seconds) 'cache_ttl' => 300, // Enable/disable dashboard routes 'enable_dashboard' => true, // Enable/disable webhook route 'enable_routes' => true, // Default platform commission (5%) 'default_platform_fee' => 0.05, ];
Usage
Core: MultiWallet Service
The MultiWalletService manages the lifecycle of system-level wallets per crypto.
Creating a Wallet
use Katchika\Services\MultiWalletService; $service = new MultiWalletService(); // Create a BTC wallet with 3% fee for the application system $wallet = $service->createWalletForCrypto('BTC', 0.03); // Response // { // "id": 1, // "crypto": "BTC", // "api_key": "sk_live_xxx", // "address": null, // "platform_fee": 0.03, // "created_at": "2026-03-22T10:00:00Z" // }
Retrieving a Wallet
$wallet = $service->getWalletForCrypto('BTC'); if ($wallet) { echo $wallet->address; echo $wallet->api_key; }
Deleting a Wallet
$service->deleteWallet($wallet);
Core: Transaction Manager
The TransactionManager handles payment requests and status updates. Transaction ownership is optional and stored on the transaction record, not on the system wallet.
Creating a Payment Request
use Katchika\Services\TransactionManager; use Katchika\Models\Wallet; $manager = new TransactionManager(); $wallet = Wallet::find(1); $transaction = $manager->createPaymentRequest( wallet: $wallet, fiatAmount: 100.00, cryptoAmount: 0.0025, externalId: 'order-12345', followerId: 123, ownerType: 'App\\Models\\User', // Optional ownerId: 42, // Optional expiresAt: now()->addHour() ); // Response // { // "id": 42, // "wallet_id": 1, // "external_id": "order-12345", // "follower_id": 123, // "crypto": "BTC", // "fiat_amount": 100.00, // "crypto_amount": 0.0025, // "wallet_address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", // "status": "pending", // "expires_at": "2026-03-22T11:00:00Z", // "created_at": "2026-03-22T10:00:00Z" // }
Updating Transaction Status
Called automatically by the webhook, but can also be invoked manually:
$transaction = Transaction::find(42); $transaction = $manager->getTransactionStatus($transaction, [ 'status' => 'paid', 'txid' => 'abc123def456...', 'paid_at' => now()->toDateTimeString(), ]);
SHKeeper Request / Response Object Model
The package provides typed request and response classes for SHKeeper API calls.
Create a Payment Request using objects
use Katchika\Services\ShkeeperApiClient; use Katchika\Services\Requests\CreatePaymentRequest; $client = app(ShkeeperApiClient::class); $request = new CreatePaymentRequest( crypto: 'ETH', externalId: 'order-999', fiat: 'USD', amount: '25.00', callbackUrl: 'https://example.com/callback', extra: [ 'customer_id' => 555 ] ); $response = $client->send($request); if ($response->isSuccess()) { echo $response->externalId(); echo $response->cryptoAmount(); echo $response->walletAddress(); }
API error handling
ShkeeperApiClient::send() throws Katchika\Services\Exceptions\ShkeeperApiException when the remote call fails or returns an error.
use Katchika\Services\Exceptions\ShkeeperApiException; try { $response = $client->send($request); } catch (ShkeeperApiException $exception) { logger()->error($exception->getMessage(), [ 'status' => $exception->httpStatus(), 'body' => $exception->body(), ]); }
Models
Wallet Model
use Katchika\Models\Wallet; $wallet = Wallet::find(1); // Relations $wallet->transactions; // All transactions for this wallet // Attributes $wallet->crypto; // Currency code (BTC, ETH, USDT, etc.) $wallet->address; // Blockchain address $wallet->platform_fee; // Commission percentage as decimal (0.05 = 5%) $wallet->api_key; // SHKeeper API key (encrypted)
Transaction Model
use Katchika\Models\Transaction; $txn = Transaction::find(42); // Relations $txn->wallet; // Parent wallet // Key Attributes $txn->external_id; // Your application reference ID $txn->follower_id; // Optional customer/follower ID $txn->status; // 'pending', 'paid', 'expired', 'failed' $txn->txid; // Blockchain transaction hash (after payment) $txn->crypto_amount; // Amount in cryptocurrency $txn->fiat_amount; // Fiat equivalent (for display) $txn->wallet_address; // Where customer sends funds $txn->paid_at; // When payment was confirmed $txn->expires_at; // When payment offer expires
Routes & API Endpoints
Webhook Endpoint (Auto-Registered)
POST /shkeeper/webhook/{walletId}
Receives payment confirmations from SHKeeper.
Headers:
X-SHKEEPER-SIGNATURE(if configured): HMAC-SHA256 signature of request body
Request Body:
{
"external_id": "order-12345",
"status": "paid",
"txid": "abc123def456...",
"paid_at": "2026-03-22T10:05:00Z"
}
Response:
{
"message": "Accepted"
}
Status Codes:
202 Accepted- Webhook processed successfully401 Unauthorized- Invalid signature403 Forbidden- IP not whitelisted404 Not Found- Wallet or transaction not found400 Bad Request- Missing required fields
Transaction Status API
GET /api/shkeeper/status/{externalId}
Returns real-time transaction status (unauthenticated).
Response:
{
"id": 42,
"status": "paid",
"crypto_amount": 0.0025,
"wallet_address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"txid": "abc123def456...",
"paid_at": "2026-03-22T10:05:00Z"
}
Dashboard Routes (Authenticated)
These routes require the auth middleware and return JSON or Blade views.
- GET
/dashboard/shkeeper/- User's wallets overview - GET
/dashboard/shkeeper/transactions- Transaction history (paginated) - GET
/dashboard/shkeeper/wallet/{walletId}- Wallet details and balance
Architecture
Database Schema
shkeeper_wallets Table
| Column | Type | Notes |
|---|---|---|
| id | BIGINT | Primary key |
| owner_type | VARCHAR | Polymorphic owner class (e.g., App\Models\User) |
| owner_id | BIGINT | Polymorphic owner ID |
| crypto | VARCHAR(12) | Currency code (BTC, ETH, USDT, etc.) |
| api_key | VARCHAR | Encrypted SHKeeper API key |
| address | VARCHAR | Blockchain address (null until wallet activated) |
| platform_fee | DECIMAL(8,4) | Commission as decimal (0.05 = 5%) |
| created_at | TIMESTAMP | Creation time |
| updated_at | TIMESTAMP | Last update time |
shkeeper_transactions Table
| Column | Type | Notes |
|---|---|---|
| id | BIGINT | Primary key |
| wallet_id | BIGINT | Foreign key to shkeeper_wallets |
| external_id | VARCHAR | Unique reference from your app |
| follower_id | BIGINT | Nullable follower/customer reference |
| crypto | VARCHAR(12) | Currency code |
| fiat_amount | DECIMAL(20,8) | Fiat value (for display/reporting) |
| crypto_amount | DECIMAL(30,18) | Cryptographic value |
| wallet_address | VARCHAR | Payment destination address |
| status | VARCHAR | 'pending', 'paid', 'expired', 'failed' |
| txid | VARCHAR | Blockchain transaction hash |
| paid_at | TIMESTAMP | Confirmation timestamp |
| expires_at | TIMESTAMP | Offer expiration |
| raw_webhook_payload | JSON | Full SHKeeper webhook data |
| created_at | TIMESTAMP | Creation time |
| updated_at | TIMESTAMP | Last update time |
Data Flow
┌──────────────────┐
│ SaaS Frontend │
└────────┬─────────┘
│
│ 1. Create payment request
▼
┌──────────────────────────────────┐
│ Your Laravel Application │
│ (using Katchika services) │
└────────┬────────────────────────┘
│
│ 2. Call TransactionManager
▼
┌──────────────────────────────────┐
│ Katchika Package │
│ - Services │
│ - Models │
│ - Events │
└────────┬────────────────────────┘
│
│ 3. Store locally (DB)
│ 4. Call SHKeeper API (optional)
▼
┌──────────────────────────────────┐
│ SHKeeper │
│ (multi-wallet blockchain manager)│
└────────┬────────────────────────┘
│
│ 5. Monitor blockchain
│ 6. Generate address
│ 7. Watch for payment
▼
┌──────────────────────────────────┐
│ Blockchain (BTC, ETH, etc.) │
└────────┬────────────────────────┘
│
│ Customer sends crypto
│
│ 8. Transaction confirmed
│ 9. Webhook callback
▼
┌──────────────────────────────────┐
│ Katchika Webhook Handler │
│ POST /shkeeper/webhook/{walletId}│
└────────┬────────────────────────┘
│
│ 10. Verify signature
│ 11. Update transaction status
│ 12. Dispatch events
▼
┌──────────────────────────────────┐
│ Your Event Listeners │
│ (activate subscription, etc.) │
└──────────────────────────────────┘
API Reference
Service Classes
MultiWalletService
namespace Katchika\Services; class MultiWalletService { public function createWalletForOwner( $owner, // Ownable model instance string $crypto, // Currency code float $platformFee = null // Commission (or use default) ): Wallet; public function getWalletForOwner( $owner, string $crypto ): ?Wallet; public function deleteWallet(Wallet $wallet): bool; }
TransactionManager
namespace Katchika\Services; class TransactionManager { public function createPaymentRequest( Wallet $wallet, float $fiatAmount, float $cryptoAmount, string $externalId, ?int $followerId = null, ?\DateTime $expiresAt = null ): Transaction; public function getTransactionStatus( Transaction $transaction, array $shkeeperPayload ): Transaction; }
Model Scopes
// Filter transactions by status Transaction::where('status', 'paid')->get(); // Get pending transactions Transaction::where('status', 'pending')->get(); // Transactions for a specific wallet $wallet = Wallet::find(1); $wallet->transactions()->get(); // Polymorphic owner lookup Wallet::where('owner_type', User::class) ->where('owner_id', $userId) ->get();
Security
Best Practices Implemented
✅ Webhook Verification - HMAC-SHA256 signature validation (optional, recommended)
✅ IP Whitelisting - Restrict webhook sources by IP address
✅ API Key Encryption - Private keys encrypted at rest using Laravel's encryption
✅ Authentication - Dashboard routes protected with auth middleware
✅ HTTPS - All API communication should use HTTPS in production
✅ Rate Limiting - Webhook endpoints should be rate-limited
✅ Input Validation - All external inputs validated before processing
Webhook Security Setup
// config/shkeeper.php return [ 'webhook_secret' => env('SHKEEPER_WEBHOOK_SECRET', null), // Restrict to SHKeeper IPs 'webhook_ips' => [ '203.0.113.0', // SHKeeper primary '203.0.113.1', // SHKeeper secondary ], ];
Protecting API Keys
The package automatically encrypts sensitive fields when stored. In your application, never:
- Log API keys to console
- Expose keys in error messages
- Transmit keys over HTTP
- Store unencrypted backups containing keys
Firewall Configuration (Production)
# nginx.conf location /shkeeper/webhook/ { # Only allow SHKeeper IP allow 203.0.113.0; deny all; }
Testing
Running Tests
cd katchika # All tests vendor/bin/phpunit --no-coverage # Specific test class vendor/bin/phpunit tests/MultiWalletServiceTest.php --no-coverage # With coverage vendor/bin/phpunit
Test Coverage
- Unit Tests: Service methods, model relations
- Integration Tests: Webhook processing, event dispatch
- Database Tests: Transaction lifecycle, persistence
Demo Server Testing
To test this package against the official SHKeeper demo server both locally and directly, export your demo credentials and run the sample demo script:
export SHKEEPER_API_URL=https://demo.shkeeper.io export SHKEEPER_API_KEY=your_demo_api_key composer demo
This runs demo/demo.php and performs a real API call to the demo server:
/api/v1/cryptoto list supported currencies/api/v1/ETH/quoteto fetch a quote for 1 USD
If you want to keep using the demo server in automated tests, use:
composer test:integration
If SHKEEPER_API_KEY is not provided, the demo script will fail clearly and the integration tests will skip automatically.
Writing Tests
Tests inherit from Katchika\Tests\TestCase, which auto-bootstraps:
- In-memory SQLite database
- Package service provider
- Migrations
Example:
namespace Katchika\Tests; use Katchika\Models\Wallet; use Katchika\Services\MultiWalletService; class YourTest extends TestCase { public function test_example() { $service = new MultiWalletService(); $wallet = $service->createWalletForOwner($owner, 'BTC'); $this->assertNotNull($wallet->id); } }
Events
The package dispatches typed events to your application's event bus.
PaymentRequestCreated
Fired when a new payment request is created.
use Katchika\Events\PaymentRequestCreated; class PaymentRequestCreatedListener { public function handle(PaymentRequestCreated $event) { $event->transaction; // Transaction model // Send payment link to customer // Log request // Update inventory } }
Register in app/Providers/EventServiceProvider.php:
protected $listen = [ PaymentRequestCreated::class => [ PaymentRequestCreatedListener::class, ], ];
PaymentConfirmed
Fired when payment is detected on the blockchain and confirmed.
use Katchika\Events\PaymentConfirmed; class PaymentConfirmedListener { public function handle(PaymentConfirmed $event) { // Activate subscription // Process order // Send notification // Update ledger } }
PaymentExpired
Fired when a payment request expires without receiving funds.
use Katchika\Events\PaymentExpired; class PaymentExpiredListener { public function handle(PaymentExpired $event) { // Notify user // Clean up resources // Log failure } }
Extending
Custom Event Listeners
Listen for events and implement your business logic:
namespace App\Listeners; use Katchika\Events\PaymentConfirmed; use App\Models\Subscription; class ActivateSubscriptionOnPayment { public function handle(PaymentConfirmed $event) { $transaction = $event->transaction; if ($transaction->follower_id) { Subscription::create([ 'user_id' => $transaction->follower_id, 'tier' => 'premium', 'expires_at' => now()->addMonth(), ]); } } }
Custom Commission Logic
Option 1: SHKeeper native (recommended)
$service->createWalletForOwner($user, 'BTC', 0.05); // 5% via SHKeeper
Option 2: Post-processing in listener
class CalculateCommission { public function handle(PaymentConfirmed $event) { $txn = $event->transaction; $fee = $txn->crypto_amount * $txn->wallet->platform_fee; // Transfer fee to master wallet // Or create accounting entry } }
Polymorphic Owners
Wallets support any Eloquent model via polymorphic relations:
// User wallet $user = User::find(1); $wallet = $service->createWalletForOwner($user, 'BTC'); // Organization wallet $org = Organization::find(5); $wallet = $service->createWalletForOwner($org, 'ETH'); // Team wallet $team = Team::find(99); $wallet = $service->createWalletForOwner($team, 'USDT'); // Query any owner's wallets $wallet->owner; // Returns User, Organization, or Team
Troubleshooting
Common Issues
"Wallet not found" on webhook
Cause: Webhook uses wallet ID, but wallet may not exist or has been deleted.
Solution: Verify wallet exists before creating payment request.
$wallet = Wallet::find($walletId); if (!$wallet) { throw new \Exception('Wallet does not exist'); }
Signature validation fails
Cause: Webhook secret mismatch or body encoding issue.
Solution: Ensure exact matching:
- SHKeeper's webhook secret matches
SHKEEPER_WEBHOOK_SECRET - Signature uses raw request body (not pretty-printed JSON)
- Hash algorithm is SHA256
Transaction doesn't update from webhook
Cause: Webhook IP not whitelisted or external_id mismatch.
Solution:
# Debug webhook tail -f storage/logs/laravel.log | grep "webhook" # Check IP whitelist php artisan tinker >>> config('shkeeper.webhook_ips')
API key is empty
Cause: Key wasn't populated when wallet created; SHKeeper API not called.
Solution: Ensure SHKeeper API credentials are configured:
config('shkeeper.api_key') // Should be set config('shkeeper.api_base_url') // Should be valid
Performance Optimization
Database Indexing
Recommended indexes for production:
// In a migration Schema::table('shkeeper_transactions', function (Blueprint $table) { $table->index('external_id'); $table->index('status'); $table->index('wallet_id'); $table->index('created_at'); }); Schema::table('shkeeper_wallets', function (Blueprint $table) { $table->index(['owner_type', 'owner_id']); $table->index('crypto'); });
Caching
Cache transaction lookups:
$transaction = cache()->remember( "transaction.{$externalId}", config('shkeeper.cache_ttl'), fn() => Transaction::where('external_id', $externalId)->first() );
Async Webhooks
Process webhooks asynchronously (recommended for high volume):
dispatch( new ProcessWebhook($transaction, $payload) )->onQueue('shkeeper');
Roadmap
- v0.2.0: SHKeeper API client integration
- v0.3.0: QR code generation for payment display
- v0.4.0: Multi-currency rate conversion
- v0.5.0: Dashboard UI templates (Livewire)
- v1.0.0: Stable release with full documentation
- Future: Rate limiting, webhook retry logic, notification channels
Support & Community
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@vsys-host.com
License
MIT License - See LICENSE file for details.
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure tests pass (
vendor/bin/phpunit) - Commit changes (
git commit -am 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
Changelog
v0.1.0 (2026-03-22)
- Initial release
- Core services: MultiWalletService, TransactionManager
- Models: Wallet, Transaction
- Webhook handling with HMAC verification
- Event system (PaymentRequestCreated, PaymentConfirmed, PaymentExpired)
- Dashboard API endpoints
- Comprehensive test suite
- Documentation and README
Built with ❤️ for the crypto community.
Last Updated: March 22, 2026