PHP & Blockchain Development — The Complete Production Developer’s Guide

1375 views
PHP & Blockchain: The Future of Web Development

Why PHP Developers Need to Understand Blockchain Now

PHP runs 77% of the web. Blockchain is powering the next generation of financial infrastructure, digital ownership, and trustless systems. The intersection — PHP developers who can build Web3-connected applications — is currently a gap in the market that pays premium rates.

The original article explains what blockchain is and shows callback-based web3.php examples. That’s the equivalent of teaching someone what a database is and showing them mysql_query(). This guide goes to production depth:

What you’ll build by the end of this guide:

  • A PHP blockchain library from scratch (understand before you abstract)
  • Ethereum wallet generation and address validation
  • Transaction signing and broadcasting without trusting a third-party wallet
  • Smart contract interaction with full ABI encoding/decoding
  • An ERC-20 token balance and transfer system
  • An NFT minting endpoint connected to IPFS
  • WooCommerce crypto payment gateway
  • Laravel blockchain service layer with events
  • Production security for private key management

This guide doesn’t just show you the “what.” It shows you the “why” — the cryptography, the encoding, the protocol — so when something breaks in production, you know exactly where to look.

 

Part 1 — Understanding the Stack Before Writing Code

How PHP Talks to Ethereum (The Complete Picture)

PHP APPLICATION
      │
      │  1. You call web3.php / custom client
      ▼
JSON-RPC REQUEST (HTTP/WebSocket)
      │
      │  POST https://mainnet.infura.io/v3/YOUR_KEY
      │  Body: {"jsonrpc":"2.0","method":"eth_getBalance","params":["0x...","latest"],"id":1}
      ▼
ETHEREUM NODE (Infura / Alchemy / Your own Geth/Besu)
      │
      │  Decodes JSON-RPC
      │  Executes against blockchain state
      ▼
BLOCKCHAIN STATE (distributed across thousands of nodes)
      │
      │  Returns hex-encoded result
      ▼
JSON-RPC RESPONSE
      │
      │  {"jsonrpc":"2.0","id":1,"result":"0x56BC75E2D63100000"}
      ▼
PHP DECODES + CONVERTS
      │
      │  hexdec("56BC75E2D63100000") / 1e18 = 100 ETH
      ▼
YOUR APPLICATION LOGIC

KEY INSIGHT: PHP is always a CLIENT to a blockchain node.
PHP cannot mine blocks, validate transactions, or run the EVM.
PHP reads state (eth_call) and submits signed transactions (eth_sendRawTransaction).

The JSON-RPC Methods You’ll Use Most

<?php
/**
 * The 10 most important Ethereum JSON-RPC methods for PHP developers.
 * Understanding these means understanding Web3.
 */
$methods = [
    // ── Reading State (free, instant) ─────────────────────────────────────
    'eth_blockNumber'          => 'Get the latest block number',
    'eth_getBalance'           => 'Get ETH balance for an address',
    'eth_call'                 => 'Call a smart contract read function (no gas)',
    'eth_getTransactionCount'  => 'Get nonce for an address (needed to send tx)',
    'eth_getTransactionReceipt'=> 'Get receipt of a submitted transaction',
    'eth_getLogs'              => 'Get event logs from smart contracts',
    'eth_gasPrice'             => 'Get current gas price',
    'eth_estimateGas'          => 'Estimate gas needed for a transaction',

    // ── Writing State (costs gas, requires signing) ────────────────────────
    'eth_sendRawTransaction'   => 'Submit a signed transaction to the network',

    // ── Node Info ─────────────────────────────────────────────────────────
    'net_version'              => 'Get the network ID (1=mainnet, 137=Polygon, etc.)',
];

 

PHP + Blockchain Architecture

Part 2 — Building a PHP Ethereum Client from Scratch

Understanding how the client works at the HTTP level makes you independent from any specific library:

<?php
declare(strict_types=1);

/**
 * Low-level Ethereum JSON-RPC Client
 *
 * This is what web3.php does internally.
 * Understanding this layer helps you debug any Web3 issue.
 */
class EthereumRpcClient {

    private int $requestId = 1;

    public function __construct(
        private readonly string $endpoint,  // e.g., 'https://mainnet.infura.io/v3/KEY'
        private readonly int    $timeout = 15
    ) {}

    /**
     * Make a JSON-RPC call to the Ethereum node.
     *
     * @param  string $method  The JSON-RPC method (e.g., 'eth_getBalance')
     * @param  array  $params  Method parameters
     * @return mixed           The decoded result
     * @throws \RuntimeException On connection or RPC error
     */
    public function call(string $method, array $params = []): mixed {

        $payload = [
            'jsonrpc' => '2.0',
            'method'  => $method,
            'params'  => $params,
            'id'      => $this->requestId++,
        ];

        $ch = curl_init($this->endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS     => json_encode($payload),
        ]);

        $body  = curl_exec($ch);
        $errno = curl_errno($ch);
        $code  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($errno !== 0) {
            throw new \RuntimeException("Connection failed: " . curl_strerror($errno));
        }

        if ($code !== 200) {
            throw new \RuntimeException("HTTP error: {$code}");
        }

        $response = json_decode($body, true);

        if (isset($response['error'])) {
            $error = $response['error'];
            throw new \RuntimeException(
                "RPC error [{$error['code']}]: {$error['message']}"
            );
        }

        return $response['result'] ?? null;
    }

    // ── Convenience methods ────────────────────────────────────────────────

    /** Get ETH balance for an address, returned in Wei (as string). */
    public function getBalance(string $address, string $block = 'latest'): string {
        $result = $this->call('eth_getBalance', [$address, $block]);
        return $this->hexToDecimalString($result);
    }

    /** Get balance in ETH (not Wei). */
    public function getBalanceEth(string $address): string {
        $balanceWei = $this->getBalance($address);
        return $this->weiToEth($balanceWei);
    }

    /** Get the number of transactions sent FROM an address (nonce). */
    public function getNonce(string $address): int {
        $result = $this->call('eth_getTransactionCount', [$address, 'latest']);
        return hexdec(substr($result, 2));
    }

    /** Get current gas price in Wei. */
    public function getGasPrice(): string {
        $result = $this->call('eth_gasPrice', []);
        return $this->hexToDecimalString($result);
    }

    /** Get current block number. */
    public function getBlockNumber(): int {
        $result = $this->call('eth_blockNumber', []);
        return hexdec(substr($result, 2));
    }

    /** Check transaction receipt. Returns null if pending. */
    public function getTransactionReceipt(string $txHash): ?array {
        return $this->call('eth_getTransactionReceipt', [$txHash]);
    }

    /** Wait for a transaction to be confirmed (poll). */
    public function waitForReceipt(
        string $txHash,
        int    $maxAttempts = 40,
        int    $sleepSeconds = 3
    ): ?array {
        for ($i = 0; $i < $maxAttempts; $i++) {
            $receipt = $this->getTransactionReceipt($txHash);
            if ($receipt !== null) {
                return $receipt;
            }
            sleep($sleepSeconds);
        }
        return null; // Transaction not confirmed within timeout
    }

    /** Call a smart contract view function (doesn't cost gas). */
    public function ethCall(array $transaction, string $block = 'latest'): string {
        return $this->call('eth_call', [$transaction, $block]) ?? '0x';
    }

    /** Submit a signed raw transaction. */
    public function sendRawTransaction(string $signedTxHex): string {
        return $this->call('eth_sendRawTransaction', [$signedTxHex]);
    }

    /** Get network chain ID. */
    public function getChainId(): int {
        $result = $this->call('eth_chainId', []);
        return hexdec(substr($result, 2));
    }

    /** Get contract event logs. */
    public function getLogs(array $filter): array {
        return $this->call('eth_getLogs', [$filter]) ?? [];
    }

    // ── Unit conversion helpers ────────────────────────────────────────────

    /** Convert hex string (with or without 0x prefix) to decimal string. */
    public function hexToDecimalString(string $hex): string {
        $hex = ltrim($hex, '0x');
        if (empty($hex)) return '0';

        // Use BCMath for large numbers (Wei values exceed PHP_INT_MAX):
        $decimal = '0';
        for ($i = 0, $len = strlen($hex); $i < $len; $i++) {
            $decimal = bcadd(bcmul($decimal, '16'), (string)hexdec($hex[$i]));
        }
        return $decimal;
    }

    /** Convert Wei (string) to ETH (string, 18 decimal places). */
    public function weiToEth(string $weiString): string {
        return bcdiv($weiString, bcpow('10', '18'), 18);
    }

    /** Convert ETH (string) to Wei (string). */
    public function ethToWei(string $ethString): string {
        return bcmul($ethString, bcpow('10', '18'));
    }

    /** Validate an Ethereum address (checksum or lowercase). */
    public function isValidAddress(string $address): bool {
        return (bool) preg_match('/^0x[0-9a-fA-F]{40}$/', $address);
    }
}

// ── Usage ──────────────────────────────────────────────────────────────────────
$client = new EthereumRpcClient('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');

$balance = $client->getBalanceEth('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
echo "Vitalik's ETH balance: {$balance} ETH\n";

$blockNum = $client->getBlockNumber();
echo "Latest block: {$blockNum}\n";

$chainId = $client->getChainId();
echo "Chain ID: {$chainId} (1=Ethereum mainnet)\n";

 

Part 3 — Wallet Generation and Address Derivation in PHP

<?php
declare(strict_types=1);

/**
 * PHP Ethereum Wallet Generator
 *
 * WARNING: Private keys are the most sensitive data in blockchain.
 * Never log, display, or transmit private keys.
 * In production, use HSMs (Hardware Security Modules) or KMS services.
 */
class EthereumWallet {

    /**
     * Generate a new Ethereum wallet (private key + address).
     *
     * Uses cryptographically secure random bytes.
     * Never call this on a machine you don't control.
     *
     * @return array ['private_key' => string, 'address' => string]
     */
    public static function generate(): array {
        // Generate 32 cryptographically random bytes (256 bits):
        $privateKeyBytes = random_bytes(32);

        // Ensure key is in valid range (less than secp256k1 curve order):
        // For simplicity, we'll validate rather than generate-in-range
        // (probability of generating an invalid key is astronomically low)
        $privateKeyHex = bin2hex($privateKeyBytes);

        // Derive the Ethereum address from the private key:
        $address = self::privateKeyToAddress($privateKeyHex);

        return [
            'private_key' => '0x' . $privateKeyHex,
            'address'     => $address,
            // WARNING: Store private_key securely — this grants full control of the wallet
        ];
    }

    /**
     * Derive Ethereum address from a private key hex string.
     *
     * Process:
     * 1. ECDSA public key = private_key × G (elliptic curve multiplication on secp256k1)
     * 2. Address = '0x' + last 20 bytes of Keccak-256(public_key_uncompressed)
     */
    public static function privateKeyToAddress(string $privateKeyHex): string {
        // This requires the secp256k1 PHP extension or a pure-PHP ECDSA library
        // Using kornrunner/keccak and simplito/elliptic-php:
        // composer require simplito/elliptic-php kornrunner/keccak

        $ec = new \Elliptic\EC('secp256k1');
        $keyPair = $ec->keyFromPrivate(ltrim($privateKeyHex, '0x'));

        // Get uncompressed public key (64 bytes = 128 hex chars, without 04 prefix):
        $publicKey = $keyPair->getPublic(false, 'hex');
        $publicKeyBytes = substr($publicKey, 2); // Remove '04' prefix

        // Keccak-256 hash of the public key:
        $hash = \kornrunner\Keccak::hash(hex2bin($publicKeyBytes), 256);

        // Take last 20 bytes (40 hex chars) and add 0x prefix:
        return '0x' . substr($hash, -40);
    }

    /**
     * Convert a private key to checksummed Ethereum address (EIP-55).
     * Checksummed addresses have mixed case — valid wallets accept either format.
     */
    public static function toChecksumAddress(string $address): string {
        $address = strtolower(ltrim($address, '0x'));
        $hash    = \kornrunner\Keccak::hash($address, 256);

        $checksummed = '0x';
        for ($i = 0; $i < 40; $i++) {
            // If corresponding nibble in hash is >= 8, uppercase the address char:
            $checksummed .= (hexdec($hash[$i]) >= 8)
                ? strtoupper($address[$i])
                : $address[$i];
        }

        return $checksummed;
    }

    /**
     * Validate an Ethereum address (with optional checksum validation).
     */
    public static function isValid(string $address, bool $checkChecksum = false): bool {
        if (!preg_match('/^0x[0-9a-fA-F]{40}$/', $address)) {
            return false;
        }

        if ($checkChecksum) {
            // If address has mixed case, validate EIP-55 checksum:
            $lower  = strtolower($address);
            $upper  = strtoupper($address);
            if ($address !== $lower && $address !== $upper) {
                return $address === self::toChecksumAddress($address);
            }
        }

        return true;
    }

    /**
     * Derive multiple addresses from a mnemonic phrase (HD Wallet / BIP-44).
     * Requires: composer require bitwasp/bitcoin
     *
     * Standard derivation path for Ethereum: m/44'/60'/0'/0/index
     */
    public static function fromMnemonic(string $mnemonic, int $count = 5): array {
        // This requires bitwasp/bitcoin library for BIP-39/44 compliance
        // The actual implementation uses PBKDF2 + BIP-32 HD key derivation
        // Shown here as pseudocode for brevity:

        // $seed     = Bip39::mnemonicToSeed($mnemonic);
        // $rootKey  = Bip32::fromSeed($seed);
        // $accounts = [];
        // for ($i = 0; $i < $count; $i++) {
        //     $derivedKey = $rootKey->derive("m/44'/60'/0'/0/{$i}");
        //     $accounts[$i] = [
        //         'index'   => $i,
        //         'path'    => "m/44'/60'/0'/0/{$i}",
        //         'address' => self::privateKeyToAddress($derivedKey->privateKey()),
        //     ];
        // }
        // return $accounts;

        throw new \RuntimeException('Implement with bitwasp/bitcoin for production mnemonic support');
    }
}

// ── Usage ──────────────────────────────────────────────────────────────────────
// Generate a new wallet (for testing only — never generate wallets in production on shared servers):
$wallet = EthereumWallet::generate();
echo "Address: " . $wallet['address'] . "\n";
// NEVER echo private_key in production — this is for demonstration only

// Validate an address:
$isValid = EthereumWallet::isValid('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', true);
echo "Valid: " . ($isValid ? 'yes' : 'no') . "\n";

 

Part 4 — ABI Encoding: The Key to Smart Contract Interaction

The Ethereum ABI (Application Binary Interface) is how PHP encodes function calls and decodes results. This is completely missing from the original article.

<?php
declare(strict_types=1);

/**
 * Ethereum ABI Encoder/Decoder
 *
 * When you call a smart contract function, Ethereum doesn't know PHP types.
 * It receives a hex-encoded byte string that follows the ABI specification.
 *
 * Method ID = first 4 bytes of Keccak-256 hash of the function signature
 * Arguments = ABI-encoded parameters following the method ID
 */
class AbiEncoder {

    /**
     * Encode a function call with its arguments.
     *
     * @param  string $functionSignature  e.g., 'transfer(address,uint256)'
     * @param  array  $args               Ordered argument values
     * @return string                     Hex-encoded calldata (with 0x prefix)
     */
    public static function encodeFunctionCall(string $functionSignature, array $args = []): string {
        // Step 1: Compute method ID (first 4 bytes of Keccak-256 hash):
        $methodId = self::getFunctionSelector($functionSignature);

        // Step 2: Parse parameter types from signature:
        preg_match('/\(([^)]*)\)/', $functionSignature, $matches);
        $paramTypesStr = $matches[1] ?? '';
        $paramTypes    = empty($paramTypesStr) ? [] : array_map('trim', explode(',', $paramTypesStr));

        // Step 3: Encode arguments:
        $encoded = self::encodeArgs($paramTypes, $args);

        return '0x' . $methodId . $encoded;
    }

    /**
     * Get the 4-byte function selector (method ID).
     * Example: 'balanceOf(address)' → '70a08231'
     */
    public static function getFunctionSelector(string $signature): string {
        $hash = \kornrunner\Keccak::hash($signature, 256);
        return substr($hash, 0, 8); // First 4 bytes = 8 hex chars
    }

    /**
     * Encode function arguments per ABI specification.
     * Handles: uint256, address, bool, bytes32, string, bytes, arrays
     */
    public static function encodeArgs(array $types, array $values): string {
        $head  = '';
        $tail  = '';

        // ABI encoding uses head+tail for dynamic types:
        // Static types (uint256, address, bool, bytes32) go in head
        // Dynamic types (string, bytes, arrays) pointer in head, data in tail

        $headOffset = count($types) * 32; // Each head slot is 32 bytes

        foreach ($types as $i => $type) {
            $value = $values[$i] ?? null;

            if (self::isDynamicType($type)) {
                // Store offset pointer in head:
                $head .= str_pad(dechex($headOffset + strlen($tail) / 2), 64, '0', STR_PAD_LEFT);
                $tail .= self::encodeDynamic($type, $value);
            } else {
                $head .= self::encodeStatic($type, $value);
            }
        }

        return $head . $tail;
    }

    private static function isDynamicType(string $type): bool {
        return in_array($type, ['string', 'bytes'], true) || str_ends_with($type, '[]');
    }

    /**
     * Encode static (fixed-size) types — each is exactly 32 bytes (64 hex chars).
     */
    public static function encodeStatic(string $type, mixed $value): string {
        // uint256 and all uint types:
        if (str_starts_with($type, 'uint') || str_starts_with($type, 'int')) {
            // Convert to decimal string for bcmath, then to hex:
            $decimal = (string) $value;
            $hex     = self::decimalToHex($decimal);
            return str_pad($hex, 64, '0', STR_PAD_LEFT);
        }

        // address (20 bytes, left-padded to 32):
        if ($type === 'address') {
            $addr = strtolower(ltrim($value, '0x'));
            return str_pad($addr, 64, '0', STR_PAD_LEFT);
        }

        // bool:
        if ($type === 'bool') {
            return str_pad($value ? '1' : '0', 64, '0', STR_PAD_LEFT);
        }

        // bytes1 through bytes32 (right-padded):
        if (preg_match('/^bytes(\d+)$/', $type, $m)) {
            $bytes = bin2hex(is_string($value) ? $value : pack('N', $value));
            return str_pad(substr($bytes, 0, 64), 64, '0', STR_PAD_RIGHT);
        }

        throw new \InvalidArgumentException("Unsupported static ABI type: {$type}");
    }

    /**
     * Encode dynamic (variable-size) types.
     */
    public static function encodeDynamic(string $type, mixed $value): string {
        if ($type === 'string' || $type === 'bytes') {
            $bytes  = $type === 'string' ? $value : hex2bin(ltrim($value, '0x'));
            $length = strlen($bytes);

            // Length prefix (32 bytes) + data (padded to multiple of 32):
            $lengthHex = str_pad(dechex($length), 64, '0', STR_PAD_LEFT);
            $dataHex   = bin2hex($bytes);
            $dataHex   = str_pad($dataHex, (int)(ceil($length / 32) * 64), '0', STR_PAD_RIGHT);

            return $lengthHex . $dataHex;
        }

        throw new \InvalidArgumentException("Unsupported dynamic ABI type: {$type}");
    }

    /**
     * Decode the result of an eth_call.
     */
    public static function decodeResult(string $types, string $hexData): array {
        $data   = ltrim($hexData, '0x');
        $types  = array_map('trim', explode(',', $types));
        $result = [];
        $offset = 0;

        foreach ($types as $type) {
            if ($type === 'uint256' || str_starts_with($type, 'uint') || str_starts_with($type, 'int')) {
                $hex        = substr($data, $offset, 64);
                $result[]   = self::hexToDecimalString($hex);
                $offset    += 64;
            } elseif ($type === 'address') {
                $hex      = substr($data, $offset + 24, 40); // Skip 12 bytes of padding
                $result[] = '0x' . $hex;
                $offset  += 64;
            } elseif ($type === 'bool') {
                $result[] = substr($data, $offset + 63, 1) === '1';
                $offset  += 64;
            } elseif ($type === 'string' || $type === 'bytes') {
                // Dynamic type — follow the pointer
                $ptrHex  = substr($data, $offset, 64);
                $ptr     = hexdec($ptrHex) * 2; // offset in hex chars
                $lenHex  = substr($data, $ptr, 64);
                $len     = hexdec($lenHex) * 2; // length in hex chars
                $rawHex  = substr($data, $ptr + 64, $len);
                $result[] = $type === 'string' ? hex2bin($rawHex) : '0x' . $rawHex;
                $offset += 64;
            }
        }

        return $result;
    }

    private static function decimalToHex(string $decimal): string {
        $hex = '';
        $n   = $decimal;
        while (bccomp($n, '0') > 0) {
            $hex = dechex((int) bcmod($n, '16')) . $hex;
            $n   = bcdiv($n, '16', 0);
        }
        return $hex ?: '0';
    }

    private static function hexToDecimalString(string $hex): string {
        $decimal = '0';
        for ($i = 0, $len = strlen($hex); $i < $len; $i++) {
            $decimal = bcadd(bcmul($decimal, '16'), (string)hexdec($hex[$i]));
        }
        return $decimal;
    }
}

 

Part 5 — ERC-20 Token Operations (The Complete Class)

<?php
declare(strict_types=1);

/**
 * ERC-20 Token Client
 *
 * Implements all standard ERC-20 functions:
 * - totalSupply, balanceOf, transfer, transferFrom, approve, allowance
 * - Plus event log parsing for Transfer events
 */
class Erc20Token {

    // Standard ERC-20 function signatures:
    private const SIGS = [
        'name'         => 'name()',
        'symbol'       => 'symbol()',
        'decimals'     => 'decimals()',
        'totalSupply'  => 'totalSupply()',
        'balanceOf'    => 'balanceOf(address)',
        'transfer'     => 'transfer(address,uint256)',
        'approve'      => 'approve(address,uint256)',
        'allowance'    => 'allowance(address,address)',
        'transferFrom' => 'transferFrom(address,address,uint256)',
    ];

    // Transfer event topic (keccak256 of "Transfer(address,address,uint256)"):
    private const TRANSFER_EVENT_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';

    private EthereumRpcClient $rpc;
    private string $contractAddress;
    private ?int $decimals = null;

    public function __construct(EthereumRpcClient $rpc, string $contractAddress) {
        $this->rpc             = $rpc;
        $this->contractAddress = $contractAddress;
    }

    // ── Read functions (free eth_call) ────────────────────────────────────────

    public function name(): string {
        $result = $this->callContract('name()', 'string', []);
        return $result[0] ?? '';
    }

    public function symbol(): string {
        $result = $this->callContract('symbol()', 'string', []);
        return $result[0] ?? '';
    }

    public function decimals(): int {
        if ($this->decimals === null) {
            $result        = $this->callContract('decimals()', 'uint8', []);
            $this->decimals = (int)($result[0] ?? 18);
        }
        return $this->decimals;
    }

    public function totalSupply(): string {
        $result = $this->callContract('totalSupply()', 'uint256', []);
        return $this->fromWei($result[0] ?? '0');
    }

    /**
     * Get token balance for an address.
     *
     * @param  string $address  Ethereum address
     * @return string           Balance in token units (not raw wei)
     */
    public function balanceOf(string $address): string {
        $calldata = AbiEncoder::encodeFunctionCall('balanceOf(address)', [$address]);

        $rawResult = $this->rpc->ethCall([
            'to'   => $this->contractAddress,
            'data' => $calldata,
        ]);

        $decoded = AbiEncoder::decodeResult('uint256', $rawResult);
        return $this->fromWei($decoded[0] ?? '0');
    }

    /**
     * Get allowance: how many tokens spender can transfer from owner.
     */
    public function allowance(string $owner, string $spender): string {
        $calldata  = AbiEncoder::encodeFunctionCall('allowance(address,address)', [$owner, $spender]);
        $rawResult = $this->rpc->ethCall(['to' => $this->contractAddress, 'data' => $calldata]);
        $decoded   = AbiEncoder::decodeResult('uint256', $rawResult);
        return $this->fromWei($decoded[0] ?? '0');
    }

    // ── Write functions (require signing + gas) ───────────────────────────────

    /**
     * Build encoded calldata for a transfer.
     * The actual transaction signing must happen with the private key.
     *
     * @param  string $to      Recipient address
     * @param  string $amount  Amount in token units (e.g., "100.5")
     * @return string          ABI-encoded calldata for eth_sendRawTransaction
     */
    public function buildTransferCalldata(string $to, string $amount): string {
        $amountWei = $this->toWei($amount);
        return AbiEncoder::encodeFunctionCall('transfer(address,uint256)', [$to, $amountWei]);
    }

    public function buildApproveCalldata(string $spender, string $amount): string {
        $amountWei = $this->toWei($amount);
        return AbiEncoder::encodeFunctionCall('approve(address,uint256)', [$spender, $amountWei]);
    }

    // ── Event log parsing ─────────────────────────────────────────────────────

    /**
     * Get Transfer events for an address (as sender or recipient).
     *
     * @param  string $address      Address to filter by
     * @param  int    $fromBlock    Starting block number
     * @param  int|null $toBlock    Ending block (null = latest)
     * @return array                Array of transfer events
     */
    public function getTransfers(string $address, int $fromBlock = 0, ?int $toBlock = null): array {
        $paddedAddress = '0x' . str_pad(ltrim($address, '0x'), 64, '0', STR_PAD_LEFT);

        $filter = [
            'address'   => $this->contractAddress,
            'fromBlock' => '0x' . dechex($fromBlock),
            'toBlock'   => $toBlock ? '0x' . dechex($toBlock) : 'latest',
            'topics'    => [
                self::TRANSFER_EVENT_TOPIC,
                null, // Any "from" address
                $paddedAddress, // Filter by recipient
            ],
        ];

        $logs    = $this->rpc->getLogs($filter);
        $events  = [];

        foreach ($logs as $log) {
            // Decode topics (indexed event parameters):
            $from  = '0x' . substr($log['topics'][1] ?? '', 26); // Last 20 bytes
            $to    = '0x' . substr($log['topics'][2] ?? '', 26);
            $value = AbiEncoder::decodeResult('uint256', $log['data']);

            $events[] = [
                'from'         => $from,
                'to'           => $to,
                'amount'       => $this->fromWei($value[0] ?? '0'),
                'tx_hash'      => $log['transactionHash'],
                'block_number' => hexdec($log['blockNumber']),
            ];
        }

        return $events;
    }

    // ── Unit helpers ──────────────────────────────────────────────────────────

    private function toWei(string $amount): string {
        $decimals = $this->decimals();
        $parts    = explode('.', $amount);
        $whole    = $parts[0];
        $fraction = str_pad($parts[1] ?? '', $decimals, '0', STR_PAD_RIGHT);
        $fraction = substr($fraction, 0, $decimals);
        return ltrim($whole . $fraction, '0') ?: '0';
    }

    private function fromWei(string $wei): string {
        $decimals = $this->decimals();
        return bcdiv($wei, bcpow('10', (string)$decimals), $decimals);
    }

    private function callContract(string $sig, string $returnType, array $args): array {
        $calldata  = AbiEncoder::encodeFunctionCall($sig, $args);
        $rawResult = $this->rpc->ethCall(['to' => $this->contractAddress, 'data' => $calldata]);
        return AbiEncoder::decodeResult($returnType, $rawResult);
    }
}

// ── Usage ──────────────────────────────────────────────────────────────────────
$rpc   = new EthereumRpcClient('https://mainnet.infura.io/v3/YOUR_KEY');
$usdc  = new Erc20Token($rpc, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'); // USDC mainnet

echo "Token: " . $usdc->symbol() . "\n";          // USDC
echo "Decimals: " . $usdc->decimals() . "\n";      // 6
echo "Total Supply: " . $usdc->totalSupply() . "\n";

$balance = $usdc->balanceOf('0xSomeUserAddress');
echo "Balance: {$balance} USDC\n";

// Get recent transfers to an address:
$transfers = $usdc->getTransfers('0xSomeUserAddress', 18000000);
foreach ($transfers as $tx) {
    echo "Received {$tx['amount']} USDC from {$tx['from']} in tx {$tx['tx_hash']}\n";
}

 

Part 6 — Transaction Signing in PHP (Without a Wallet)

The most security-critical part of blockchain development — signing transactions without exposing private keys:

<?php
declare(strict_types=1);

/**
 * Ethereum Transaction Signer
 *
 * Signs raw Ethereum transactions using ECDSA on the secp256k1 curve.
 * This is what MetaMask does internally when you approve a transaction.
 *
 * SECURITY NOTE: Private keys must NEVER leave a secure environment.
 * In production, use AWS KMS, HashiCorp Vault, or an HSM.
 * NEVER store private keys in environment variables on shared servers.
 * NEVER log or transmit private keys.
 *
 * Requires: composer require simplito/elliptic-php kornrunner/keccak
 */
class TransactionSigner {

    /**
     * Build and sign a raw transaction.
     *
     * @param  array  $transaction   Transaction parameters
     * @param  string $privateKeyHex Private key (32 bytes hex, with or without 0x)
     * @param  int    $chainId       Network chain ID (1=mainnet, 137=Polygon, etc.)
     * @return string                Signed transaction hex (ready for eth_sendRawTransaction)
     */
    public static function sign(array $transaction, string $privateKeyHex, int $chainId = 1): string {

        $privateKeyHex = ltrim($privateKeyHex, '0x');

        // Build the transaction object with required fields:
        $tx = [
            'nonce'    => $transaction['nonce'],
            'gasPrice' => $transaction['gasPrice'],
            'gasLimit' => $transaction['gasLimit'],
            'to'       => $transaction['to'] ?? '',
            'value'    => $transaction['value'] ?? '0',
            'data'     => $transaction['data'] ?? '',
            'chainId'  => $chainId,
        ];

        // ── Step 1: RLP encode the transaction data ────────────────────────
        $rlpData = self::rlpEncode([
            self::toBigInt($tx['nonce']),
            self::toBigInt($tx['gasPrice']),
            self::toBigInt($tx['gasLimit']),
            $tx['to'] ? hex2bin(ltrim($tx['to'], '0x')) : '',
            self::toBigInt($tx['value']),
            $tx['data'] ? hex2bin(ltrim($tx['data'], '0x')) : '',
            self::toBigInt($chainId),
            '',  // v (empty for EIP-155 signing)
            '',  // r (empty for EIP-155 signing)
        ]);

        // ── Step 2: Keccak-256 hash of the RLP-encoded transaction ────────
        $txHash = \kornrunner\Keccak::hash($rlpData, 256);

        // ── Step 3: ECDSA sign the hash with the private key ──────────────
        $ec        = new \Elliptic\EC('secp256k1');
        $keyPair   = $ec->keyFromPrivate($privateKeyHex);
        $signature = $keyPair->sign($txHash, ['canonical' => true]);

        $r = str_pad($signature->r->toString(16), 64, '0', STR_PAD_LEFT);
        $s = str_pad($signature->s->toString(16), 64, '0', STR_PAD_LEFT);

        // Recovery parameter v (EIP-155 replay protection):
        // v = chainId * 2 + 35 + recoveryParam
        $recoveryId = $signature->recoveryParam;
        $v          = dechex($chainId * 2 + 35 + $recoveryId);

        // ── Step 4: RLP encode the final signed transaction ───────────────
        $signedRlp = self::rlpEncode([
            self::toBigInt($tx['nonce']),
            self::toBigInt($tx['gasPrice']),
            self::toBigInt($tx['gasLimit']),
            $tx['to'] ? hex2bin(ltrim($tx['to'], '0x')) : '',
            self::toBigInt($tx['value']),
            $tx['data'] ? hex2bin(ltrim($tx['data'], '0x')) : '',
            hex2bin(str_pad($v, 2, '0', STR_PAD_LEFT)),
            hex2bin($r),
            hex2bin($s),
        ]);

        return '0x' . bin2hex($signedRlp);
    }

    /**
     * RLP encode a list of items.
     * RLP = Recursive Length Prefix encoding (Ethereum's serialization format).
     */
    private static function rlpEncode(array $items): string {
        $encodedItems = '';
        foreach ($items as $item) {
            $encodedItems .= self::rlpEncodeItem($item);
        }

        $length = strlen($encodedItems);
        if ($length < 56) {
            return chr(0xc0 + $length) . $encodedItems;
        }

        $lenBin  = ltrim(pack('N', $length), "\x00");
        $lenLen  = strlen($lenBin);
        return chr(0xf7 + $lenLen) . $lenBin . $encodedItems;
    }

    private static function rlpEncodeItem(string $item): string {
        if ($item === '') {
            return chr(0x80);
        }

        $length = strlen($item);

        if ($length === 1 && ord($item) < 0x80) {
            return $item; // Single byte, value < 128: encoded as itself
        }

        if ($length < 56) {
            return chr(0x80 + $length) . $item;
        }

        $lenBin = ltrim(pack('N', $length), "\x00");
        $lenLen = strlen($lenBin);
        return chr(0xb7 + $lenLen) . $lenBin . $item;
    }

    private static function toBigInt(mixed $value): string {
        if ($value === '' || $value === null || $value === '0' || $value === 0) {
            return '';
        }

        // Convert hex or decimal to bytes:
        if (is_string($value) && str_starts_with($value, '0x')) {
            $hex = ltrim($value, '0x');
        } else {
            $hex = dechex((int)$value);
        }

        if (strlen($hex) % 2 !== 0) {
            $hex = '0' . $hex;
        }

        return ltrim(hex2bin($hex), "\x00") ?: '';
    }
}

// ── Example: Send ETH transaction ─────────────────────────────────────────────
// WARNING: This is for testing on a local testnet (Ganache/Hardhat) ONLY

$rpc = new EthereumRpcClient('http://127.0.0.1:8545'); // Local Ganache
$from = '0xYourFromAddress';
$privateKey = '0xYourPrivateKeyForTestingOnly'; // NEVER hardcode in production

// Get current nonce and gas price:
$nonce    = $rpc->getNonce($from);
$gasPrice = $rpc->getGasPrice();

$transaction = [
    'nonce'    => '0x' . dechex($nonce),
    'gasPrice' => '0x' . dechex((int)$gasPrice),
    'gasLimit' => '0x5208', // 21000 gas for simple ETH transfer
    'to'       => '0xRecipientAddress',
    'value'    => '0x' . dechex(1000000000000000), // 0.001 ETH in Wei
    'data'     => '',
];

// Sign the transaction (locally — private key never sent anywhere):
$signedTx = TransactionSigner::sign($transaction, $privateKey, 1337); // 1337 = Ganache chain ID

// Broadcast to network:
$txHash = $rpc->sendRawTransaction($signedTx);
echo "Transaction sent: {$txHash}\n";

// Wait for confirmation:
$receipt = $rpc->waitForReceipt($txHash, 20, 2);
echo "Status: " . ($receipt['status'] === '0x1' ? 'Success' : 'Failed') . "\n";

 

Part 7 — Solidity Smart Contract + PHP Interaction

Write the smart contract and interact with it from PHP:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// File: contracts/SimpleStore.sol
// Deploy with Hardhat or Remix IDE, get the contract address

contract SimpleStore {

    address public owner;
    string  private storedMessage;
    uint256 public messageCount;

    event MessageStored(address indexed sender, string message, uint256 timestamp);

    constructor(string memory initialMessage) {
        owner         = msg.sender;
        storedMessage = initialMessage;
        messageCount  = 0;
    }

    // View function — free to call via eth_call
    function getMessage() public view returns (string memory) {
        return storedMessage;
    }

    function getStats() public view returns (address, uint256, string memory) {
        return (owner, messageCount, storedMessage);
    }

    // State-changing function — requires gas + transaction
    function storeMessage(string memory newMessage) public {
        require(bytes(newMessage).length > 0, "Message cannot be empty");
        require(bytes(newMessage).length <= 280, "Message too long");

        storedMessage = newMessage;
        messageCount++;

        emit MessageStored(msg.sender, newMessage, block.timestamp);
    }

    // Only owner can reset:
    function reset() public {
        require(msg.sender == owner, "Only owner can reset");
        storedMessage = "";
        messageCount  = 0;
    }
}
<?php
declare(strict_types=1);

/**
 * PHP interaction with the SimpleStore smart contract.
 * Shows both read (eth_call) and write (eth_sendRawTransaction) operations.
 */
class SimpleStoreContract {

    private EthereumRpcClient $rpc;
    private string $address;
    private int    $chainId;

    public function __construct(EthereumRpcClient $rpc, string $contractAddress, int $chainId = 1337) {
        $this->rpc      = $rpc;
        $this->address  = $contractAddress;
        $this->chainId  = $chainId;
    }

    // ── Read functions ────────────────────────────────────────────────────────

    public function getMessage(): string {
        $calldata  = AbiEncoder::encodeFunctionCall('getMessage()', []);
        $rawResult = $this->rpc->ethCall(['to' => $this->address, 'data' => $calldata]);
        $decoded   = AbiEncoder::decodeResult('string', $rawResult);
        return $decoded[0] ?? '';
    }

    public function getMessageCount(): int {
        $calldata  = AbiEncoder::encodeFunctionCall('messageCount()', []);
        $rawResult = $this->rpc->ethCall(['to' => $this->address, 'data' => $calldata]);
        $decoded   = AbiEncoder::decodeResult('uint256', $rawResult);
        return (int)($decoded[0] ?? '0');
    }

    public function getOwner(): string {
        $calldata  = AbiEncoder::encodeFunctionCall('owner()', []);
        $rawResult = $this->rpc->ethCall(['to' => $this->address, 'data' => $calldata]);
        $decoded   = AbiEncoder::decodeResult('address', $rawResult);
        return $decoded[0] ?? '';
    }

    // ── Write functions (require signing) ─────────────────────────────────────

    /**
     * Store a new message in the contract.
     *
     * @param  string $message    The message to store
     * @param  string $fromAddress Sender's Ethereum address
     * @param  string $privateKey  Sender's private key (NEVER log this)
     * @return string             Transaction hash
     */
    public function storeMessage(string $message, string $fromAddress, string $privateKey): string {

        // Validate message:
        if (empty($message) || strlen($message) > 280) {
            throw new \InvalidArgumentException("Message must be 1–280 characters");
        }

        // Build calldata:
        $calldata = AbiEncoder::encodeFunctionCall('storeMessage(string)', [$message]);

        // Estimate gas:
        $gasEstimate = $this->rpc->call('eth_estimateGas', [[
            'from'  => $fromAddress,
            'to'    => $this->address,
            'data'  => $calldata,
        ]]);
        // Add 20% buffer to gas estimate:
        $gasLimit = '0x' . dechex((int)(hexdec($gasEstimate) * 1.2));

        // Build transaction:
        $nonce    = $this->rpc->getNonce($fromAddress);
        $gasPrice = $this->rpc->getGasPrice();

        $tx = [
            'nonce'    => '0x' . dechex($nonce),
            'gasPrice' => '0x' . dechex((int)($gasPrice * 1.1)), // 10% above current price
            'gasLimit' => $gasLimit,
            'to'       => $this->address,
            'value'    => '0',
            'data'     => $calldata,
        ];

        // Sign and send:
        $signedTx = TransactionSigner::sign($tx, $privateKey, $this->chainId);
        return $this->rpc->sendRawTransaction($signedTx);
    }

    /**
     * Parse MessageStored events from transaction receipt.
     */
    public function parseEvents(array $receipt): array {
        $eventSignature = \kornrunner\Keccak::hash('MessageStored(address,string,uint256)', 256);
        $events         = [];

        foreach ($receipt['logs'] ?? [] as $log) {
            if (($log['topics'][0] ?? '') !== '0x' . $eventSignature) continue;

            $sender  = '0x' . substr($log['topics'][1] ?? '', 26);
            $decoded = AbiEncoder::decodeResult('string,uint256', $log['data'] ?? '');

            $events[] = [
                'sender'    => $sender,
                'message'   => $decoded[0] ?? '',
                'timestamp' => $decoded[1] ?? '0',
            ];
        }

        return $events;
    }
}

// ── Full example against local Ganache/Hardhat ─────────────────────────────────
$rpc      = new EthereumRpcClient('http://127.0.0.1:8545');
$contract = new SimpleStoreContract($rpc, '0xDeployedContractAddress', 1337);

// Read current state:
echo "Current message: " . $contract->getMessage() . "\n";
echo "Total messages stored: " . $contract->getMessageCount() . "\n";

// Write new message (with test account from Ganache):
$txHash = $contract->storeMessage(
    "Hello from PHP! " . date('Y-m-d H:i:s'),
    '0xTestAccountAddress',
    '0xTestPrivateKey' // Ganache test key — never use real key in code
);
echo "Transaction sent: {$txHash}\n";

// Wait and verify:
$receipt = $rpc->waitForReceipt($txHash, 10, 1);
if ($receipt && $receipt['status'] === '0x1') {
    $events = $contract->parseEvents($receipt);
    echo "Event emitted: " . json_encode($events) . "\n";
    echo "New message: " . $contract->getMessage() . "\n";
}

 

Part 8 — NFT Minting with IPFS (ERC-721)

<?php
declare(strict_types=1);

/**
 * NFT Minting Service
 *
 * Flow:
 * 1. Upload image to IPFS
 * 2. Create metadata JSON and upload to IPFS
 * 3. Call mint() on ERC-721 contract with metadata URI
 * 4. Return token ID and transaction hash
 */
class NftMintingService {

    private EthereumRpcClient $rpc;
    private string $contractAddress;
    private string $ipfsApiUrl;
    private string $ipfsApiKey;
    private int    $chainId;

    public function __construct(
        EthereumRpcClient $rpc,
        string $contractAddress,
        string $ipfsApiUrl,   // e.g., 'https://api.pinata.cloud'
        string $ipfsApiKey,
        int    $chainId = 1
    ) {
        $this->rpc             = $rpc;
        $this->contractAddress = $contractAddress;
        $this->ipfsApiUrl      = $ipfsApiUrl;
        $this->ipfsApiKey      = $ipfsApiKey;
        $this->chainId         = $chainId;
    }

    /**
     * Complete NFT minting flow: upload → metadata → mint.
     *
     * @param  string $imagePath     Path to local image file
     * @param  array  $metadata      NFT attributes
     * @param  string $recipientAddress Who receives the NFT
     * @param  string $minterAddress  Address that pays for gas
     * @param  string $privateKey     Minter's private key (keep this secret!)
     * @return array                  ['token_id', 'tx_hash', 'metadata_uri']
     */
    public function mint(
        string $imagePath,
        array  $metadata,
        string $recipientAddress,
        string $minterAddress,
        string $privateKey
    ): array {

        // ── Step 1: Upload image to IPFS ──────────────────────────────────
        echo "Uploading image to IPFS...\n";
        $imageCid = $this->uploadFileToPinata($imagePath);
        $imageUri = "ipfs://{$imageCid}";

        // ── Step 2: Create and upload metadata JSON ───────────────────────
        $metadataJson = json_encode([
            'name'        => $metadata['name'],
            'description' => $metadata['description'],
            'image'       => $imageUri,
            'external_url'=> $metadata['external_url'] ?? '',
            'attributes'  => $metadata['attributes'] ?? [],
        ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);

        echo "Uploading metadata to IPFS...\n";
        $metadataCid = $this->uploadJsonToPinata($metadataJson, $metadata['name'] . '.json');
        $metadataUri = "ipfs://{$metadataCid}";

        // ── Step 3: Call safeMint() on the NFT contract ───────────────────
        echo "Minting NFT on blockchain...\n";

        // ERC-721 mint function signature (adjust to your contract):
        $calldata = AbiEncoder::encodeFunctionCall(
            'safeMint(address,string)',
            [$recipientAddress, $metadataUri]
        );

        // Estimate gas:
        $gasEstimate = $this->rpc->call('eth_estimateGas', [[
            'from'  => $minterAddress,
            'to'    => $this->contractAddress,
            'data'  => $calldata,
        ]]);
        $gasLimit = '0x' . dechex((int)(hexdec($gasEstimate) * 1.3));

        $nonce    = $this->rpc->getNonce($minterAddress);
        $gasPrice = $this->rpc->getGasPrice();

        $tx = [
            'nonce'    => '0x' . dechex($nonce),
            'gasPrice' => '0x' . dechex((int)$gasPrice),
            'gasLimit' => $gasLimit,
            'to'       => $this->contractAddress,
            'value'    => '0',
            'data'     => $calldata,
        ];

        $signedTx = TransactionSigner::sign($tx, $privateKey, $this->chainId);
        $txHash   = $this->rpc->sendRawTransaction($signedTx);

        echo "Waiting for confirmation...\n";
        $receipt = $this->rpc->waitForReceipt($txHash, 30, 5);

        if (!$receipt || $receipt['status'] !== '0x1') {
            throw new \RuntimeException("Minting transaction failed: {$txHash}");
        }

        // Extract token ID from Transfer event (topic[3] = tokenId for ERC-721):
        $tokenId = null;
        foreach ($receipt['logs'] ?? [] as $log) {
            if (count($log['topics'] ?? []) === 4) {
                $tokenId = hexdec(ltrim($log['topics'][3], '0x'));
                break;
            }
        }

        return [
            'token_id'     => $tokenId,
            'tx_hash'      => $txHash,
            'metadata_uri' => $metadataUri,
            'image_uri'    => $imageUri,
        ];
    }

    /**
     * Upload a file to Pinata IPFS.
     * Pinata provides a free IPFS pinning service with an API.
     */
    private function uploadFileToPinata(string $filePath): string {
        if (!file_exists($filePath)) {
            throw new \InvalidArgumentException("File not found: {$filePath}");
        }

        $boundary = '----FormBoundary' . uniqid();

        $postBody = "--{$boundary}\r\n"
            . "Content-Disposition: form-data; name=\"file\"; filename=\"" . basename($filePath) . "\"\r\n"
            . "Content-Type: " . mime_content_type($filePath) . "\r\n\r\n"
            . file_get_contents($filePath) . "\r\n"
            . "--{$boundary}--\r\n";

        $ch = curl_init($this->ipfsApiUrl . '/pinning/pinFileToIPFS');
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $this->ipfsApiKey,
                "Content-Type: multipart/form-data; boundary={$boundary}",
            ],
            CURLOPT_POSTFIELDS     => $postBody,
            CURLOPT_TIMEOUT        => 60,
        ]);

        $response = curl_exec($ch);
        $code     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($code !== 200) {
            throw new \RuntimeException("IPFS upload failed: HTTP {$code}");
        }

        $data = json_decode($response, true);
        return $data['IpfsHash'] ?? throw new \RuntimeException('No IPFS hash returned');
    }

    private function uploadJsonToPinata(string $jsonContent, string $name): string {
        $payload = json_encode([
            'pinataOptions' => ['cidVersion' => 1],
            'pinataMetadata'=> ['name' => $name],
            'pinataContent' => json_decode($jsonContent, true),
        ]);

        $ch = curl_init($this->ipfsApiUrl . '/pinning/pinJSONToIPFS');
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $this->ipfsApiKey,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS     => $payload,
            CURLOPT_TIMEOUT        => 30,
        ]);

        $response = curl_exec($ch);
        $code     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($code !== 200) {
            throw new \RuntimeException("IPFS metadata upload failed: HTTP {$code}");
        }

        $data = json_decode($response, true);
        return $data['IpfsHash'] ?? throw new \RuntimeException('No IPFS hash returned');
    }
}

Part 9 — WooCommerce Crypto Payment Gateway

<?php
declare(strict_types=1);

/**
 * WooCommerce Ethereum Payment Gateway
 *
 * Adds "Pay with ETH/USDC" as a payment option at checkout.
 * Uses a polling mechanism to detect payments.
 *
 * Install: Place in wp-content/plugins/woo-crypto-pay/woo-crypto-pay.php
 */
if (!defined('ABSPATH')) exit;

add_action('plugins_loaded', function() {
    if (!class_exists('WC_Payment_Gateway')) return;

    class WC_Ethereum_Gateway extends WC_Payment_Gateway {

        public function __construct() {
            $this->id                 = 'ethereum_pay';
            $this->icon               = plugins_url('eth-icon.png', __FILE__);
            $this->has_fields         = false;
            $this->method_title       = 'Crypto Payment (ETH/USDC)';
            $this->method_description = 'Accept Ethereum or USDC payments directly to your wallet.';

            $this->init_form_fields();
            $this->init_settings();

            $this->title          = $this->get_option('title', 'Pay with Crypto (ETH/USDC)');
            $this->merchant_address = $this->get_option('merchant_address');
            $this->infura_key      = $this->get_option('infura_key');
            $this->usdc_contract   = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC mainnet

            add_action('woocommerce_update_options_payment_gateways_' . $this->id,
                       [$this, 'process_admin_options']);

            // Poll for payment confirmation via WP-Cron:
            add_action('woo_crypto_check_payment', [$this, 'check_payment_confirmation'], 10, 1);
        }

        public function init_form_fields(): void {
            $this->form_fields = [
                'enabled'           => ['title' => 'Enable', 'type' => 'checkbox', 'default' => 'yes'],
                'title'             => ['title' => 'Title', 'type' => 'text', 'default' => 'Pay with Crypto'],
                'merchant_address'  => ['title' => 'Your ETH Wallet Address', 'type' => 'text'],
                'infura_key'        => ['title' => 'Infura Project ID', 'type' => 'password'],
                'accepted_tokens'   => [
                    'title'   => 'Accepted Tokens',
                    'type'    => 'multiselect',
                    'options' => ['eth' => 'ETH', 'usdc' => 'USDC'],
                    'default' => ['eth', 'usdc'],
                ],
            ];
        }

        public function process_payment(int $order_id): array {
            $order = wc_get_order($order_id);

            // Get real-time ETH price in order currency:
            $orderTotal    = (float) $order->get_total();
            $orderCurrency = $order->get_currency();
            $ethPrice      = $this->getEthPrice($orderCurrency);
            $ethAmount     = round($orderTotal / $ethPrice, 8);

            // Generate unique payment reference:
            $paymentRef = bin2hex(random_bytes(8));

            // Store payment details in order meta:
            $order->update_meta_data('_crypto_payment_ref',     $paymentRef);
            $order->update_meta_data('_crypto_eth_amount',      $ethAmount);
            $order->update_meta_data('_crypto_order_currency',  $orderCurrency);
            $order->update_meta_data('_crypto_merchant_address',$this->merchant_address);
            $order->update_meta_data('_crypto_expires_at',      time() + 1800); // 30 min
            $order->save();

            // Set order to pending payment:
            $order->update_status('pending', 'Awaiting crypto payment');

            // Schedule payment confirmation checks:
            // Check every 30 seconds for 30 minutes:
            for ($i = 1; $i <= 60; $i++) {
                wp_schedule_single_event(time() + ($i * 30), 'woo_crypto_check_payment', [$order_id]);
            }

            // Redirect to payment page:
            return [
                'result'   => 'success',
                'redirect' => $this->get_return_url($order) . '&crypto_payment=pending&order=' . $order_id,
            ];
        }

        /**
         * Check if ETH payment has been received for an order.
         * Called by WP-Cron every 30 seconds after order placement.
         */
        public function check_payment_confirmation(int $order_id): void {
            $order = wc_get_order($order_id);

            if (!$order || $order->get_status() !== 'pending') return;

            $expectedAmount  = (float) $order->get_meta('_crypto_eth_amount');
            $merchantAddress = $order->get_meta('_crypto_merchant_address');
            $expiresAt       = (int) $order->get_meta('_crypto_expires_at');
            $alreadyPaid     = $order->get_meta('_crypto_tx_hash');

            if ($alreadyPaid) return; // Already marked as paid

            if (time() > $expiresAt) {
                $order->update_status('cancelled', 'Crypto payment window expired');
                return;
            }

            try {
                $rpc = new EthereumRpcClient(
                    'https://mainnet.infura.io/v3/' . $this->infura_key
                );

                // Check ETH transactions to merchant in recent blocks:
                $currentBlock = $rpc->getBlockNumber();
                $fromBlock    = $currentBlock - 20; // Last ~4 minutes

                // Check ETH transfers via transaction logs:
                // (Simplified — full implementation would use eth_getTransactionByHash)
                $balance = $rpc->getBalance($merchantAddress);

                // For simplicity, check current balance vs stored baseline
                // In production: check specific transactions via eth_getLogs or transaction history API
                $storedBalance = (float)($order->get_meta('_crypto_merchant_baseline') ?: '0');
                $currentBalance = (float)$rpc->getBalanceEth($merchantAddress);

                if ($currentBalance >= $storedBalance + $expectedAmount - 0.001) {
                    // Payment detected (within 0.001 ETH tolerance for gas):
                    $order->update_meta_data('_crypto_confirmed_at', time());
                    $order->save();
                    $order->payment_complete('eth_payment_detected');
                    $order->add_order_note("ETH payment confirmed. Expected: {$expectedAmount} ETH");
                }

            } catch (\Throwable $e) {
                error_log("[WooCryptoPay] Check failed for order #{$order_id}: " . $e->getMessage());
            }
        }

        private function getEthPrice(string $currency = 'USD'): float {
            // Cache price for 5 minutes:
            $cacheKey = 'eth_price_' . $currency;
            $cached   = get_transient($cacheKey);
            if ($cached) return (float) $cached;

            $response = wp_remote_get(
                "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=" . strtolower($currency)
            );

            if (is_wp_error($response)) return 3000.0; // Fallback price

            $data  = json_decode(wp_remote_retrieve_body($response), true);
            $price = (float)($data['ethereum'][strtolower($currency)] ?? 3000.0);

            set_transient($cacheKey, $price, 300); // Cache 5 minutes
            return $price;
        }
    }

    add_filter('woocommerce_payment_gateways', function($gateways) {
        $gateways[] = 'WC_Ethereum_Gateway';
        return $gateways;
    });
});

 

Part 10 — Laravel Blockchain Service Layer

<?php
// In a Laravel application:
// app/Services/BlockchainService.php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Event;
use App\Events\TransactionConfirmed;
use App\Events\PaymentReceived;

class BlockchainService {

    private EthereumRpcClient $rpc;
    private int $chainId;

    public function __construct() {
        $endpoint    = config('blockchain.endpoint');
        $this->rpc   = new EthereumRpcClient($endpoint);
        $this->chainId = config('blockchain.chain_id', 1);
    }

    /**
     * Get ETH balance with caching (prevents excessive RPC calls).
     */
    public function getBalance(string $address): string {
        return Cache::remember(
            "eth_balance_{$address}",
            60, // Cache for 1 minute
            fn() => $this->rpc->getBalanceEth($address)
        );
    }

    /**
     * Get ERC-20 token balance.
     */
    public function getTokenBalance(string $tokenAddress, string $walletAddress): string {
        $token = new Erc20Token($this->rpc, $tokenAddress);
        return Cache::remember(
            "token_balance_{$tokenAddress}_{$walletAddress}",
            30,
            fn() => $token->balanceOf($walletAddress)
        );
    }

    /**
     * Submit a signed transaction and dispatch Laravel event on confirmation.
     */
    public function sendAndWatch(string $signedTx, array $metadata = []): string {
        try {
            $txHash = $this->rpc->sendRawTransaction($signedTx);

            Log::info('Transaction submitted', ['tx_hash' => $txHash, 'metadata' => $metadata]);

            // Dispatch a job to watch for confirmation:
            \App\Jobs\WatchTransaction::dispatch($txHash, $metadata)
                ->delay(now()->addSeconds(15));

            return $txHash;

        } catch (\Throwable $e) {
            Log::error('Transaction submission failed', [
                'error'    => $e->getMessage(),
                'metadata' => $metadata,
            ]);
            throw $e;
        }
    }
}

// app/Jobs/WatchTransaction.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class WatchTransaction implements ShouldQueue {
    use Queueable, InteractsWithQueue;

    public int $tries   = 20; // Max 20 retries
    public int $backoff = 15; // 15 seconds between retries

    public function __construct(
        private string $txHash,
        private array  $metadata = []
    ) {}

    public function handle(): void {
        $rpc     = new \EthereumRpcClient(config('blockchain.endpoint'));
        $receipt = $rpc->getTransactionReceipt($this->txHash);

        if ($receipt === null) {
            // Transaction pending — release back to queue:
            $this->release($this->backoff);
            return;
        }

        $success = $receipt['status'] === '0x1';

        \App\Events\TransactionConfirmed::dispatch(
            txHash: $this->txHash,
            success: $success,
            receipt: $receipt,
            metadata: $this->metadata
        );

        if (!$success) {
            \Illuminate\Support\Facades\Log::warning('Transaction reverted', [
                'tx_hash' => $this->txHash,
                'receipt' => $receipt,
            ]);
        }
    }
}

// config/blockchain.php
return [
    'endpoint'     => env('BLOCKCHAIN_RPC_URL', 'http://127.0.0.1:8545'),
    'chain_id'     => env('BLOCKCHAIN_CHAIN_ID', 1),
    'explorer_url' => env('BLOCKCHAIN_EXPLORER', 'https://etherscan.io'),
];

 

Part 11 — Multi-Chain Support (Ethereum, Polygon, BSC)

<?php
/**
 * Multi-chain configuration.
 * The same PHP code works on any EVM-compatible chain — just change the RPC URL and chain ID.
 */
class ChainConfig {

    public const CHAINS = [
        'ethereum' => [
            'name'         => 'Ethereum Mainnet',
            'chain_id'     => 1,
            'rpc_url'      => 'https://mainnet.infura.io/v3/{KEY}',
            'explorer'     => 'https://etherscan.io',
            'native_token' => 'ETH',
            'usdc_address' => '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
        ],
        'polygon' => [
            'name'         => 'Polygon (MATIC)',
            'chain_id'     => 137,
            'rpc_url'      => 'https://polygon-mainnet.infura.io/v3/{KEY}',
            'explorer'     => 'https://polygonscan.com',
            'native_token' => 'MATIC',
            'usdc_address' => '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
            // Polygon is 100× cheaper than Ethereum for gas
        ],
        'bsc' => [
            'name'         => 'BNB Smart Chain',
            'chain_id'     => 56,
            'rpc_url'      => 'https://bsc-dataseed.binance.org/',
            'explorer'     => 'https://bscscan.com',
            'native_token' => 'BNB',
            'usdc_address' => '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
        ],
        'sepolia' => [
            'name'         => 'Sepolia Testnet (Ethereum)',
            'chain_id'     => 11155111,
            'rpc_url'      => 'https://sepolia.infura.io/v3/{KEY}',
            'explorer'     => 'https://sepolia.etherscan.io',
            'native_token' => 'SepoliaETH',
            'usdc_address' => null, // Use test token contracts
        ],
        'localhost' => [
            'name'         => 'Local Ganache/Hardhat',
            'chain_id'     => 1337,
            'rpc_url'      => 'http://127.0.0.1:8545',
            'explorer'     => null,
            'native_token' => 'ETH',
        ],
    ];

    public static function get(string $chain): array {
        if (!isset(self::CHAINS[$chain])) {
            throw new \InvalidArgumentException("Unknown chain: {$chain}");
        }
        $config = self::CHAINS[$chain];
        $config['rpc_url'] = str_replace('{KEY}', getenv('INFURA_KEY'), $config['rpc_url']);
        return $config;
    }

    public static function client(string $chain): EthereumRpcClient {
        $config = self::get($chain);
        return new EthereumRpcClient($config['rpc_url']);
    }
}

// ── Usage: same code, different chains ────────────────────────────────────────
$chains = ['ethereum', 'polygon', 'bsc'];

foreach ($chains as $chainName) {
    $config  = ChainConfig::get($chainName);
    $client  = ChainConfig::client($chainName);
    $balance = $client->getBalanceEth('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
    echo "{$config['name']}: {$balance} {$config['native_token']}\n";
}

 

Part 12 — Security: Private Key Management in Production

<?php
/**
 * PRODUCTION PRIVATE KEY MANAGEMENT
 *
 * NEVER do these (common mistakes):
 * ❌ Store private keys in .env files
 * ❌ Store private keys in databases
 * ❌ Log private keys
 * ❌ Pass private keys via URL parameters
 * ❌ Store private keys in version control
 * ❌ Hardcode private keys in source code
 *
 * DO this instead:
 */

/**
 * Pattern 1: AWS KMS (Key Management Service)
 * AWS manages the private key — PHP only provides data to sign.
 * The raw key never leaves AWS hardware.
 *
 * composer require aws/aws-sdk-php
 */
class AwsKmsEthereumSigner {

    private \Aws\Kms\KmsClient $kms;
    private string $keyId;

    public function __construct(string $keyId, string $region = 'eu-west-1') {
        $this->kms = new \Aws\Kms\KmsClient([
            'region'  => $region,
            'version' => 'latest',
        ]);
        $this->keyId = $keyId;
    }

    /**
     * Sign data using a KMS-managed key.
     * The private key never touches your PHP server.
     */
    public function sign(string $dataHex): string {
        $result = $this->kms->sign([
            'KeyId'            => $this->keyId,
            'Message'          => hex2bin($dataHex),
            'MessageType'      => 'DIGEST',
            'SigningAlgorithm' => 'ECDSA_SHA_256',
        ]);

        return bin2hex($result['Signature']);
    }
}

/**
 * Pattern 2: HashiCorp Vault Transit Secrets Engine
 * Similar to KMS but self-hosted.
 */
class VaultEthereumSigner {

    private string $vaultAddr;
    private string $vaultToken;
    private string $keyName;

    public function __construct(string $vaultAddr, string $vaultToken, string $keyName) {
        $this->vaultAddr  = $vaultAddr;
    $this->vaultToken = $vaultToken;
        $this->keyName    = $keyName;
    }

    public function sign(string $dataBase64): string {
        $ch = curl_init("{$this->vaultAddr}/v1/transit/sign/{$this->keyName}");
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'X-Vault-Token: ' . $this->vaultToken,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS     => json_encode(['input' => $dataBase64]),
        ]);

        $response = json_decode(curl_exec($ch), true);
        curl_close($ch);

        return $response['data']['signature'] ?? throw new \RuntimeException('Vault signing failed');
    }
}

/**
 * Pattern 3: Environment-based (acceptable for development, not production)
 * If you MUST use environment variables, at least:
 * - Use a secrets manager to inject them (not .env files)
 * - Restrict file system access
 * - Never log the value
 */
class EnvBasedSigner {

    private function getPrivateKey(): string {
        $key = getenv('ETH_PRIVATE_KEY');

        if (!$key) {
            throw new \RuntimeException('ETH_PRIVATE_KEY environment variable not set');
        }

        if (!preg_match('/^(0x)?[0-9a-f]{64}$/i', $key)) {
            throw new \RuntimeException('Invalid private key format');
        }

        return $key;
    }

    public function signTransaction(array $tx): string {
        $key = $this->getPrivateKey();
        // Use immediately, don't store in a variable that might be logged
        $signed = TransactionSigner::sign($tx, $key);
        unset($key); // Unset as soon as done
        return $signed;
    }
}

 

Part 13 — Development Setup with Ganache / Hardhat

# ── Install development tools ─────────────────────────────────────────────────

# Install Node.js (required for Hardhat):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install nodejs

# Install Hardhat (modern, fast local blockchain):
mkdir blockchain-project && cd blockchain-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox

# Or Ganache GUI (simpler for beginners):
npm install -g ganache

# ── Start a local blockchain ───────────────────────────────────────────────────

# Option A: Ganache CLI (10 test accounts with 100 ETH each):
ganache --port 8545 --chainId 1337 --accounts 10 --deterministic
# The --deterministic flag generates the same addresses every time
# Default mnemonic: test test test test test test test test test test test junk

# Option B: Hardhat Network:
npx hardhat node

# ── Deploy a contract ──────────────────────────────────────────────────────────

# Initialize Hardhat project:
npx hardhat init

# Deploy to local network (after writing your contract in contracts/):
npx hardhat run scripts/deploy.js --network localhost
# Note the deployed contract address — use it in PHP

# ── PHP Composer packages ─────────────────────────────────────────────────────
# composer.json:
{
    "require": {
        "sc0vu/web3.php": "^0.1.4",
        "kornrunner/keccak": "^1.1",
        "simplito/elliptic-php": "^1.0",
        "ext-bcmath": "*",
        "ext-gmp": "*"
    }
}

# Install dependencies:
composer install

# ── Test your PHP connection to local Ganache ─────────────────────────────────

php -r "
require 'vendor/autoload.php';
\$rpc = new EthereumRpcClient('http://127.0.0.1:8545');
echo 'Block: ' . \$rpc->getBlockNumber() . PHP_EOL;
echo 'Chain: ' . \$rpc->getChainId() . PHP_EOL;
"

 

Part 14 — When to Use PHP + Blockchain (and When Not To)

USE BLOCKCHAIN + PHP WHEN:
─────────────────────────────────────────────────────────────────────────────
✅ Digital ownership is required (NFTs, certificates, licenses)
✅ Audit trail must be tamper-proof (supply chain, medical records)
✅ Trustless payments (no payment processor, peer-to-peer)
✅ DAO governance (community voting, on-chain proposals)
✅ Token-gated content (access control based on NFT ownership)
✅ Cross-border payments (crypto cheaper/faster than wire transfer)
✅ Provenance tracking (luxury goods, art authentication)

DO NOT USE BLOCKCHAIN WHEN:
─────────────────────────────────────────────────────────────────────────────
❌ Simple CRUD app — a database is faster, cheaper, and simpler
❌ GDPR compliance required — blockchain data is permanent and public
❌ High-frequency data (every click, every sensor reading) — too expensive
❌ Private data that must be deletable — immutability conflicts with right-to-erasure
❌ You need fast writes (blockchain: 3-15 seconds, database: <1ms)
❌ Team has no blockchain expertise — security bugs in smart contracts
    can cost millions (see: The DAO hack, $60M lost in 2016)

COST REALITY CHECK (Ethereum mainnet, July 2025):
─────────────────────────────────────────────────────────────────────────────
Simple ETH transfer:         ~$0.50–$3.00 in gas
ERC-20 token transfer:       ~$1.00–$5.00 in gas
Smart contract deployment:   ~$50–$500 in gas
NFT mint:                    ~$5–$50 in gas
Contract function call:      ~$0.50–$20 in gas

Polygon (same code, 100× cheaper):
Simple transfer:             ~$0.001–$0.01
NFT mint:                    ~$0.05–$0.50

→ For high-frequency applications, use Polygon, Base, or Arbitrum
─────────────────────────────────────────────────────────────────────────────

 

Blockchain technology has been making waves in fintech, supply chains, identity systems, and more. Meanwhile, PHP remains one of the most widely used server-side languages powering web applications. The intriguing question: Can PHP and blockchain converge to shape the next generation of web development?

In this article, we explore:

  • What blockchain brings to the Web
  • Why PHP still matters
  • How PHP can be used to interact with blockchain
  • Specific code samples
  • Architectural considerations & challenges
  • A case for real-world adoption
  • Future opportunities & research directions

What Is Blockchain & Why It Matters for Web Dev

To set the stage, here’s a concise overview of blockchain and its relevance in web development.

Key Properties of Blockchain

  1. Decentralization — Instead of a centralized server controlling data, blockchain distributes data among many nodes, reducing single points of failure.
  2. Immutability — Once a block is added, it’s computationally hard to alter previous records, enabling tamper-resistance.
  3. Transparency & Auditability — Transactions on public blockchains are visible and traceable.
  4. Smart Contracts — Self-executing code embedded in the blockchain which run when conditions are met.
  5. Trustless Systems — Participants can transact or exchange without needing a trusted intermediary.

These features offer new paradigms for web apps, particularly for domains like identity, supply chain, voting, decentralized finance (DeFi), and content provenance.

As WEDOWEBAPPS outlines, integrating blockchain into web development yields benefits such as eliminating single server dependency, preventing data tampering, eliminating intermediaries, and enhancing security & transparency. WEDOWEBAPPS

However, blockchain isn’t a silver bullet: for many typical CRUD websites, traditional architectures may still be more efficient. But in scenarios requiring trust, audit, and decentralization, blockchain adds value.

Why PHP Still Has a Place

Although newer frameworks (Node.js, Go, Rust) often dominate blockchain ecosystems, PHP remains relevant for several reasons:

  • Ubiquity & Familiarity: Many developers already know PHP, and many existing systems use it. Leveraging it reduces learning overhead.

  • Rich Ecosystem: Frameworks like Laravel, Symfony, and CodeIgniter provide robust tooling for APIs, middleware, server logic, and templating.
  • Interoperability / Middleware Role: PHP can act as a middle layer, handling user interfaces, web endpoints, and bridging to blockchain layers.
  • Rapid Development: PHP is mature, with many packages, a large developer base, and easier prototyping in many scenarios.

Infuy’s article argues that PHP offers flexibility, interoperability, and speed when building blockchain applications.

That said, PHP is not designed for writing smart contracts, so it typically works around blockchain rather than on it.

How PHP Interacts with Blockchain — Architecture & Code

To build blockchain-enabled web apps, PHP is usually part of a multi-tier architecture. A typical stack might look like:

[Front-end UI (React, Vue, etc.)]  
     ↕  
[PHP Backend / API / Middleware]  
     ↕  
[Blockchain / Smart Contracts / Node Providers]  

PHP would not replace the blockchain or smart-contract layer, but serves as a convenient bridge for HTTP clients, business logic, user auth, database integration, and more.

Libraries & Tools (PHP → Blockchain)

One of the main PHP libraries is web3.php (a PHP port of web3). Infuy shows a sample in their blog. Blockchain Development Company Here’s a refined version:

<?php
require 'vendor/autoload.php';

use Web3\Web3;
use Web3\Contract;

$rpcUrl = 'http://localhost:8545';  // or Infura / Alchemy / other node endpoint
$web3 = new Web3($rpcUrl);

// Get client version
$web3->clientVersion(function ($err, $version) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    echo "Client version: $version\n";
});

// Example: retrieve accounts and balances
$web3->eth->accounts(function ($err, $accounts) use ($web3) {
    if ($err !== null) {
        echo 'Error: ' . $err->getMessage();
        return;
    }
    echo "Accounts: " . json_encode($accounts) . "\n";

    $from = $accounts[0] ?? null;
    $to = $accounts[1] ?? null;
    if ($from && $to) {
        // get balances
        $web3->eth->getBalance($from, function ($err, $bal) use ($from) {
            echo "$from balance: $bal\n";
        });
        $web3->eth->getBalance($to, function ($err, $bal) use ($to) {
            echo "$to balance: $bal\n";
        });
        // send transaction
        $value = '0xde0b6b3a7640000';  // 1 Ether in hex Wei
        $txParams = [
            'from' => $from,
            'to' => $to,
            'value' => $value,
        ];
        $web3->eth->sendTransaction($txParams, function ($err, $txHash) use ($web3, $from, $to) {
            if ($err !== null) {
                echo 'Error: ' . $err->getMessage();
                return;
            }
            echo "Transaction hash: $txHash\n";
            // after sending, get balances again if needed
        });
    }
});
?>

This example covers:

  1. Connecting to a blockchain node via RPC
  2. Getting client version
  3. Retrieving accounts & balance
  4. Sending a basic Ether transaction

Of course, for production, you’d need to manage keys, gas, error handling, and security.

You can also integrate smart contract interactions. For example, using Contract class:

<?php
use Web3\Contract;

$contractAbi = json_decode(file_get_contents('MyContract.abi.json'), true);
$contractAddress = '0xYourContractAddress';
$contract = new Contract($rpcUrl, $contractAbi);

// call a "view" function
$contract->at($contractAddress)
    ->call('myReadFunction', [], function ($err, $result) {
        if ($err !== null) {
            echo "Error: " . $err->getMessage();
            return;
        }
        print_r($result);
    });

// send a transaction (state-changing)
$contract->at($contractAddress)
    ->send('myWriteFunction', [123, "hello"], [
        'from' => '0xYourFromAddress',
        'gas' => '0x76c0',  // gas limit
        'gasPrice' => '0x9184e72a000',  // gas price
    ], function ($err, $txHash) {
        if ($err !== null) {
            echo "Error: " . $err->getMessage();
            return;
        }
        echo "TX Hash: $txHash\n";
    });
?>

Database + Off-chain Storage

Note: not all data fits on-chain (expensive, slow). PHP apps often maintain relational or NoSQL databases to store metadata, user profiles, off-chain indexes, etc. Meanwhile, hashes or references to on-chain data may be stored there.

For example, storing a transaction hash or receipt in MySQL, linked to a user record:

CREATE TABLE user_transactions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  tx_hash VARCHAR(66),
  action VARCHAR(64),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

PHP would insert:

$stmt = $pdo->prepare("INSERT INTO user_transactions (user_id, tx_hash, action) VALUES (?, ?, ?)");
$stmt->execute([$userId, $txHash, 'mintNFT']);

This pattern allows you to retrieve transaction history per user without querying blockchain each time.

Full Example: Simple Token Balance Checker

Let’s build a minimal PHP script that reads the token balance (ERC-20) for a given address.

<?php
require 'vendor/autoload.php';

use Web3\Web3;
use Web3\Contract;

$rpc = 'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID';
$web3 = new Web3($rpc);

// ERC-20 ABI fragment (balanceOf)
$erc20Abi = '[{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"}]';

$contractAddress = '0xTokenContractAddress';
$userAddress = '0xUserAddress';

$contract = new Contract($rpc, json_decode($erc20Abi, true));
$contract->at($contractAddress)
    ->call('balanceOf', $userAddress, function ($err, $result) use ($userAddress) {
        if ($err !== null) {
            echo "Error: " . $err->getMessage();
            return;
        }
        // result is array, first element is balance in Wei
        $balanceWei = $result[0];
        echo "Balance of $userAddress: $balanceWei (in wei)\n";
    });
?>

You may convert Wei to token units by dividing by 10^decimals.

Benefits, Challenges & Considerations

Benefits (as per WEDOWEBAPPS and others)

  • Decentralization & Fault Tolerance: No single point of failure. WEDOWEBAPPS

  • Tamper-Proof Records: Immutable data ensures authenticity. WEDOWEBAPPS
  • Transparent & Auditable: All transactions are visible on-chain. WEDOWEBAPPS
  • Eliminates Intermediaries: Reduces need for central intermediaries or middlemen. WEDOWEBAPPS
  • Better Security for Sensitive Data: Using blockchain reduces risk of unauthorized modifications.
  • Enhanced SEO / Trust Signals: WEDOWEBAPPS also notes that blockchain-backed sites can reduce ad fraud and increase marketing transparency. WEDOWEBAPPS

Challenges & Limitations

  1. Performance & Scalability
    Blockchains are typically slower and have throughput limits. You cannot store heavy data (images, large files) on-chain.
  2. Cost (Gas / Fees)
    Every write operation costs gas, which can be expensive during network congestion.
  3. Complexity
    Requires developers to understand blockchain, cryptography, smart contracts, and key management.
  4. Security Risks
    Smart contract bugs, private key leaks, reentrancy, etc., present serious risks.
  5. Regulatory / Legal Constraints
    Data privacy, compliance, and cross-jurisdiction regulation may complicate adoption.
  6. Interoperability
    Different blockchains, token standards, and APIs must be bridged or integrated. PHP must handle heterogeneity.

Use Cases & Real-World Scenarios

Some example applications where PHP + blockchain integration makes sense:

  • Decentralized Identity (DID)
    PHP backend interfaces with blockchain-based identity verifiers, stores proofs in DB.
  • Tokenized Access / Membership
    Access control (e.g. premium content) via ERC-20 / ERC-721 checks handled by PHP middleware.
  • Supply Chain / Provenance
    Upload metadata off-chain; anchor records and hashes on-chain; use PHP to generate visual front-end.
  • Micropayments / Crypto Payments
    PHP handles payment initiation, listens for confirmations, updates order status.
  • Voting / DAO Interfaces
    PHP web UI connects to DAO smart contracts, displays proposals, lets users vote.
  • NFT Marketplaces / Royalties
    PHP handles listing logic, user dashboards, and interacts with contracts for minting and trades.

PHP’s Real Role in the Blockchain Ecosystem

The original article correctly identifies that PHP works “around” blockchain rather than “on” it. This guide shows what that actually means in practice — and why it’s a powerful position.

PHP is not writing smart contracts (Solidity does that). PHP is not running consensus algorithms (Ethereum nodes do that). PHP is not managing private keys in production (KMS/HSM does that). What PHP is uniquely good at:

The middleware layer — Every blockchain application needs a traditional web interface, user authentication, off-chain data storage, email notifications, payment processing, and business logic that doesn’t belong on-chain. PHP’s mature ecosystem — Laravel, WordPress, Symfony, thousands of packages — covers all of it.

The bridge — JSON-RPC is a simple HTTP protocol. PHP speaks HTTP natively. The EthereumRpcClient in Part 2 of this guide is 100 lines and connects PHP to any EVM-compatible blockchain. No special runtime, no new language to learn.

The business logic host — Smart contracts are intentionally minimal. They enforce rules, emit events, and store state. The actual business logic — which products to display, which users qualify for a discount, how to format a transaction confirmation email — lives in PHP.

The developers who understand both the blockchain protocol layer (JSON-RPC, ABI encoding, transaction signing) and the PHP application layer (Laravel, MySQL, Redis, caching) are the ones who can build complete Web3 applications — not just demos.

This guide gives you the protocol layer. Your existing PHP knowledge covers the rest.

 

Quick Reference: Essential Blockchain Concepts for PHP Developers

UNIT CONVERSIONS
─────────────────────────────────────────────────────────────────────────────
1 ETH  = 1,000,000,000,000,000,000 Wei (10^18)
1 USDC = 1,000,000 units (10^6 — USDC has 6 decimals)
1 WBTC = 100,000,000 units (10^8 — like Bitcoin's satoshis)
Gas price × Gas used = Transaction fee in Wei

CHAIN IDS (For Transaction Signing)
─────────────────────────────────────────────────────────────────────────────
1      Ethereum Mainnet
137    Polygon Mainnet
56     BNB Smart Chain Mainnet
42161  Arbitrum One
8453   Base
10     Optimism
11155111 Sepolia Testnet
80001  Polygon Mumbai Testnet
1337   Ganache/Hardhat local (default)

KEY JSON-RPC METHODS
─────────────────────────────────────────────────────────────────────────────
eth_getBalance           → Balance in Wei (hex)
eth_call                 → Read-only contract call (no gas)
eth_sendRawTransaction   → Submit signed transaction
eth_getTransactionReceipt→ Get tx result after mining
eth_estimateGas          → Estimate gas before sending
eth_gasPrice             → Current gas price
eth_getTransactionCount  → Get nonce for address

PHP PACKAGES
─────────────────────────────────────────────────────────────────────────────
sc0vu/web3.php           Callback-based Ethereum client
kornrunner/keccak        Keccak-256 hashing (not SHA-3!)
simplito/elliptic-php    ECDSA signing on secp256k1
ext-bcmath               Large number arithmetic (always required)
ext-gmp                  Alternative large number library
aws/aws-sdk-php          AWS KMS for production key management

 

Frequently Asked Questions

+

What is blockchain technology?

Blockchain is a decentralized digital ledger that records transactions across multiple computers, making data transparent, tamper-resistant, and difficult to alter after it has been confirmed.
+

Can PHP be used for blockchain development?

Yes. While PHP isn't typically used to write smart contracts, it is widely used to build web applications that interact with blockchain networks through APIs, SDKs, and RPC endpoints.
+

Can PHP create smart contracts?

No. Smart contracts are generally written in blockchain-specific languages such as Solidity (Ethereum), Rust (Solana), or Move (Aptos and Sui). PHP is commonly used as the backend layer that communicates with deployed smart contracts.
+

How does PHP interact with blockchain networks?

PHP typically communicates with blockchain nodes using JSON-RPC, REST APIs, or libraries such as web3.php. It can retrieve balances, send transactions, read smart contract data, and integrate blockchain functionality into web applications.
+

What are the benefits of combining PHP with blockchain?

Using PHP with blockchain offers:

  • Secure transaction processing
  • Decentralized data verification
  • Transparent audit trails
  • Faster web application development
  • Easy integration with existing PHP frameworks like Laravel and Symfony
+

Which PHP frameworks are suitable for blockchain applications?

Popular choices include:

  • Laravel
  • Symfony
  • CodeIgniter
  • Laminas
  • Slim Framework

These frameworks help build APIs, dashboards, authentication systems, and middleware for blockchain-enabled applications.

+

Which blockchain networks can PHP connect to?

PHP can interact with many blockchain platforms, including:

  • Ethereum
  • Polygon
  • Binance Smart Chain (BNB Chain)
  • Avalanche
  • Arbitrum
  • Optimism
  • Hyperledger Fabric
  • Bitcoin (via appropriate APIs)
+

Can PHP build decentralized applications (dApps)?

Yes. PHP can power the backend of a decentralized application by handling user authentication, business logic, databases, and communication with blockchain networks, while the blockchain manages decentralized data and smart contracts.
+

What is the role of PHP in Web3 development?

PHP often serves as middleware between the frontend and blockchain infrastructure. It manages APIs, databases, user sessions, payments, notifications, and blockchain interactions.
+

Is blockchain suitable for every PHP application?

No. Traditional databases are often more efficient for standard CRUD applications. Blockchain is most valuable when applications require decentralization, transparency, immutability, or trustless verification.
+

Is blockchain more secure than a traditional database?

Blockchain provides strong tamper resistance and decentralized verification, but it does not replace general application security. Secure coding, authentication, encryption, and infrastructure protection remain essential.
+

Can PHP interact with NFTs and smart contracts?

Yes. PHP can call smart contract functions, read blockchain data, monitor transactions, and integrate NFT marketplaces through blockchain APIs and RPC providers.
+

Is PHP still relevant for blockchain projects?

Yes. While PHP isn't used to build blockchain protocols themselves, it remains valuable for developing dashboards, APIs, admin panels, e-commerce systems, and enterprise applications that integrate with blockchain technologies.
+

What is the future of PHP in blockchain development?

PHP is expected to continue playing an important role as a backend integration layer for Web3 applications, especially in businesses that already rely on PHP-based systems such as WordPress, Laravel, Magento, and custom enterprise platforms.
Previous Article

AI Chatbot for Shopify — The Complete Production Developer's Guide

Next Article

WordPress SQL Injection - The Complete Developer's Prevention & Detection Guide

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨