enjin/php-blockchain-tools
A PHP library of blockchain utility tools from Enjin, providing helper classes for common operations like encoding, hashing, and data handling. Designed to simplify integration with Enjin and EVM-based chains in PHP and Laravel applications.
Install and verify the runtime first. The package is a low-level utility library, not a framework integration, so there is no service provider, config file, or facade to register:
composer require enjin/php-blockchain-tools
Before writing any code, open composer.json in vendor/enjin/php-blockchain-tools and confirm which big-number backend it expects (ext-gmp, ext-bcmath, or a pure-PHP math dependency) and that your CLI/web SAPI both have it. Mismatched extensions between php-fpm and php artisan is the single most common "works in tinker, fails in the queue worker" failure with this class of library.
Where to look first, in this order:
src/HexConverter.php — the static workhorse: isHex(), addHexPrefix(), removeHexPrefix(), hexToInt(), intToHex(), hexToBin(), binToHex(), padLeft(), padRight(). Read this file top to bottom; it is the API you will actually use 90% of the time.src/HexNumber/ — typed wrappers such as HexUint256, HexUint8, HexInt256, and the HexUint/HexInt factories. These give you range checking and sized padding for free.src/Contract/ — ContractData and the function-signature/method-id helpers used to decode calldata.First use case: decode an ERC-20 transfer from a transaction you already have. This is the fastest way to validate the install and to see the library's whole value proposition in one screen — it saves you from hand-rolling substr($data, 10, 64) everywhere:
use Enjin\BlockchainTools\Contract\ContractData;
use Enjin\BlockchainTools\HexConverter;
$data = '0xa9059cbb'
. HexConverter::padLeft(HexConverter::removeHexPrefix('0x742d35cc6634c0532925a3b844bc454e4438f44e'), 64)
. HexConverter::padLeft(HexConverter::removeHexPrefix(HexConverter::intToHex(1000000)), 64);
$decoded = ContractData::decode($data);
// inspect method id/name and decoded arguments
If that round-trips, you have the toolchain you need for indexing, webhook handling, and reconciliation jobs.
Wrap the static calls behind your own value objects. HexConverter returns scalars and throws on malformed input. In an application, let it be an implementation detail of one small class per domain concept, so the rest of your code never sees a 0x prefix:
final class Wei
{
private function __construct(public readonly string $decimal) {}
public static function fromRpcHex(string $hex): self
{
return new self(HexConverter::hexToInt(HexConverter::removeHexPrefix($hex)));
}
public function toHex(): string
{
return HexConverter::addHexPrefix(HexConverter::intToHex($this->decimal));
}
}
This is the highest-leverage pattern with this package: it is deliberately primitive, so the boundary between "untrusted hex from the wire" and "typed value in your domain" should be a constructor.
Normalize all RPC input at a single edge. JSON-RPC clients return quantities as unpadded, lowercase-or-not hex (0x0, 0x1b4, sometimes 0x01b4). In whatever client/service wraps your node calls, run every numeric field through HexConverter::hexToInt(HexConverter::removeHexPrefix($raw)) and every data field through a isHex() guard before it reaches your models. Then the rest of the app deals in int/string and you never again debug a hexdec() deprecation or a 0x prefix leaking into a DECIMAL column.
Use HexUint*/HexInt* where the bit width is part of the spec. For uint8 chain IDs, uint64 timestamps, int256 deltas, and anything that must be exactly 32 bytes on the wire (storage slots, topic filters, ABI arguments), the sized classes are better than HexConverter because they enforce the range and pad correctly. Reach for padLeft(..., 64) only when you are assembling raw calldata.
Decode calldata in one place and cache the selector map. Build a small CalldataDecoder service that maps 0x-prefixed 4-byte selectors to a handler (transfer, transferFrom, approve, safeTransferFrom, etc.). Compute the selector once at boot, memoize it in a static array or a container singleton, and have ContractData::decode() be the fallback for unknown selectors — log the raw payload when it returns nothing so you can add the signature later. This turns the package into an extensible dispatch table rather than a hard-coded branch.
Pair it with an RPC client, not in place of one. This library converts and decodes; it does not sign, send, or ABI-encode arbitrary structs. The clean integration is: your RPC client (web3.php, a Guzzle wrapper, whatever you already have) produces hex → this package converts/decodes → your Eloquent models store decimals/strings. Keep encoding of complex types in an ABI library and use HexConverter only for the padding/prefix trivia around it.
Use it in tests as an assertion tool. HexConverter::isHex() and explicit padLeft checks make excellent expectations for fixture data, and decoding known mainnet calldata into expected arguments is a cheap regression test for your indexer.
Prefix handling is not implicit. Almost every method has a prefixed and unprefixed flavour, and mixing them silently produces wrong-length strings rather than exceptions. Pick one convention per layer (suggestion: 0x-prefixed at the HTTP boundary, unprefixed inside) and enforce it. addHexPrefix on an already-prefixed string and removeHexPrefix on a bare string are both no-ops, so normalizing is cheap.
hexToInt is bounded by PHP's integer size. Anything above PHP_INT_MAX (i.e. most 32-byte values: wei balances, token IDs, hashes) will overflow or throw. Treat "is this value bigger than a native int?" as a design question up front: keep large values as decimal strings in the database (DECIMAL(65,0) or VARCHAR) and never round-trip them through int. Use the string-oriented helpers and the HexUint256 wrapper for those.
Signed vs unsigned matters more than you think. Decoding 0xffff...ff as unsigned gives you a huge positive number, as signed it is -1. If you handle int256 fields (P&L deltas, price changes), use the HexInt* classes explicitly; do not assume hexToInt knows the type.
Padding is to bytes, not characters, for odd-length input. An odd-length hex string is a common symptom of a number that lost its leading zero in a database column. Validate length before padding, or you will produce topics that look right but never match on-chain logs.
Case sensitivity. Hex is case-insensitive, but string comparisons of addresses, selectors, and log topics are not. Lowercase everything on ingest (strtolower) and store a single canonical form. Checksummed addresses will otherwise cause duplicate rows in your indexer.
Empty/zero edge cases. 0x, 0x0, '', and null all mean different things and libraries in this space handle them inconsistently. Guard at the edge: an empty revert reason is 0x, not null.
The package is small and lightly depended upon — pin it and isolate it. With essentially no downstream dependents, treat it as vendor code: composer.lock it, keep all usage behind the one or two wrapper classes described above, and be prepared to fork or inline HexConverter if maintenance stalls. Because it has no framework coupling, a fork is a copy-paste, which is the best possible exit strategy.
Extension points are limited by design. There is no plugin registry or config; the extension surface is (a) subclassing the HexUint/HexInt types for custom widths, and (b) your own decoder map around ContractData. Do not expect to register new ABI types with the package — put that logic in your own encoder/ABI dependency.
Debugging tip: when a decode "returns nothing" or looks wrong, dump strlen($hex) alongside the value. Nine times out of ten the payload is 2, 66, or 130 characters instead of the expected 10/74/138, which points straight at a missing prefix or an unpadded argument rather than a library bug.
How can I help you explore Laravel packages today?