gmajor/substrate-codec-php
PHP implementation of the Substrate SCALE codec for encoding and decoding Polkadot/Substrate data structures, with compact serialization for integers, strings, arrays, tuples, and custom types in PHP applications.
gmajor/substrate-codec-php is a SCALE (Simple Concatenated Aggregate Little-Endian) codec plus a thin Substrate helper layer: type encoding/decoding, compact integers, composite/struct types, SS58 addresses, keyring signing, and extrinsic/RPC plumbing. For a Laravel developer the value is narrow but sharp: when your app has to read or write raw Substrate bytes — storage values from an indexer, call data for offline signing, or hex blobs pulled from state_getStorage — this package lets you do it in PHP instead of standing up a Node sidecar. If you only ever consume a friendly REST API, you probably don't need it.
Minimal setup:
composer require gmajor/substrate-codec-php
There is no service provider, no config file, no facade. It is a plain library, so you wire it yourself:
// config/services.php
'substrate' => [
'rpc' => env('SUBSTRATE_RPC', 'wss://rpc.polkadot.io'),
'ss58' => (int) env('SUBSTRATE_SS58_PREFIX', 0), // 0 = Polkadot, 2 = Kusama, 42 = generic
'decimals' => (int) env('SUBSTRATE_DECIMALS', 10),
],
// app/Providers/SubstrateServiceProvider.php
public function register(): void
{
$this->app->singleton(Substrate::class, fn () => new Substrate(
config('services.substrate.rpc')
));
}
Where to look first, in this order: src/ for the codec entry point and the Substrate helper, then tests/ — for a small, low-star package the test suite is the real documentation, since it holds the known-good vectors. The README covers the happy path only.
First use case, and the one you'll hit 80% of the time: decoding an account's on-chain balance. System.Account returns a SCALE blob of AccountInfo — nonce, consumers, providers, sufficients, { free, reserved, frozen, flags } — where the three balance fields are u128. Decoding that gives you balances your app can display and reconcile, without trusting a third-party API's formatting.
// Illustrative — confirm exact method names against vendor/gmajor/... on install
$info = $codec->decode($accountInfoType, $storageHex);
$free = $info['data']['free']; // u128 — comes back as string, not int
1. Type-driven decoding of RPC payloads. SCALE is not self-describing; you supply the shape. Build the descriptor once (as a constant or a small class) and reuse it:
final class Types
{
public const ACCOUNT_INFO = 'AccountInfo'; // nonce/consumers/providers/sufficients + AccountData
public const ACCOUNT_DATA = 'AccountData'; // free/reserved/frozen/flags, all u128
}
Keep descriptors in one file per chain. When a runtime upgrade changes a struct, you change one line and the diff is reviewable.
2. Encode calls, don't hand-roll hex. For transfers, staking, governance votes — build the call data from a pallet/call index plus SCALE-encoded args, then submit or hand off for signing:
$callData = $codec->encodeCall(
palletIndex: $meta['Balances'], // read from metadata, never hardcode
callIndex: $meta['transfer_keep_alive'],
args: [$destAddress, $amountPlanck],
);
3. Wrap the client in a service, not in controllers. The Substrate helper is stateful (websocket/RPC connection). Bind it as a singleton, expose intention-revealing methods, and let the container handle lifecycle:
final class ChainGateway
{
public function __construct(private Substrate $client) {}
public function balanceOf(string $ss58): string { /* decode + format */ }
public function buildTransfer(string $to, string $planck): string { /* encoded call */ }
}
4. Format balances at the boundary. Store planck as string in the DB (decimal(38,0) or varchar), convert to human units only in the presenter. A u128 does not fit in PHP's 64-bit int — see Gotchas.
5. Offline signing / air-gapped flows. Encode the call → construct the payload with the correct specVersion, transactionVersion, genesisHash, blockHash, era, nonce, tip and the chain's signed extensions in metadata order → sign with the keyring → submit the signed extrinsic hex. This is the most fragile path in the package and the one most worth a test.
6. Verification loop with polkadot-js. During development, encode the same value in polkadot-js (api.registry.createType(...).toHex()) and assert byte equality against this package's output. Keep those pairs as PHPUnit data providers; they become your regression net across runtime upgrades.
7. Prefer reads over writes where you can. If you only need balances, events and block data, substrate-api-sidecar or a hosted indexer will save you a lot of pain. Reserve this package for the write path and for byte-exact decoding tasks.
u128 overflows PHP ints. Polkadot's total issuance (~1.4e9 DOT → ~1.4e19 planck) exceeds PHP_INT_MAX (~9.2e18). Treat all amounts as strings or use bcmath/gmp. This is the single most common bug in PHP Substrate integrations.Compact<u128> and u128 have different byte widths. Balances in storage are usually plain u128; nonces and indices are often Compact. Mixing them shifts every subsequent field.retry() with backoff."gmajor/substrate-codec-php": "0.x.y"), vendor nothing critical on a path you can't rewrite, and keep your own codec wrapper thin so a swap to polkadot-js via a Node microservice stays a one-file change.state_getStorage directly rather than a wrapper so you see real bytes.How can I help you explore Laravel packages today?