Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions docs/pages/advanced-events.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
# Events

Transactional events provide lifecycle visibility for asynchronous writes. Use them for logging, metrics, user
notifications, and fee escalation monitoring.
notifications, and fee escalation monitoring. Each write-related event includes an optional `$payload` that you can attach via `sendAsync(..., $payload)`.

## Event List

| Event | When Fired | Key Fields | Purpose |
|-----------------|-----------------------|-----------------------------------------|-------------------------------------|
| `TxQueued` | Job dispatched | request_id, function, address | Track submission before broadcast |
| `TxBroadcasted` | First raw tx accepted | tx_hash, nonce, fees | Persist hash, start monitoring |
| `TxReplaced` | Replacement broadcast | old_tx_hash, new_tx_hash, attempt, fees | Fee bump / speed-up diagnostics |
| `TxMined` | Receipt obtained | tx_hash, receipt | Success; update state models |
| `TxFailed` | Terminal failure | reason, attempts | Alert; possible manual intervention |
| `CallPerformed` | Read call completed | from, to, function, raw_result | Auditing read queries |
| Event | When Fired | Key Fields (excerpt) | Purpose |
|-----------------|-----------------------|-------------------------------------|-------------------------------------|
| `TxQueued` | Job dispatched | to, data, payload | Track submission before broadcast |
| `TxBroadcasted` | First raw tx accepted | txHash, fields(fees,nonce), payload | Persist hash, start monitoring |
| `TxReplaced` | Replacement broadcast | oldTxHash, newFields, attempt, payload | Fee bump / speed-up diagnostics |
| `TxMined` | Receipt obtained | txHash, receipt, payload | Success; update state models |
| `TxFailed` | Terminal failure | to, data, reason, payload | Alert; possible manual intervention |
| `CallPerformed` | Read call completed | from, address, function, rawResult | Auditing read queries |

`payload` is `mixed` and can hold any serializable context (e.g. Eloquent model) to correlate app state and blockchain lifecycle.

## Attaching Payload Example

```php
$order = Order::find(123);
$contract->sendAsync('transfer', ['0xRecipient', 1000], [], $order);
```

## Example Listener Registration

Expand All @@ -37,8 +46,12 @@ class LogTxBroadcasted
{
Log::info('TX broadcasted', [
'hash' => $e->txHash,
'nonce' => $e->nonce,
'fees' => $e->fees,
'nonce' => $e->fields['nonce'] ?? null,
'fees' => [
'maxFeePerGas' => $e->fields['maxFeePerGas'] ?? null,
'maxPriorityFeePerGas' => $e->fields['maxPriorityFeePerGas'] ?? null,
],
'payload_id' => method_exists($e->payload, 'getKey') ? $e->payload->getKey() : null,
]);
}
}
Expand Down Expand Up @@ -84,4 +97,3 @@ On `TxFailed`, inspect `reason`:
- Gas estimation failure: review contract & args.
- Broadcast rejection: fees or nonce invalid.
- Timeout without receipt: consider manual bump or external explorer check.

13 changes: 12 additions & 1 deletion docs/pages/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ $requestId = $contract->sendAsync('transfer', ['0xRecipient', 1000]);
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:

```php
$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.

### Transaction Job Lifecycle

The queued job executes these steps:
Expand Down Expand Up @@ -181,4 +192,4 @@ $block = \Farbcode\LaravelEvm\Facades\EvmRpc::call('eth_blockNumber');

Direct access for diagnostics or unsupported methods.

Proceed to Advanced Usage for log filtering and custom components.
Proceed to Advanced Usage for log filtering, events, payload handling details and custom components.
40 changes: 20 additions & 20 deletions docs/pages/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@

## ContractClient (via `Evm`)

| Method | Args | Returns | Notes |
|--------------------------------------------|----------------------------|----------------------|------------------------------------------|
| `at(address, abi)` | `string`, `array\| string` | self | Set target contract & ABI JSON/array |
| `call(fn, args=[])` | `string`, `array` | `CallResult\| mixed` | eth_call; hex wrapped for decoding |
| `sendAsync(fn, args=[], opts=[])` | `string`, `array`, `array` | `string` | Dispatch async job; returns request UUID |
| `wait(txHash, timeoutSec=120, pollMs=800)` | `string`, `int`, `int` | `array\| null` | Poll receipt until mined/timeout |
| `estimateGas(data, from?)` | `string`, `?string` | `int` | Uses eth_estimateGas + padding |
| Method | Args | Returns | Notes |
|-------------------------------------------------|-----------------------------------|----------------------|------------------------------------------------------------------------|
| `at(address, abi)` | `string`, `array\| string` | self | Set target contract & ABI JSON/array |
| `call(fn, args=[])` | `string`, `array` | `CallResult\| mixed` | eth_call; hex wrapped for decoding |
| `sendAsync(fn, args=[], opts=[], payload=null)` | `string`, `array`, `array`, mixed | `string` | Dispatch async job; returns request UUID; payload flows through events |
| `wait(txHash, timeoutSec=120, pollMs=800)` | `string`, `int`, `int` | `array\| null` | Poll receipt until mined/timeout |
| `estimateGas(data, from?)` | `string`, `?string` | `int` | Uses eth_estimateGas + padding |

### CallResult

Expand Down Expand Up @@ -62,10 +62,10 @@

## FeePolicy (via `EvmFees`)

| Method | Args | Returns | Notes |
|---------------------------|----------------|-----------------------------------------------------------|-----------------------------|
| `suggest()` | - | `['maxFeePerGas'=>string,'maxPriorityFeePerGas'=>string]` | Initial fee suggestion |
| `bump(previous, attempt)` | `array`, `int` | `array` | Adjust fees for replacement |
| Method | Args | Returns | Notes |
|----------------------------|--------------------|---------------------------|------------------------|
| `suggest(cb)` | `callable` | `[prioHex, maxHex]` | Initial fee suggestion |
| `replace(prioHex, maxHex)` | `string`, `string` | `[newPrioHex, newMaxHex]` | Replacement bump |

---

Expand Down Expand Up @@ -104,14 +104,14 @@ Start with `EvmLogs::query()` then chain:

## Events

| Event | When | Key Data |
|-----------------|--------------------|-----------------------------------|
| `TxQueued` | Job pushed | request_id, function, address |
| `TxBroadcasted` | First broadcast ok | tx_hash, nonce, fees |
| `TxReplaced` | Fee bump broadcast | old_tx_hash, new_tx_hash, attempt |
| `TxMined` | Receipt found | tx_hash, receipt |
| `TxFailed` | Terminal failure | reason, attempts |
| `CallPerformed` | Read executed | from, to, function, raw_result |
| Event | When | Key Data (excerpt) |
|-----------------|--------------------|----------------------------------------|
| `TxQueued` | Job pushed | to, data, payload |
| `TxBroadcasted` | First broadcast ok | txHash, fields, payload |
| `TxReplaced` | Fee bump broadcast | oldTxHash, newFields, attempt, payload |
| `TxMined` | Receipt found | txHash, receipt, payload |
| `TxFailed` | Terminal failure | to, data, reason, payload |
| `CallPerformed` | Read executed | from, address, function, rawResult |

---

Expand Down Expand Up @@ -157,4 +157,4 @@ Maintains nonce ordering; for scaling use a distributed nonce manager.
- Never log private keys.
- Limit queue concurrency.
- Use multiple RPC endpoints for resilience.

- Attach domain payloads to events for traceability.
5 changes: 3 additions & 2 deletions src/Clients/ContractClientGeneric.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function estimateGas(string $data, ?string $from = null): int
return (int) max(150000, ceil($n * $pad));
}

public function sendAsync(string $function, array $args = [], array $opts = []): string
public function sendAsync(string $function, array $args = [], array $opts = [], mixed $payload = null): string
{
$data = $this->abi->encodeFunction($this->abiJson, $function, $args);
$queue = (string) ($this->txCfg['queue'] ?? 'evm-send');
Expand All @@ -80,7 +80,8 @@ public function sendAsync(string $function, array $args = [], array $opts = []):
data: $data,
opts: array_merge($opts, ['request_id' => $requestId]),
chainId: $this->chainId,
txCfg: $this->txCfg
txCfg: $this->txCfg,
payload: $payload
))->onQueue($queue);

return $requestId;
Expand Down
8 changes: 6 additions & 2 deletions src/Contracts/ContractClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ public function at(string $address, array|string $abi = []): self;
/** Synchronous read only call returning raw hex or decoded value depending on ABI usage. */
public function call(string $function, array $args = []): mixed;

/** Enqueue a non blocking write job. Returns job id string. */
public function sendAsync(string $function, array $args = [], array $opts = []): string;
/**
* Enqueue a non blocking write job. Returns job id string.
* Optional $payload allows attaching any serializable context (e.g. an Eloquent model) that will
* be forwarded to all transaction lifecycle events (TxQueued, TxBroadcasted, TxReplaced, TxMined, TxFailed).
*/
public function sendAsync(string $function, array $args = [], array $opts = [], mixed $payload = null): string;

/** Wait for a receipt with timeout returns receipt array or null. */
public function wait(string $txHash, int $timeoutSec = 120, int $pollMs = 800): ?array;
Expand Down
2 changes: 1 addition & 1 deletion src/Events/TxBroadcasted.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

class TxBroadcasted
{
public function __construct(public string $txHash, public array $fields) {}
public function __construct(public string $txHash, public array $fields, public mixed $payload = null) {}
}
2 changes: 1 addition & 1 deletion src/Events/TxFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

class TxFailed
{
public function __construct(public string $to, public string $data, public string $reason) {}
public function __construct(public string $to, public string $data, public string $reason, public mixed $payload = null) {}
}
2 changes: 1 addition & 1 deletion src/Events/TxMined.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

class TxMined
{
public function __construct(public string $txHash, public array $receipt) {}
public function __construct(public string $txHash, public array $receipt, public mixed $payload = null) {}
}
2 changes: 1 addition & 1 deletion src/Events/TxQueued.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

class TxQueued
{
public function __construct(public string $to, public string $data) {}
public function __construct(public string $to, public string $data, public mixed $payload = null) {}
}
2 changes: 1 addition & 1 deletion src/Events/TxReplaced.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ class TxReplaced
* @param array $newFields Neue Felder inklusive angehobener Fees
* @param int $attempt Laufende Ersatz-Versuchsnummer (1-basiert)
*/
public function __construct(public string $oldTxHash, public array $newFields, public int $attempt) {}
public function __construct(public string $oldTxHash, public array $newFields, public int $attempt, public mixed $payload = null) {}
}
23 changes: 12 additions & 11 deletions src/Jobs/SendTransaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ class SendTransaction implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;

public function __construct(public string $address, public string $data, public array $opts, public int $chainId, public array $txCfg) {}
public function __construct(public string $address, public string $data, public array $opts, public int $chainId, public array $txCfg, public mixed $payload = null) {}

public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, FeePolicy $fees): void
{
event(new TxQueued($this->address, $this->data));
event(new TxQueued($this->address, $this->data, $this->payload));

$from = $signer->getAddress();

Expand Down Expand Up @@ -67,7 +67,7 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee

$pk = method_exists($signer, 'privateKey') ? $signer->privateKey() : null;
if (! $pk) {
event(new TxFailed($this->address, $this->data, 'Signer has no privateKey method'));
event(new TxFailed($this->address, $this->data, 'Signer has no privateKey method', $this->payload));

return;
}
Expand All @@ -78,10 +78,10 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
$rawHex = str_starts_with($raw, '0x') ? $raw : '0x'.$raw; // ensure 0x prefix
$txHash = $rpc->call('eth_sendRawTransaction', [$rawHex]);
$nonces->markUsed($from, $nonce);
event(new TxBroadcasted($txHash, $fields));
event(new TxBroadcasted($txHash, $fields, $this->payload));

} catch (\Throwable $e) {
event(new TxFailed($this->address, $this->data, 'rpc_send_error: '.$e->getMessage()));
event(new TxFailed($this->address, $this->data, 'rpc_send_error: '.$e->getMessage(), $this->payload));

return;
}
Expand All @@ -94,7 +94,7 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
while (time() < $deadline) {
$rec = $rpc->call('eth_getTransactionReceipt', [$txHash]);
if (! empty($rec)) {
event(new TxMined($txHash, $rec));
event(new TxMined($txHash, $rec, $this->payload));

return;
}
Expand All @@ -109,15 +109,15 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
$fields['maxFeePerGas'] = $max;

// Emit replacement attempt (before rebroadcast)
event(new TxReplaced($oldTxHash, $fields, $i + 1));
event(new TxReplaced($oldTxHash, $fields, $i + 1, $this->payload));

try {
$raw = new EIP1559Transaction($fields)->sign($pk);
$rawHex = str_starts_with($raw, '0x') ? $raw : '0x'.$raw; // ensure 0x prefix
$txHash = $rpc->call('eth_sendRawTransaction', [$rawHex]);
event(new TxBroadcasted($txHash, $fields));
event(new TxBroadcasted($txHash, $fields, $this->payload));
} catch (\Throwable $e) {
event(new TxFailed($this->address, $this->data, 'rpc_send_error_replacement_'.$i.': '.$e->getMessage()));
event(new TxFailed($this->address, $this->data, 'rpc_send_error_replacement_'.$i.': '.$e->getMessage(), $this->payload));

return;
}
Expand All @@ -126,7 +126,7 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
while (time() < $deadline) {
$rec = $rpc->call('eth_getTransactionReceipt', [$txHash]);
if (! empty($rec)) {
event(new TxMined($txHash, $rec));
event(new TxMined($txHash, $rec, $this->payload));

return;
}
Expand All @@ -137,7 +137,8 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
event(new TxFailed(
$this->address,
$this->data,
sprintf('no_receipt_after_%d_replacements (last maxFee=%d priority=%d)', $maxRep, $fields['maxFeePerGas'], $fields['maxPriorityFeePerGas'])
sprintf('no_receipt_after_%d_replacements (last maxFee=%d priority=%d)', $maxRep, $fields['maxFeePerGas'], $fields['maxPriorityFeePerGas']),
$this->payload
));
}
}