Basic Usage
This page provides the minimal steps to read (eth_call) and write (async EIP-1559) contract functions using the Laravel EVM package. For events, log filtering and full API details see the Advanced Usage and Reference pages.
Contract Handle
$abi = file_get_contents(base_path('abi/ERC20.json'));
$contract = \Farbcode\LaravelEvm\Facades\Evm::at('0xTokenAddress', $abi);at() returns a new handle bound to that contract; it does not modify the client it was called on. Two handles are therefore independent:
$usdc = Evm::at('0xA0b8...', $abi);
$dai = Evm::at('0x6B17...', $abi);
$usdc->call('symbol'); // still USDCReads (eth_call)
$symbol = $contract->call('symbol')->as('string');
$balance = $contract->call('balanceOf', ['0xUser'])->as('uint256');Integers come back as decimal strings, whatever their size. A wei amount does not fit in a PHP integer, so returning one would silently lose precision. Use bcmath or GMP to work with them:
$whole = bcdiv($balance, bcpow('10', '18'), 6);Reads are synchronous and return a CallResult wrapper for convenience decoding.
Decoding Convenience
Any raw hex result from call() is wrapped in CallResult. Supported as() types:
string,bytesuint,uint8…uint256- returned as a decimal stringint,int8…int256- returned as a decimal string, negatives resolvedbooladdress
as() decodes a single return value. A function returning a tuple needs its own decoding.
Example:
$name = $contract->call('name')->as('string');
$totalSupply = $contract->call('totalSupply')->as('uint256');
$isPaused = $contract->call('paused')->as('bool');If you need the original hex use ->raw() or cast to string.
Error Handling for Reads
A revert and an outage are different problems and have different exceptions:
use Farbcode\LaravelEvm\Exceptions\RpcErrorException;
use Farbcode\LaravelEvm\Exceptions\RpcTransportException;
try {
$value = $contract->call('someFn');
} catch (RpcErrorException $e) {
// the node answered: a revert, bad arguments, insufficient funds
if ($e->isRevert()) {
report($e->rpcData); // ABI encoded revert reason, when supplied
}
} catch (RpcTransportException $e) {
// no endpoint could be reached - retry later
}Reads should not emit failure events; only CallPerformed is emitted on success.
Writes (Async Transactions)
$requestId = $contract->sendAsync('transfer', ['0xRecipient', '1000000000000000000']);Amounts may be given as an int, a decimal string or 0x-hex. Pass anything above PHP_INT_MAX as a string, not an int.
Sending Value
For a payable function, put the amount in wei into $opts:
$contract->sendAsync('deposit', [], ['value' => '1000000000000000000']); // 1 ETHTransactional Safety
sendAsync() performs an irreversible action, and the job waits for the surrounding database transaction to commit before it runs. A rollback therefore cancels the broadcast rather than racing it.
Writes enqueue a SendTransaction job. You need a running queue worker for progress (unless using the sync queue driver).
Attaching Context Payload
You can attach any serializable payload (e.g. an Eloquent model instance) that will travel through all lifecycle events:
$order = Order::find(123);
$requestId = $contract->sendAsync('transfer', ['0xRecipient', 1000], [], $order);Each emitted event (TxQueued, TxBroadcasted, TxReplaced, TxMined, TxFailed) will expose $payload so you can correlate blockchain progress with your domain object.
Tracking a Transaction
With EVM_TRACKING=true and the published migration, every transaction is recorded under the id sendAsync() returned:
use Farbcode\LaravelEvm\Models\EvmTransaction;
$requestId = $contract->sendAsync('transfer', [$to, $amount]);
EvmTransaction::where('request_id', $requestId)->first()?->status;
// queued | broadcast | mined | reverted | failedTransaction Job Lifecycle
The queued job executes these steps:
- ABI encode function + args.
- Gas estimation with padding.
- Nonce retrieval (preventing collisions).
- EIP-1559 fee suggestion.
- Transaction build & signature.
- Broadcast to RPC.
- Receipt polling until mined or timeout.
- Optional fee bump & replacement attempts.
Events provide visibility: TxQueued, TxBroadcasted, TxReplaced, TxMined, TxReverted, TxFailed.
TxMined means mined and successful. A transaction that was included but reverted emits TxReverted instead: gas was spent and no state change happened, so it must not be treated as a success.
Common Write Pitfalls
- Stuck Pending: Increase priority fee.
- Nonce Errors: Ensure only one worker per signing address.
Queue Configuration & Workers
By default outgoing transactions are placed on the queue name defined in config/evm.php (config('evm.tx.queue'), .env: EVM_QUEUE, default: evm-send). Ensure your QUEUE_CONNECTION (recommended: redis) is configured.
If you set the queue driver to sync, jobs execute inline and no worker is required. This is acceptable for local experiments but not recommended in production (no concurrency control, risk of nonce clashes, blocking HTTP requests).
Start a dedicated worker for the send queue:
php artisan queue:work --queue=evm-send --timeout=400Transactions for one signing address are serialised by a cache lock, so a second worker queues behind the first instead of racing the nonce. Running one worker per signing address is still the simpler setup and keeps ordering obvious. For higher throughput, add signing addresses rather than processes.
Set --timeout at least as high as the job's own: with the shipped defaults a transaction can take (1 + max_replacements) * confirm_timeout seconds before it gives up.
Horizon Example
When using Laravel Horizon, configure a separate supervisor for evm-send:
// config/horizon.php (excerpt)
return [
'defaults' => [
// ...
'supervisor-2' => [
'connection' => 'redis',
'queue' => ['evm-send'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1, // DO NOT raise for same signer; keep 1 to preserve nonce ordering
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
'timeout' => 400, // must cover confirm_timeout plus every replacement
'nice' => 0,
],
],
'environments' => [
'production' => [
// ...
'supervisor-2' => [
'maxProcesses' => 1,
],
],
'local' => [
// ...
'supervisor-2' => [
'maxProcesses' => 1,
],
],
],
];Production can scale the default queue separately while keeping evm-send serialized.
Debugging Writes
If a transaction never appears:
- Verify worker running (
queue:work --queue=evm-send). - Check emitted events order.
- Inspect RPC responses (enable verbose logging around
eth_sendRawTransaction). - Confirm chain ID and private key match network.
- Bump priority fee.
Waiting for Receipt
use Farbcode\LaravelEvm\Support\Receipt;
$receipt = $contract->wait('0xTxHash');
if (Receipt::isSuccessful($receipt)) {
// mined and did not revert
}A receipt only proves that the transaction was included. Receipt::isReverted() tells the two apart. Wait uses polling; no fee replacement logic here.
Gas Estimation
estimateGas() takes encoded calldata, not a result:
$data = app(\Farbcode\LaravelEvm\Contracts\AbiCodec::class)
->encodeFunction($abi, 'transfer', ['0xRecipient', '1000']);
$gas = $contract->estimateGas($data);Adds configurable padding to avoid underestimation. sendAsync() estimates on its own, so this is only needed for a cost preview.
Raw RPC
$block = \Farbcode\LaravelEvm\Facades\EvmRpc::call('eth_blockNumber');Direct access for diagnostics or unsupported methods.
Proceed to Advanced Usage for log filtering, events, payload handling details and custom components.