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

Web3 Laravel Package

web3-php/web3

Web3 PHP is a PHP library for interacting with Ethereum and EVM blockchains via JSON-RPC. It supports wallets, transactions, smart contracts, events, and Laravel integration, making it easy to build decentralized apps in PHP.

Deep Wiki
Context7

Getting Started

Install and point at a node. The client is a thin JSON-RPC wrapper — nothing works until you have an Ethereum node (Anvil/Hardhat locally, Alchemy/Infura/self-hosted in staging and prod).

composer require web3p/web3.php   # published name for web3-php/web3
use Web3\Web3;

$web3 = new Web3('http://127.0.0.1:8545');   // or new Web3('https://mainnet.example/v3/KEY')

Where to look first, in this order:

  1. Web3::eth — everything you actually use day-to-day (blockNumber, getBalance, call, sendRawTransaction, getTransactionReceipt, getLogs).
  2. Web3\Contract — the ABI layer. This is where 80% of your code will live.
  3. Web3\Utils — toWei, fromWei, toHex, sha3, isAddress, toChecksumAddress. Learn these before writing any conversion code by hand.
  4. Web3\Providers\HttpProvider — swap it out when you need timeouts, retries, or a different transport.

First use case — read the chain and read a contract:

$web3->eth->blockNumber(function ($err, $block) {
    if ($err !== null) { throw new RuntimeException($err->getMessage()); }
    echo $block->toString();          // BigInteger, not int
});

$web3->eth->getBalance('0xabc...', function ($err, $balance) {
    echo \Web3\Utils::fromWei($balance, 'ether');
});
use Web3\Contract;

$contract = new Contract($web3->provider, $erc20AbiJsonArray);
$contract->at('0xToken...')->call('balanceOf', '0xWallet...', function ($err, $result) {
    echo $result[0]->toString();      // returns always come back as an array
});

If those two snippets work, you have everything you need for read-only features; writes are the same API with send instead of call.

Implementation Patterns

Wrap the client in a single service, not scattered new Web3(...). The provider holds no state you want duplicated, but config (RPC URL, chain ID, contract addresses, confirmations) does. In Laravel, bind it as a singleton and inject it:

// app/Providers/Web3ServiceProvider.php
$this->app->singleton(Web3::class, fn () => new Web3(config('web3.rpc_url')));

$this->app->bind(TokenContract::class, fn ($app) => (new Contract(
    $app->make(Web3::class)->provider,
    config('web3.abis.erc20')
))->at(config('web3.addresses.token')));

Keep ABIs as JSON files under resources/abis/ and load the array — not the full Truffle/Hardhat artifact object. Passing the whole artifact is the single most common cause of "invalid ABI" errors.

Always separate call from send. call is eth_call — free, instant, no state change, ideal for balances/views. send broadcasts a transaction and costs gas. Your application code should make that distinction explicit in method names (balanceOf() vs transfer()), because the failure modes are completely different: call fails on revert, send fails on revert or on gas/nonce/replacement issues.

Write path pattern: estimate → send → wait for receipt. The client does not do this for you.

$contract->at($address)->send(
    'transfer', $to, $amount,
    [
        'from'  => $from,
        'value' => '0x0',                  // hex string, not int
        'gas'   => '0x' . dechex($estimated),
        'gasPrice' => \Web3\Utils::toHex(\Web3\Utils::toWei('20', 'gwei')),
    ],
    function ($err, $txHash) use ($web3) { /* ... */ }
);

Then poll eth_getTransactionReceipt (the result is null until mined — a null receipt is not an error) with a bounded retry loop and backoff, and check status === '0x1'. Store the tx hash the moment you get it so a crash mid-wait is recoverable.

Nonce and gas management belong to you. Fetch eth_getTransactionCount($from, 'pending') and track it per sending address (a DB row or an atomic cache key) if you send more than one transaction at a time. Two concurrent sends with 'latest' nonces will collide. For anything beyond a toy app, either serialize sends through a queue job with a single worker per address, or sign and broadcast manually with eth_sendRawTransaction.

Cache aggressively on the read side. eth_call, getBalance, and getBlockByNumber('latest') are idempotent — cache them for a few seconds (or one block) in Redis. This is the difference between a usable UI and rate-limit errors, especially on free-tier hosted RPC.

Handle amounts as strings/BigInteger end to end. Convert at the edges only:

use Web3\Utils;

$wei = Utils::toWei('1.5', 'ether');          // BigInteger
$hex = '0x' . $wei->toHex();                 // RPC quantity
$back = Utils::fromWei($wei, 'ether');       // "1.5"

Token amounts with non-18 decimals need toWei($amount, $decimals) passed as an integer — never hardcode 18.

Testing. Run Anvil or Hardhat node in CI, seed with deterministic accounts, and assert on receipts rather than on tx hashes (hashes change with timestamps/gas). Fork mainnet in Anvil when you need to exercise integrations against real contracts.

Gotchas and Tips

The package is archived. Treat it as frozen: pin the exact version in composer.json, vendor the lockfile, and budget an exit. Practical options are forking it, or moving the chain-touching code behind a small HTTP sidecar (Node/viem, or a Go service) and calling that from PHP. Do not expect PRs, PHP 8.x fixes, or new RPC method coverage upstream. Audit it against your minimum PHP version before committing.

Every method takes a callback, and the error is the first argument. function ($err, $result) — if you write function ($result), you silently read the error as the result. Wrap calls in a helper that throws on $err so failures surface as exceptions rather than nulls deep in a view.

Numeric results are phpseclib BigInteger objects, not ints. (int) $result will truncate or explode on large balances. Always ->toString() and compare as strings, or use bccomp/gmp_cmp. The same applies to blockNumber, gasPrice, and the value/gas fields inside block objects.

Hex quantities vs hex data. RPC quantities must be minimally-encoded hex (0x0, 0x1a, no leading zeros); addresses, hashes, and contract calldata are 32-byte-padded hex data. Passing a decimal string or "20" in a value field gets you an opaque -32602 invalid argument from the node. Utils::toHex handles integers; it does not make an address checksummed.

Checksum addresses. Some nodes reject all-lowercase addresses with -32000 invalid argument. Normalize with Utils::toChecksumAddress($addr) on the way in.

toWei/fromWei second argument. It expects the unit name ('ether', 'gwei', 'wei') or a decimal integer for token math. fromWei($v, 6) for USDC; fromWei($v, 'ether') for ETH. Mixing them up is a silent 10^12 factor error.

Reorgs and "confirmed" ≠ "mined". A receipt with status: 0x1 at 1 confirmation can still be reorged out. For anything moving money, require N confirmations by comparing blockNumber of the receipt against the current head, and reconcile with an idempotent job keyed on tx hash.

Contract return values are always an array, even for a single return, and tuple/array outputs may come back nested or as raw hex depending on the ABI shape — decode those manually with the ABI types if the shape looks wrong. Log the raw $result before assuming the decoder mangled it.

eth_getLogs is the event story. There's no built-in event subscription on HTTP; you filter by address + topics and decode data yourself against the ABI. Indexed dynamic types are hashed into topics and cannot be recovered — only filterable. Keep block ranges small (most providers cap at 1k–10k blocks) and use fromBlock/toBlock deliberately; the default latest silently returns nothing.

Provider quirks. HttpProvider has no retry or timeout policy of its own beyond the underlying Guzzle client — configure timeouts explicitly or a single slow node will stall your request. WebsocketProvider/IpcProvider exist but add deployment surface (and IPC is a non-starter on some hosts); use HTTP until you genuinely need subscriptions.

Debugging: log the JSON-RPC payload. When a call fails, the useful error is the request body, not the PHP exception. A small decorator around the provider that dumps method + params + raw response will save hours; the node's error.data.revertReason field is where actual contract revert messages live.

Extension points are thin. You can subclass Provider to add retries/logging, and you can pass a prebuilt Contract instance around, but there is no middleware pipeline, no batch-request support, and no automatic ABI encoding beyond what Contract does. Plan for that: put batching and resilience in your own service layer, not in the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager