yannxtrem/katchika

Package Laravel pour intégrer SHKeeper Crypto Multi-Wallet

Maintainers

Package info

github.com/yannXtrem/katchika

pkg:composer/yannxtrem/katchika

Statistics

Installs: 28

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-04-09 16:03 UTC

This package is auto-updated.

Last update: 2026-04-10 08:34:11 UTC


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.

PHP Laravel License

Table of Contents

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 keys
  • shkeeper_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 successfully
  • 401 Unauthorized - Invalid signature
  • 403 Forbidden - IP not whitelisted
  • 404 Not Found - Wallet or transaction not found
  • 400 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/crypto to list supported currencies
  • /api/v1/ETH/quote to 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:

  1. SHKeeper's webhook secret matches SHKEEPER_WEBHOOK_SECRET
  2. Signature uses raw request body (not pretty-printed JSON)
  3. 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

License

MIT License - See LICENSE file for details.

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for new functionality
  4. Ensure tests pass (vendor/bin/phpunit)
  5. Commit changes (git commit -am 'Add amazing feature')
  6. Push to branch (git push origin feature/amazing-feature)
  7. 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