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

Ethereum Offline Raw Tx Laravel Package

kornrunner/ethereum-offline-raw-tx

Build and sign Ethereum transactions offline in PHP. Set nonce, gas price, gas limit, recipient, value, and data, sign with a local private key, and output raw transaction hex ready to broadcast via any node. Keep private keys off RPC servers for wallets and backend payouts.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require kornrunner/ethereum-offline-raw-tx

This pulls in kornrunner/keccak (hashing) and kornrunner/secp256k1 (pure-PHP EC signing). There is no PDO/HTTP dependency — the package talks to nothing, it only produces signed bytes.

Where to look first

  1. README.md — the usage example is the entire API surface.
  2. src/Transaction.php — the package is essentially one class in the kornrunner\Ethereum namespace. Read the constructor and sign() / getRawTransaction() before anything else.
  3. composer.json — confirm the PHP constraint and the pinned versions of keccak and secp256k1; those two libraries are where 90% of your bugs will actually live.

First use case: a plain ETH transfer

use kornrunner\Ethereum\Transaction;

// All numeric fields are hex strings. No 0x prefix. Ever.
$tx = new Transaction(
    '09',                                       // nonce = 9
    '04a817c800',                               // gasPrice = 20 gwei
    '5208',                                     // gasLimit = 21000
    '3535353535353535353535353535353535353535', // to (40 hex chars, no 0x)
    '0de0b6b3a7640000',                         // value = 1 ETH in wei
    '',                                         // data
    1                                           // chainId (EIP-155)
);

$tx->sign(str_repeat('1', 64));                 // 64-hex private key, no 0x
echo $tx->getRawTransaction();                  // 0xf86c... ready for eth_sendRawTransaction

Feed the output straight into eth_sendRawTransaction (via web3p/web3.php, Guzzle, or curl). The returned hex string is the entire signing step — persistence, queueing, and transport are your problem.

Implementation Patterns

Fetch → sign → broadcast, kept separate

The package intentionally has no knowledge of the network. The canonical workflow is a three-stage pipeline you should keep as separate services/functions:

// 1. Gather (RPC)
$nonce     = $rpc->eth_getTransactionCount($from, 'pending');
$gasPrice  = $rpc->eth_gasPrice();
$gasLimit  = $rpc->eth_estimateGas([...]);
$chainId   = $rpc->eth_chainId();

// 2. Sign (this package — can run on an air-gapped box)
$raw = (new Transaction(
    dechex($nonce), dechex($gasPrice), dechex($gasLimit),
    $to, $valueHex, $data, hexdec($chainId)
))->sign($privateKeyHex)->getRawTransaction();

// 3. Broadcast (RPC)
$rpc->eth_sendRawTransaction($raw);

This split is the main value proposition: the signing box needs the private key, the RPC box does not.

Normalizing RPC responses into hex strings

Every RPC call returns 0x-prefixed quantities. Wrap the conversion once so you don't scatter dechex() across the codebase:

function hex0xToField(string $q): string {
    $q = ltrim($q, '0x');
    $q = ltrim($q, '0');           // strip leading zeros — see Gotchas
    return $q === '' ? '' : (strlen($q) % 2 ? '0' . $q : $q);
}

ERC-20 transfers

Contract calls are just to = token contract, value = 0, and data = selector + padded args:

$data = 'a9059cbb'
      . str_pad(ltrim($recipient, '0x'), 64, '0', STR_PAD_LEFT)
      . str_pad(dechex($amountWei), 64, '0', STR_PAD_LEFT);

You are responsible for ABI encoding — this package does no ABI work. Pair it with web3p/ethereum-abi or hand-roll the padding for simple calls.

Contract deployment

Set to to an empty string (''), put the deployment bytecode (hex, no 0x) in data, and budget a real gasLimit from eth_estimateGas (never hardcode 21000).

Nonce management for batches

Sign N transactions with nonce n, n+1, n+2... derived from a single eth_getTransactionCount(..., 'pending') snapshot. Because signing is deterministic (RFC 6979), re-signing the same inputs yields the identical raw tx — safe to retry the broadcast, not the signing with a different nonce.

Wrapping it

In a real codebase, put a thin value object around it:

final class RawTransaction {
    private function __construct(public readonly string $hex) {}
    public static function transfer(string $pk, string $to, string $weiWei, int $chainId, int $nonce, int $gasPrice, int $gasLimit): self { /* ... */ }
}

This gives you one place to validate hex, normalize zeros, and swap in web3p/ethereum-tx later if you need EIP-1559.

Testing

Keep a set of known vectors (ethers.js / web3.py output for identical inputs) and assert byte equality in unit tests. Then do one real eth_sendRawTransaction on Sepolia before shipping. The signing path is deterministic, so a single golden test catches almost every regression.

Gotchas and Tips

Every field is a hex string without 0x — including "numbers"

nonce, gasPrice, gasLimit, value, and chainId are all raw hex strings. Passing 9 instead of '09', or '0x5208' instead of '5208', produces a transaction that RLP-encodes but is semantically wrong (or throws). Strip 0x at the boundary and never look back.

Leading zeros and RLP integer encoding

RLP requires integers to be minimally encoded. A value of '00' or '000000' can produce bytes that differ from a reference implementation. Normalize numeric fields: left-trim leading zeros, and decide explicitly what you pass for zero ('' vs '00'). Test this against a known vector — this is the single most common source of "my tx hash doesn't match ethers" bugs.

Private keys are raw 32-byte hex, no 0x

sign() wants 64 hex characters. Not a WIF, not a mnemonic, not a keystore JSON. If you're loading from a keystore, decrypt it yourself first and pass the raw hex. Store keys in env/secret manager and never log the argument to sign().

to is 40 hex chars; deployment is ''

No EIP-55 checksum validation is done — checksummed, lowercase, and uppercase addresses all encode to the same bytes, so a bad-but-well-formed address will happily sign. Validate preg_match('/^[0-9a-fA-F]{40}$/', $to) yourself and compare against an expected whitelist if you're moving funds.

Chain ID is not optional in practice

Omitting chainId (or passing null) yields a pre-EIP-155 signature with v = 27/28, which modern nodes reject for replay protection. Always pass the chain ID from eth_chainId and double-check you're not mixing mainnet values into a testnet run — that's a silent funds-loss path.

No RPC, no ABI, no gas estimation, no nonce tracking

The package does one thing. You must supply nonce, gasPrice, gasLimit, chainId, and any data (ABI-encoded). If you want those handled for you, use web3p/ethereum-tx or web3p/web3.php's contract layer.

EIP-1559 support depends on your version

Check the installed version's source/tests rather than the README before assuming type-2 support. If you need maxFeePerGas/maxPriorityFeePerGas and it isn't there, either upgrade, submit legacy type-0 transactions (still accepted), or move to web3p/ethereum-tx. Pin the version in composer.json — with 0 dependents and a small maintainer surface, an unexpected minor bump is a real risk.

Pure-PHP EC signing is slow

Expect single-digit to low-tens of milliseconds per signature, and noticeably more on cold PHP-FPM workers. It's fine for wallet-style flows and CI; benchmark before putting it in a hot loop. Cache nothing that contains a private key.

Deterministic signatures are a feature, not a bug

RFC 6979 means the same inputs always yield the same raw tx. That makes retries idempotent and testing trivial — but it also means signing with a reused nonce and different payloads is instantly catastrophic. Guard nonce allocation with a lock.

Debugging raw transaction mismatches

When eth_sendRawTransaction returns rlp: ... or invalid sender, diff your hex against ethers.js or web3.py for the exact same inputs, field by field. Nine times out of ten the culprit is an odd-length hex field, a stray 0x, or a leftover leading zero in value/gasPrice.

Verify the tx hash independently

The tx hash is keccak256(rawTxBytes). If getHash() isn't available in your version, compute it with kornrunner/keccak over hex2bin(ltrim($raw, '0x')) and log it — it's your only handle for polling after broadcast.

already known is success

When you re-broadcast a previously submitted raw tx, most nodes return an "already known" / "nonce too low" error. Treat those as idempotent success, not failure.

Extension points are minimal by design

Transaction is not heavily abstracted. For custom behavior (EIP-2930 access lists, fee oracles, metadata) wrap it rather than subclass it — you're better off with a value object you own than with fragile inheritance over a single-file class.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle