Skip to content

Commit ea4a24c

Browse files
committed
feat: add generate new address command
1 parent faa32d9 commit ea4a24c

File tree

3 files changed

+106
-9
lines changed

3 files changed

+106
-9
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,36 @@ $res = $contract->call('isAnchored', [1, '0x'.$hashHex]);
4444
$jobId = $contract->sendAsync('anchor', [1, '0x'.$hashHex, 'meta']);
4545
```
4646

47+
### Generate new addresses
48+
49+
The package ships with a helper to generate fresh Ethereum keypairs (using `kornrunner/ethereum-address`).
50+
51+
Generate one address (JSON output):
52+
53+
```bash
54+
php artisan evm:address:generate --json
55+
```
56+
57+
Generate 3 addresses (table output):
58+
59+
```bash
60+
php artisan evm:address:generate --count=3
61+
```
62+
63+
Sample JSON response:
64+
65+
```json
66+
[
67+
{
68+
"address": "0xAbcDEF1234...",
69+
"private_key": "0x6f8d...64hex",
70+
"public_key": "0x04b3...uncompressed"
71+
}
72+
]
73+
```
74+
75+
Security note: Private keys are shown once. Persist them securely (e.g. Vault, KMS). Never commit them.
76+
4777
## Testing
4878

4979
```bash

src/Jobs/SendTransaction.php

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,16 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
7171
return;
7272
}
7373

74-
// First broadcast
75-
$raw = new EIP1559Transaction($fields)->sign($pk);
76-
$txHash = $rpc->call('eth_sendRawTransaction', [$raw]);
77-
$nonces->markUsed($from, $nonce);
78-
event(new TxBroadcasted($txHash, $fields));
74+
// First broadcast with error handling
75+
try {
76+
$raw = new EIP1559Transaction($fields)->sign($pk);
77+
$txHash = $rpc->call('eth_sendRawTransaction', [$raw]);
78+
$nonces->markUsed($from, $nonce);
79+
event(new TxBroadcasted($txHash, $fields));
80+
} catch (\Throwable $e) {
81+
event(new TxFailed($this->address, $this->data, 'rpc_send_error: '.$e->getMessage()));
82+
return;
83+
}
7984

8085
$timeout = (int)($this->opts['timeout'] ?? $this->txCfg['confirm_timeout']);
8186
$pollMs = (int)($this->opts['poll_ms'] ?? $this->txCfg['poll_interval_ms']);
@@ -101,9 +106,14 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
101106
// Emit replacement attempt (before rebroadcast)
102107
event(new TxReplaced($oldTxHash, $fields, $i + 1));
103108

104-
$raw = new EIP1559Transaction($fields)->sign($pk);
105-
$txHash = $rpc->call('eth_sendRawTransaction', [$raw]);
106-
event(new TxBroadcasted($txHash, $fields));
109+
try {
110+
$raw = new EIP1559Transaction($fields)->sign($pk);
111+
$txHash = $rpc->call('eth_sendRawTransaction', [$raw]);
112+
event(new TxBroadcasted($txHash, $fields));
113+
} catch (\Throwable $e) {
114+
event(new TxFailed($this->address, $this->data, 'rpc_send_error_replacement_'.$i.': '.$e->getMessage()));
115+
return;
116+
}
107117

108118
$deadline = time() + $timeout;
109119
while (time() < $deadline) {
@@ -116,6 +126,10 @@ public function handle(RpcClient $rpc, Signer $signer, NonceManager $nonces, Fee
116126
}
117127
}
118128

119-
event(new TxFailed($this->address, $this->data, 'No receipt after attempts'));
129+
event(new TxFailed(
130+
$this->address,
131+
$this->data,
132+
sprintf('no_receipt_after_%d_replacements (last maxFee=%d priority=%d)', $maxRep, $fields['maxFeePerGas'], $fields['maxPriorityFeePerGas'])
133+
));
120134
}
121135
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Event;
4+
use Farbcode\LaravelEvm\Jobs\SendTransaction;
5+
use Farbcode\LaravelEvm\Events\TxFailed;
6+
use Farbcode\LaravelEvm\Contracts\{RpcClient, Signer, NonceManager, FeePolicy};
7+
8+
class FFailSigner implements Signer {
9+
public function __construct(private string $pk) {}
10+
public function getAddress(): string { return '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; }
11+
public function privateKey(): string { return $this->pk; }
12+
}
13+
class FFailNonce implements NonceManager {
14+
public function getPendingNonce(string $address, callable $fetcher): int { return 1; }
15+
public function markUsed(string $address, int $nonce): void {}
16+
}
17+
class FFailFees implements FeePolicy {
18+
public function suggest(callable $gasPriceFetcher): array { return [1_000_000_000, 50_000_000_000]; }
19+
public function replace(int $oldPriority, int $oldMax): array { return [$oldPriority + 1_000_000_000, $oldMax + 10_000_000_000]; }
20+
}
21+
class FFailRpc implements RpcClient {
22+
public function call(string $method, array $params = []): mixed {
23+
if ($method === 'eth_estimateGas') { return '0x5208'; }
24+
if ($method === 'eth_getTransactionCount') { return '0x1'; }
25+
if ($method === 'eth_gasPrice') { return '0x3b9aca00'; }
26+
if ($method === 'eth_getTransactionReceipt') { return []; }
27+
if ($method === 'eth_sendRawTransaction') { throw new RuntimeException('simulated failure'); }
28+
return [];
29+
}
30+
public function callRaw(string $method, array $params = []): array { return []; }
31+
public function health(): array { return ['chainId' => 137, 'block' => 123]; }
32+
}
33+
34+
it('emits TxFailed on initial broadcast error', function () {
35+
Event::fake();
36+
$job = new SendTransaction(
37+
address: '0xContract',
38+
data: '0xabcdef',
39+
opts: ['timeout' => 1],
40+
chainId: 137,
41+
txCfg: [
42+
'estimate_padding' => 1.2,
43+
'confirm_timeout' => 0,
44+
'max_replacements' => 0,
45+
'poll_interval_ms' => 50,
46+
'queue' => 'evm-send'
47+
]
48+
);
49+
$job->handle(new FFailRpc(), new FFailSigner('0x'.str_repeat('aa',32)), new FFailNonce(), new FFailFees());
50+
Event::assertDispatched(TxFailed::class, function ($e) {
51+
return str_contains($e->reason, 'rpc_send_error');
52+
});
53+
});

0 commit comments

Comments
 (0)