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

Laravel Evm Laravel Package

farbcode/laravel-evm

Simple, reliable Ethereum integration for Laravel with EIP-1559 fees, queued async transactions, nonce tracking, and events for every step from queued to mined. Requires PHP 8.4+ and ext-gmp.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites, before you write a line of code. PHP 8.4+, Laravel 11/12/13, and the GMP extension (ext-gmp) enabled. Confirm with php -m | grep gmp — the package declares it in composer.json, but a missing extension surfaces as a runtime error the first time the maths runs, not at boot.

composer require farbcode/laravel-evm
php artisan vendor:publish --tag="evm-config"

Then the three environment values that every deployment needs:

EVM_CHAIN_ID=137
EVM_RPC_1=https://polygon-mainnet.g.alchemy.com/v2/KEY
EVM_PRIVATE_KEY=0xabc123...64hex

Run a queue worker. Writes are asynchronous by default and go to the evm-send queue. If you forget this, sendAsync() returns a job id and nothing ever happens on chain — the single most common "the package is broken" report.

php artisan queue:work --queue=evm-send --timeout=180

Keep the worker --timeout above the job's own $timeout, otherwise the worker kills the job mid-poll on receipt and the transaction is orphaned.

Where to look first. The docs site (laravel-evm.farbcode.net) for API shape, the published config/evm.php for every knob you will actually touch (rpc.timeout, rpc.tries, tx.gas_floor, logs.max_chunk, fee floors), and UPGRADING.md if you are coming from anything below 0.4.0 — that release changes computed values, not just internals.

First use case: read, then write. Start with a read so you validate RPC connectivity without touching a key:

use Farbcode\LaravelEvm\Facades\Evm;

$abi = file_get_contents(storage_path('app/abi/ERC20.abi.json'));
$contract = Evm::at('0xToken', $abi);

$balance = $contract->call('balanceOf', ['0xUser'])->as('uint256');
// "1000000000000000000"  — a decimal *string*, always

Only then attempt a write, and keep the returned id:

$jobId = $contract->sendAsync('transfer', ['0xRecipient', '1000000000000000000']);

Store that id (or enable the optional evm_transactions table) — it is your only handle for reconciling the transaction later.

Implementation Patterns

Wrap the client in a small application service. The contract handle is cheap and stateless-per-instance now, so instantiate per call rather than reaching for Evm::at() all over controllers:

final class TokenService
{
    public function __construct(private string $address, private string $abi) {}

    private function contract(): Contract
    {
        return Evm::at($this->address, $this->abi);
    }

    public function balanceOf(string $wallet): string
    {
        return $this->contract()->call('balanceOf', [$wallet])->as('uint256');
    }

    public function transfer(string $to, string $amount): string
    {
        return $this->contract()->sendAsync('transfer', [$to, $amount]);
    }
}

This gives you one place to swap chain, ABI, or add logging, and it keeps the facade out of your domain code.

Treat writes as two phases: enqueue and confirm. sendAsync() proves nothing about the chain. Listen for events rather than polling in a controller:

Event::listen(TxMined::class, function ($event) {
    // receipt confirmed — but still check status
});

Event::listen(TxReverted::class, function ($event) {
    // mined and rolled back: no balance moved
});

Because the workflow is event-driven (TxQueued, TxBroadcasted, TxMined, TxReverted, TxFailed), model your side effects as listeners on those events and keep the request/response path short. For the synchronous case, use wait() and check the receipt explicitly:

use Farbcode\LaravelEvm\Support\Receipt;

$receipt = $contract->wait('0xTxHash');

if (Receipt::isSuccessful($receipt)) {
    // mined, did not revert
}

Native transfers use the same path. No ABI, no contract — pass value in the options array:

Evm::sendAsync(null, [], ['value' => '1000000000000000000']);

Indexer / log-decoding pattern. For anything reading chain history (deposits, transfers, fills), build the filter and decode in one pass. Use chunked() for wide ranges rather than a single get() over millions of blocks:

$logs = EvmLogs::query()
    ->fromBlock($cursor)
    ->toBlock('latest')
    ->address($token)
    ->eventByAbi($abi, 'Transfer')
    ->topicAny(1, [
        LogFilterBuilder::padAddress($from),
        LogFilterBuilder::padAddress($to),
    ])
    ->chunked(); // honours evm.logs.max_chunk

foreach ($logs as $log) {
    $decoded = LogFilterBuilder::decodeEvent($abi, $log);
    // args come back as decimal strings — never cast to int
}

Persist the last processed block as your cursor and make the job idempotent; logs can be re-read.

Bind your own Signer for key custody. The Signer contract exists precisely so you do not put a raw private key in production .env. Implement sign() against KMS/HSM and bind it; the rest of the pipeline is unchanged.

Swap NonceManager and FeePolicy for chain-specific behaviour. Both are contracts with suggest(FeeSnapshot) / invalidate(); override them when a chain has unusual fee dynamics instead of fighting the config.

Respect one-transaction-at-a-time per signing address. The package enforces it with a cache lock. That means your throughput per key is serial — if you need parallel sends, use multiple signing addresses (separate config/connection or separate worker pools), not more workers on one key.

Testing. Do not mock the facade; mock your service and use an Anvil/Hardhat node for integration tests. Assert on decimal strings, not ints, so your tests catch precision regressions.

Gotchas and Tips

Every integer is a decimal string. This is the 0.4.0 change that silently corrupts applications. CallResult::as() and decodeEvent() used to return int for small values and bare hex above ~1.15 tokens. Do all arithmetic with bcmath or GMP, never (int) or +:

bccomp($balance, '1000000000000000000', 0); // correct
$balance > 0;                               // PHP juggling, fragile

A receipt only proves inclusion. Before 0.4.0, TxMined fired for reverted transactions and listeners credited balances for rolled-back transfers. Always gate on Receipt::isSuccessful().

at() returns a new handle; the binding is no longer a singleton. Two contract handles are now independent. If your code relied on the old retargeting behaviour, it was a latent bug — audit any place that called Evm::at() and then reused a previously captured variable.

Never compare or manipulate wei with int. Amounts above PHP_INT_MAX were previously clamped in calldata — transfer($to, '10000000000000000000') encoded 9.22 tokens. That is fixed, but only if you keep passing strings.

Pass all arguments, even optional ones. A forgotten argument previously became 0, which for minOut or deadline silently removes the guard. The encoder now rejects invalid input instead of reshaping it, so expect exceptions — that is the fix working.

Configure fees per chain. Defaults moved to 1/0 gwei with multiplier 2 because the chain now supplies baseFee. On Polygon set EVM_MIN_PRIORITY_GWEI=30. On Ethereum, check tx.gas_floor (default 21 000) — it was once hard-coded to 150 000 and reserved seven times the funds a plain transfer needs.

Nonce cache can drift. A dropped response or lost transaction puts the cache out of step with the chain. Call NonceManager::invalidate() on your recovery path instead of restarting workers.

already known and nonce too low mean success. They indicate the node accepted the transaction. Older versions classified them as endpoint failures and emitted TxFailed; make sure your listeners do not treat those as fatal if you have legacy code paths.

Fee bumps produce multiple hashes. Poll the original and replacement hashes — if the original wins the race and you only watch the newest hash, you report a mined transaction as failed.

Topic filters: use topicAny(1, ...) deliberately. Index 1 is the first indexed parameter (topic0 is the event signature). Filtering on topic2 alone was previously dropped entirely, returning every log in range while looking filtered.

Keep provider keys out of logs. All four RPC log calls used to emit the full endpoint URL — including the API key in the path. Audit any custom logging you wrapped around the client; a single provider outage copied every key into laravel.log.

Log ranges are bounded by evm.logs.max_chunk. chunked() reads it, but a raw get() over a huge range will still hit provider limits. Always page.

Extension points, in order of usefulness: Signer (key custody), FeePolicy (chain fee strategy), NonceManager (recovery), the five events (side effects), and config/evm.php (everything else). Reach for these before forking.

Pin the version and read UPGRADING.md before bumping. 0.4.0 changed returned types, receipt semantics, signer API, and fee derivation — four separate things that can alter what your application computes without throwing. The test suite went from 14 tests to 165 in that release; a package at 4 stars with this much recent churn deserves a changelog read and an integration test on a testnet before production deploy.

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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc