Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Blockchain Context Bundle Laravel Package

amashukov/blockchain-context-bundle

Symfony 7 bundle for crypto payments that autowires pure-PHP TON and EVM SDKs into typed RPC clients, plus signature verification, AES-256-GCM key encryption, and per-chain finality, detection, gas, and tx-builder services.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to internalise is that this is a Symfony bundle, not a Laravel package. There is no BlockchainContextServiceProvider, no auto-discovery, no config/blockchain_context.php publish. In Laravel you have two viable paths:

  1. Bypass the bundle, use the SDKs directly. amashukov/eth-php, amashukov/ton-php, amashukov/eth-rpc-client-php, amashukov/toncenter-client-php are all framework-agnostic pure-PHP libraries and are where 90% of the day-to-day value lives. The bundle's "value add" is DI wiring + tagged chains, which you must recreate anyway.
  2. Bridge the bundle into Laravel's container by hand-registering the concrete services it exposes (SignatureVerifier, PrivKeyEncrypter, RPC clients, signers, per-chain Gas/TxBuilder/Finality classes). The bundle's own services.yaml/services.php and DependencyInjection/Configuration.php are the reference for how each service is constructed.

Minimal setup:

composer require amashukov/blockchain-context-bundle

Ensure host extensions (the bundle hard-requires them on PHP 8.3+):

ext-gmp, ext-sodium, ext-openssl, ext-bcmath

Add env vars and mirror the bundle's config tree in config/blockchain_context.php:

// config/blockchain_context.php
return [
    'eth' => [
        'enabled'             => (bool) env('ETH_ENABLED', true),
        'rpc_url'             => env('ETH_RPC_URL'),
        'chain_id'            => (int) env('BRIDGE_ETH_CHAIN_ID', 1),
        'wallet_private_key'  => env('BRIDGE_ETH_WALLET_PRIVATE_KEY'),
        'usdt_token_address'  => env('USDT_TOKEN_ADDRESS'),
        'explorer'            => env('BRIDGE_ETH_EXPLORER', 'https://etherscan.io'),
    ],
    'ton' => [
        'enabled'            => (bool) env('TON_ENABLED', true),
        'toncenter_api_key'  => env('TONCENTER_API_KEY'),
        'wallet_mnemonic'    => env('BRIDGE_TON_WALLET_MNEMONIC'),
        'bridge_contract'    => env('BRIDGE_TON_CONTRACT'),
        'finality_polls'     => (int) env('TON_FINALITY_POLLS', 0),
        'explorer'           => env('TON_EXPLORER', 'https://tonscan.org'),
    ],
    'deposit_wallet_encryption_key' => env('DEPOSIT_WALLET_ENCRYPTION_KEY'),
];

First use case to prove the wiring works: signature verification. It's stateless, needs no chain, and validates the bundle is usable from your container:

// app/Services/Crypto/SignatureService.php
namespace App\Services\Crypto;

use Amashukov\BlockchainContextBundle\Service\SignatureVerifier;

final class SignatureService
{
    public function __construct(private SignatureVerifier $verifier) {}

    public function verifyEvm(string $message, string $sig, string $address): bool
    {
        return $this->verifier->verifyEip191($message, $sig, $address);
    }

    public function verifyTon(string $message, string $sig, string $address): bool
    {
        return $this->verifier->verifyTonConnect($message, $sig, $address);
    }
}

Bind it in a service provider (see next section for the full pattern). Where to look first: the bundle's src/Resources/config/services*.yaml (definitions), DependencyInjection/Configuration.php (the exact tree), and Service/SignatureVerifier.php + Service/PrivKeyEncrypter.php (the two leaf services that never need a live RPC).

Implementation Patterns

Pattern 1 — A single BlockchainContextServiceProvider that mirrors the bundle's autowiring

Register everything as singletons so the RPC clients are reused (important under Octane). Bind the SDK interfaces, not the bundle classes, so tests can swap fakes:

// app/Providers/BlockchainContextServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Amashukov\EthRpc\JsonRpcProvider;
use Amashukov\EthRpc\JsonRpcProviderInterface;
use Amashukov\Toncenter\ToncenterClient;
use Amashukov\Toncenter\ToncenterClientInterface;
use Amashukov\BlockchainContextBundle\Service\SignatureVerifier;
use Amashukov\BlockchainContextBundle\Service\PrivKeyEncrypter;

final class BlockchainContextServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(JsonRpcProviderInterface::class, fn () =>
            new JsonRpcProvider(
                $this->app->make(\Psr\Http\Client\ClientInterface::class),
                config('blockchain_context.eth.rpc_url'),
                (int) config('blockchain_context.eth.chain_id'),
            )
        );

        $this->app->singleton(ToncenterClientInterface::class, fn () =>
            new ToncenterClient(
                $this->app->make(\Psr\Http\Client\ClientInterface::class),
                (string) config('blockchain_context.ton.toncenter_api_key'),
            )
        );

        $this->app->bind(SignatureVerifier::class, fn () => new SignatureVerifier());
        $this->app->bind(PrivKeyEncrypter::class, fn () =>
            new PrivKeyEncrypter(config('blockchain_context.deposit_wallet_encryption_key'))
        );
    }
}

Always confirm the concrete constructor signatures against the bundle's services.yaml — the README shows injection, not construction, so the argument order for JsonRpcProvider, ToncenterClient, Eip1559Signer, and WalletV4R2 must be read from the bundle source before you hardcode these closures.

Pattern 2 — Replace the tagged-iterator chains with an explicit registrar

The bundle's ChainDepositCheckChain, ConfirmationCheckChain, ConfirmationCounterRegistry, and DepositTxBuilderChain are Symfony tagged_iterator magic over blockchain_context.* tags. Laravel's container can do tagged bindings ($this->app->tag([...], 'deposit.checkers') + $this->app->tagged('deposit.checkers')), but for the money-handling paths, an explicit manager is clearer and easier to unit-test:

namespace App\Services\Crypto\Deposits;

use Amashukov\BlockchainContextBundle\Service\Detection\ChainDepositCheckChain;

final class DepositDetectionManager
{
    /** @param iterable<\App\Contracts\DepositChecker> $checkers */
    public function __construct(private iterable $checkers) {}

    public function runAll(): array
    {
        $out = [];
        foreach ($this->checkers as $checker) {
            $out[$checker->getChain() ?? $checker::class] = $checker->check();
        }
        return $out;
    }
}

Register with tags:

$this->app->singleton(DepositDetectionManager::class, fn ($app) =>
    new DepositDetectionManager($app->tagged('blockchain.deposit.checkers'))
);
$this->app->tag([\App\Crypto\Checks\EthErc20DepositChecker::class,
                 \App\Crypto\Checks\TonJettonDepositChecker::class],
                'blockchain.deposit.checkers');

Pattern 3 — Deposit detection as a scheduled, idempotent job

Chain scanning is long-lived, rate-limited, and must be idempotent. Put each chain's check in a queued job and persist a cursor:

public function handle(ToncenterClientInterface $ton, PrivKeyEncrypter $enc): void
{
    $cursor = DepositCursor::where('chain', 'ton')->firstOrFail();
    foreach ($ton->getTransactions($cursor->last_lt) as $tx) {
        if ($tx->to !== config('blockchain_context.ton.bridge_contract')) continue;

        Deposit::updateOrCreate(
            ['chain' => 'ton', 'tx_hash' => $tx->hash],
            ['amount' => $tx->value, 'status' => 'detected'],
        );
    }
    $cursor->update(['last_lt' => $lastSeen]);
}

Schedule per-chain with ->withoutOverlapping() and a rate-limit-aware delay; the bundle's toncenter client is built to honor 429/5xx/542 via its retry middleware, but your job loop still needs back-off.

Pattern 4 — Signature verification at API boundaries

Use SignatureVerifier in a middleware or form-request for "connect wallet" flows, and store the message + address you hashed so replays are auditable. For EVM this is EIP-191 personal_sign (secp256k1 + Keccak-256); for TON it's Ed25519 TON Connect. Keep the raw message, never re-serialize before verifying (whitespace/JSON ordering breaks personal_sign).

Pattern 5 — Key encryption as a thin vault

PrivKeyEncrypter is AES-256-GCM with associated authentication. Use it as the only path to persisted key material:

$enc->encrypt($derivedPrivateKeyHex);   // store ciphertext in DB column
$enc->decrypt($row->encrypted_key);     // only in the signing job, never logged

The DepositWalletDeriverInterface port is the extension point: implement it in your app (HD derivation is your business), and consume DerivedWallet/DepositEvidence value objects. Derivation should happen in the encrypted job, not the HTTP request.

Pattern 6 — Compose gas and tx-builder per chain, not globally

The bundle's EthGasFetcher, TonGasFetcher, ZeroGasFetcher, and the per-chain DepositTxBuilder implementations are separated for a reason. Resolve the gas fetcher by chain at the call site (a small GasFetcherResolver), and never assume ZeroGasFetcher semantics in fee display — it exists for chains that don't charge gas on deposit.

Pattern 7 — Bitcoin evidence (v0.7.2+) for fee analytics

$evidence = $this->btcEvidence->read($txid, $blockHash);
if ($evidence->hasProvenFee()) {
    $fee = $evidence->getFee(); // reliable
} else {
    // inputs unavailable -> fee cannot be computed. Do not fall back to getFee() blindly.
}

Persist hasProvenFee() alongside the fee — the reader deliberately returns null rather than guess, and refuses contradictory reported fees.

Gotchas and Tips

  • The bundle's config is %blockchain_context.*% parameters, not Laravel config. Symfony's Configuration.php reads container parameters. In Laravel, either set those parameters in a Container shim, or skip the bundle's Configuration entirely and pass values directly to constructors. Do not trust config('blockchain_context...') to be visible to the bundle's own code — it isn't.
  • %env(int:FOO)% is Symfony's processor syntax. In Laravel use (int) env('FOO', 0). Mixing the two gives you 0 where you expected a chain ID.
  • enabled defaults to true. A single-chain deployment must explicitly set the other chain to false, or you'll get a container full of clients pointed at null RPC URLs and cryptic 400s on first call.
  • Missing autowiring on the bundle's own interfaces. The SDK interfaces (JsonRpcProviderInterface, ToncenterClientInterface) are the safe targets to bind. The bundle's internal service classes (ChainDepositCheckChain, DepthPollingFinalityVerifier, etc.) are concrete, often constructor-heavy, and expect tagged iterables; bind them by hand or instantiate directly rather than expecting Laravel to resolve them.
  • Tagged iterators do not exist for you. If a chain class takes iterable $checkers, Laravel won't magically find them. Use app()->tag() / app()->tagged() (Laravel 8.19+) or an explicit list. This is the single most common reason a "working in Symfony" example fails in Laravel.
  • deposit_wallet_encryption_key must be base64 of exactly 32 bytes. Generate with openssl rand -base64 32. A hex key, or a 32-character ASCII string, passes env parsing but fails AES-256-GCM init — and the error is on the first decrypt, not at boot.
  • The PSR-18 client is the choke point. The bundle wires amashukov/http-client-php (a cURL client). If you substitute a mocked PSR-18 client in tests, the toncenter retry/X-Api-Key middleware pipeline is bypassed — test the middleware separately or inject the real pipeline.
  • Rate limits are real without TONCENTER_API_KEY. toncenter is aggressively throttled for anonymous callers; the bundle's 429/5xx/542 retry is a safety net, not a license to poll tightly. Set finality_polls: 0 to skip depth re-polling when you don't need it — it saves the majority of TON RPC load.
  • Finality is polling-based (DepthPollingFinalityVerifier). Don't hold a request open waiting for confirmations — persist ConfirmationCounterRegistry state in a job that advances counts and emits an event when depth is reached. Treat confirmations as a workflow state, not a synchronous check.
  • SignatureVerifier returns booleans and does not tell you why it failed (malformed sig vs wrong address vs bad recovery ID). Wrap it in your own exception with the recovered address for logs; do not pass the raw signature into logs (it's sometimes treated as a bearer for one-shot flows).
  • Never log private keys, mnemonics, or decrypted key material. PrivKeyEncrypter protects at rest, not from your Log::debug — add a sanitizer or use a dedicated logger channel for the crypto services.
  • Bitcoin fees can be legitimately unavailable. hasProvenFee() === false means missing prevouts, not zero. If you display getFee() without checking, you'll show null/0 for a real transaction.
  • Version pin tightly. Zero stars, zero dependents, last release labelled for the future (2026-10-13). Pin an exact version in composer.json, vendor the code if you're going to production, and read the services.yaml for the version you installed — the README and the actual constructor signatures drift.
  • Under Octane, singletons persist between requests. RPC clients, signers, and PrivKeyEncrypter hold no per-request state and are safe to keep; anything that caches a nonce or a block cursor must be request-scoped or reset — resolve those with bind() not singleton().
  • The extension points you should actually use: DepositWalletDeriverInterface (your derivation logic), the per-chain DepositTxBuilder implementations (your tx assembly), and PrivKeyEncrypter (your at-rest storage). Everything else is plumbing that Laravel can own more cleanly than the Symfony container can.
  • Run composer why-not php ext-gmp early. ext-gmp in particular is absent on many PHP build images; you'll discover it as a Class 'GMP' not found deep inside signing, not at install time, if you skip --ignore-platform-req discipline.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin