amashukov / blockchain-context-bundle
Symfony bundle providing typed Bitcoin Core, TON and EVM integration services.
Package info
github.com/AndreyMashukov/blockchain-context-bundle
Type:symfony-bundle
pkg:composer/amashukov/blockchain-context-bundle
Requires
- php: >=8.3
- ext-gmp: *
- ext-openssl: *
- ext-sodium: *
- amashukov/bchn-rpc-client-php: ^0.2.0
- amashukov/eth-php: ^0.1.0
- amashukov/http-client-php: ^0.1.0
- amashukov/ton-php: ^0.1.0
- nyholm/psr7: ^1.8
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/cache: ^7.0
- symfony/config: ^7.0
- symfony/dependency-injection: ^7.0
- symfony/framework-bundle: ^7.0
- symfony/http-kernel: ^7.0
- symfony/yaml: ^7.0
Requires (Dev)
- amashukov/rector-php-rules: ^0.4.0
- friendsofphp/php-cs-fixer: ^3.50
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
- symfony/clock: ^7.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Symfony 7 bundle for crypto payments — provides typed Bitcoin Core, Bitcoin Cash Node, TON and EVM clients alongside signature, finality and transaction-building services.
amashukov/blockchain-context-bundle is a Symfony 7 bundle for crypto payments that exposes Bitcoin Core JSON-RPC together with the pure-PHP TON + EVM SDKs (amashukov/ton-php + amashukov/eth-php) through one autowired blockchain surface.
Features
- Autowired RPC clients — a typed
BitcoinRpcClient,Amashukov\EthRpc\JsonRpcProvider/EthRpcClient, andAmashukov\Toncenter\ToncenterClient, wired over PSR-18 transports. - Exact Bitcoin amounts — Bitcoin Core JSON numbers are preserved as decimal strings and converted to satoshis without floating-point arithmetic.
- Signing & wallet —
Amashukov\Eip1559TxSigner\Eip1559Signer(EIP-1559 offline signer) andAmashukov\TonWallet\WalletV4R2(built from a mnemonic via the bundle factory);ToncenterWalletRpcimplements the wallet's RPC port. - Per-chain domain services (host-app-agnostic, owning ports the host implements):
Detection\ChainDepositCheckChain,Finality\{ConfirmationCheckChain,ConfirmationCounterRegistry,DepthPollingFinalityVerifier},Gas\{EthGasFetcher,TonGasFetcher,ZeroGasFetcher},TxBuilder\DepositTxBuilderChain(+ per-chain TON / TON-Jetton / ETH / USDT-ERC20 builders),Explorer\DefaultExplorerUrl,Numeric\{BcDecimal,UuidIntCodec,UsdtJettonDecimals},Time\RealSleeper. SignatureVerifier— EIP-191 (personal_sign, secp256k1 ecrecover + Keccak-256) + Ed25519 (TON Connect) verification.PrivKeyEncrypter— AES-256-GCM authenticated encryption for keys at rest.DepositWalletDeriverInterfaceport +DerivedWallet/DepositEvidencevalue objects.- HD deposit wallets —
Service\Hd\EvmHdDeriver(BIP39 seed → BIP32 secp256k1,m/44'/60'/{account}'/0/{index}, lowercase0xaddress) andService\Hd\TonHdDeriver(BIP39 seed → SLIP-0010 ed25519,m/44'/607'/{account}'/0'/{index}', WalletV4R2 non-bounceableUQ…address) implementDepositWalletDeriverInterface. One mnemonic serves any number of separate address spaces through theaccountargument;TonHdDeriver::wallet($index)returns the signingWalletV4R2. The TON keys come from a BIP39 seed, not from a TON-native mnemonic, so wallet apps cannot import them from the phrase — recovery goes through the deriver. Pinned against the official BIP32 / SLIP-0010 / BIP39 vectors and against wallets derived by ethers and @ton/ton. - BIP39 mnemonics —
Service\Hd\Bip39Mnemonic::generate(int $words = 24)turns freshrandom_bytesentropy into a standard English mnemonic with its checksum,fromEntropy()does the same for given entropy, andisValid()checks the words and the checksum. The official 2048-word English list ships inResources/bip39/english.txt(SHA-2562f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda). Pinned against the official BIP39 test vectors. Feed the phrase toBip39Seed,EvmHdDeriverorTonHdDeriver. - Tagged-iterator chains —
ChainDepositCheckChain,ConfirmationCheckChain,ConfirmationCounterRegistry, andDepositTxBuilderChainauto-collect their members via bundle-namespaced tags (blockchain_context.*); drop a new impl and it joins the chain with no DI edits.
Why amashukov/blockchain-context-bundle
There is no maintained Symfony bundle for crypto-payment infrastructure — most projects glue raw RPC clients into services by hand. This bundle wires Bitcoin Core, TON and EVM services into Symfony's container with autowiring, env-agnostic config, and a tagged-iterator extension model.
Installation
composer require amashukov/blockchain-context-bundle
Register the bundle (Symfony Flex does this automatically):
// config/bundles.php return [ // ... Amashukov\BlockchainContextBundle\BlockchainContextBundle::class => ['all' => true], ];
Usage
Inject any autowired service directly:
use Amashukov\EthRpc\JsonRpcProviderInterface; use Amashukov\Toncenter\ToncenterClientInterface; use Amashukov\BlockchainContextBundle\Service\Bitcoin\BitcoinRpcClientInterface; use Amashukov\BlockchainContextBundle\Service\SignatureVerifier; final class SomeService { public function __construct( private JsonRpcProviderInterface $eth, private ToncenterClientInterface $ton, private BitcoinRpcClientInterface $bitcoin, private SignatureVerifier $verifier, ) {} }
Bitcoin transaction timestamps
Bitcoin wallet transaction DTOs expose optional blockTime (mined block timestamp) and timeReceived (wallet first receipt). Missing fields remain null; malformed, negative, fractional and overflowing timestamps fail decoding. These are separate evidence clocks, not an inferred payment timestamp. Existing constructor calls remain compatible.
Configuration
The bundle is env-agnostic: it exposes a config tree and reads only %blockchain_context.*% parameters internally. The host maps them to its own environment. Config is split per chain (bitcoin: / eth: / ton:), each toggleable via enabled:
# config/packages/blockchain_context.yaml blockchain_context: bitcoin: enabled: true rpc_url: '%env(BITCOIN_RPC_URL)%' rpc_user: '%env(BITCOIN_RPC_USER)%' rpc_password: '%env(BITCOIN_RPC_PASSWORD)%' timeout_seconds: 30 explorer: '%env(BITCOIN_EXPLORER)%' # default https://mempool.space; '' disables links (regtest) eth: enabled: true rpc_url: '%env(ETH_RPC_URL)%' # EVM JSON-RPC endpoint chain_id: '%env(int:BRIDGE_ETH_CHAIN_ID)%' wallet_private_key:'%env(BRIDGE_ETH_WALLET_PRIVATE_KEY)%' usdt_token_address:'%env(USDT_TOKEN_ADDRESS)%' # USDT-ERC20 deposits explorer: '%env(BRIDGE_ETH_EXPLORER)%' # default https://etherscan.io ton: enabled: true toncenter_api_key: '%env(TONCENTER_API_KEY)%' # optional — lifts toncenter rate limit wallet_mnemonic: '%env(BRIDGE_TON_WALLET_MNEMONIC)%' bridge_contract: '%env(BRIDGE_TON_CONTRACT)%' # USDT-Jetton deposits finality_polls: '%env(int:TON_FINALITY_POLLS)%' # 0 = skip depth re-poll timeout_seconds: '%env(int:TON_HTTP_TIMEOUT_SECONDS)%' # toncenter response timeout, default 60 connect_timeout_seconds: '%env(int:TON_HTTP_CONNECT_TIMEOUT_SECONDS)%' # default 60 explorer: '%env(TON_EXPLORER)%' # default https://tonscan.org deposit_wallet_encryption_key: '%env(DEPOSIT_WALLET_ENCRYPTION_KEY)%' # base64 of 32 bytes
Every key is optional (sensible empty / 0 / explorer defaults), so a host that uses only one chain can leave the other section's values unset (or enabled: false).
Requirements
- PHP 8.3+
- Symfony 7.x (
symfony/framework-bundle) ext-gmp,ext-sodium,ext-openssl,ext-bcmath
Related packages
| Package | Layer |
|---|---|
| amashukov/ton-php | Umbrella TON SDK (Cell/BOC, wallet, toncenter) |
| amashukov/eth-php | Umbrella EVM SDK (Keccak, secp256k1, RLP, EIP-1559, ABI, RPC) |
| amashukov/http-client-php | PSR-18 cURL HTTP client |
| amashukov/eth-rpc-client-php | ethers.js v6-style JSON-RPC client |
| amashukov/toncenter-client-php | Typed toncenter v2 client |
Quality
- PHPStan level 9 across
src/. - php-cs-fixer with the
@PER-CSruleset. - GitHub Actions CI on every push.
composer install composer test # PHPUnit composer stan # PHPStan (level 9) composer cs # php-cs-fixer (dry-run) composer rector # Rector (dry-run)
License
MIT — see LICENSE.
Bitcoin Cash Node
BitcoinCashNodeRpcClient implements the same BitcoinRpcClientInterface on top of amashukov/bchn-rpc-client-php, so code written for Bitcoin Core can drive a Bitcoin Cash Node wallet (tested against BCHN v29.2.0):
- answers come back in the Core shape —
decodeRawTransaction()reportsvsize = sizeandweight = 4 × size(no SegWit), an output with exactly one address getsscriptPubKey.address, OP_RETURN outputs keep no address; walletProcessPsbt()signs withALL|FORKID;createPsbt()builds from explicit inputs and outputs;estimateSmartFee()readsestimatefee(BCH per kB);listUnspent(..., includeTokens: true)returns CashToken outputs withtokenData;lockUnspent()has no persistent flag on BCHN — locks are lost when the node restarts, so keep your own record of reserved inputs;walletCreateFundedPsbt()andpsbtBumpFee()throw: BCHN keeps the first-seen transaction, there is no fee bumping.
Wire it as a second service next to the Core client:
Amashukov\BchnRpc\BchnRpcClient: arguments: $rpcUrl: '%env(BCH_RPC_URL)%' $rpcUser: '%env(BCH_RPC_USER)%' $rpcPassword: '%env(BCH_RPC_PASSWORD)%' Amashukov\BlockchainContextBundle\Service\Bitcoin\BitcoinCashNodeRpcClient: arguments: $node: '@Amashukov\BchnRpc\BchnRpcClient'
createPsbt() and getTxOut() are available on the Core client as well; BitcoinAddressInfo::hdKeyPath, BitcoinWalletInfo::hdSeedId and BitcoinUnspentOutput::tokenData are optional fields (null on Core where it has no such data).
Bitcoin transaction fee evidence
BitcoinRpcClientInterface::getRawTransaction($txid, $blockHash) uses Core verbosity 2 and returns typed inputs, outputs and block identity. BitcoinRawTransaction::fee is the exact complete input-minus-output sum; missing previous outputs or coinbase inputs leave it null. A proven zero stays zero. A conflicting reported fee or a returned transaction/block identity mismatch throws. Callers must check active-chain evidence and classify payer ownership separately. The whole transaction fee is not additive across multiple deposit outputs. Existing wallet transaction fee defaults are unchanged and must not be treated as proof of an external payer’s fee.