npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oyaprotocol/ethereum

v0.2.0

Published

Oya Ethereum JSON-RPC utilities.

Readme

@oyaprotocol/ethereum

Ethereum JSON-RPC utilities for Oya kernel code. This package is a hardened kernel surface: callers provide explicit transport settings and explicit fetch implementations instead of relying on process-level defaults.

Public Entrypoint

  • @oyaprotocol/ethereum

Current Surface

  • createHttpConfig(options): validate explicit HTTP transport settings, re-exported from @oyaprotocol/utils.
  • requestEthereumJsonRpc(options): send one JSON-RPC POST request with explicit config and injected fetch, returning the raw result, attempt count, id, and parsed response payload.
  • parseTransactionQuantity(value, name): parse an Ethereum hexadecimal quantity into a bigint, rejecting noncanonical syntax and values larger than 256 bits. Available since 0.1.2.
  • createTransactionPreparer(options): configure a reusable EIP-1559 preparer that fetches transaction fields and invokes a host signer, without broadcasting.
  • ethSendRawTransaction(options): submit a signed raw transaction and return the transaction hash with attempt metadata. Callers may pass transactionHash when they already know the hash, allowing the wrapper to verify duplicate-style retry errors with eth_getTransactionByHash.
  • ethGetTransactionReceipt(options): look up a transaction receipt, returning { receipt, attemptCount, response }. The receipt is null when unavailable, including pending or unknown transactions.
  • ethWaitForTransactionReceipt(options): poll for a receipt with an explicit overall deadline, poll interval, and optional cancellation signal. Returns { receipt, pollCount, attemptCount, response } with a non-null receipt.
  • encodeLedgerCall(cid): encode calldata for the Oya Ledger's log(string) function.
  • hashLedgerCid(cid): validate a canonical CID and compute its Keccak-256 lookup topic.
  • decodeLedgerEvent(log, ledgerContract): decode a Log(address indexed node, bytes32 indexed cidKeccak256Hash, string cid) event from the expected Ledger, returning { node, cidKeccak256Hash, cid, removed? } or null for an unrelated log.
  • logCid(cid, options): prepare a Ledger transaction through a host callback, submit it, await its receipt, and verify the expected event.
  • LogCidError: reports the failed logging stage, CID, known transaction hash, receipt when available, and original cause.
  • EthereumJsonRpcError: thrown when an HTTP-successful JSON-RPC response contains an error payload.
  • HttpStatusError: thrown when the HTTP response itself is not successful, re-exported from @oyaprotocol/utils.
  • EthereumRawTransactionRecoveryError: thrown when raw transaction submission may have succeeded before a retry returned a duplicate-style error, but the wrapper could not verify the supplied transaction hash.
  • EthereumTransactionReceiptTimeoutError: thrown when the overall receipt wait expires, exposing transactionHash, timeoutMs, and pollCount.

Behavior

createHttpConfig(...) accepts the shared CreateHttpConfigOptions shape from @oyaprotocol/utils. The url value is normalized by trimming trailing slashes before JSON-RPC requests are sent.

requestEthereumJsonRpc(...) owns the JSON-RPC envelope and request headers. It sends content-type: application/json, rejects caller-provided content-type config headers, enforces a request timeout, retries transient HTTP/network failures only for read-only Ethereum methods, and treats JSON-RPC error payloads as non-retryable semantic errors.

parseTransactionQuantity(value, name) validates raw RPC quantities such as chain IDs and balances. It accepts "0x0" or "0x" followed by up to 64 hexadecimal digits with no leading zero; hexadecimal digits may use either case. Decimal strings, numbers, whitespace, negatives, empty hex, leading zeros, and oversized values throw. name is a caller-supplied field label for errors; input values are not included in those errors. The parser makes no requests and applies no application policy: callers still compare the parsed chain ID or require a positive balance.

ethSendRawTransaction(...) does not sign transactions and does not compute transaction hashes. It expects callers to provide a signed raw transaction. If transactionHash is supplied and a retry of eth_sendRawTransaction returns duplicate-style JSON-RPC errors such as already known or nonce too low, the wrapper checks eth_getTransactionByHash(transactionHash) before returning a recovered result. Without transactionHash, those cases are surfaced as EthereumRawTransactionRecoveryError because this wrapper requires the host's transaction hash to verify acceptance.

Hosts own transaction signing, environment configuration, and RPC endpoint discovery. ABI support is limited to the Ledger helpers described below. Callers are responsible for preparing JSON-RPC params, including converting bigint values to Ethereum quantity hex before calling the raw request primitive.

Shared Transaction Types

src/transactions.ts defines these types, exported from @oyaprotocol/ethereum:

  • TransactionRequest: readonly to, data, value: bigint (wei), and optional signal. It describes call intent; the host supplies the remaining transaction fields.
  • SignedTransaction: readonly signed rawTransaction and its transactionHash.
  • UnsignedTransaction: readonly to, data, value, type: 2, chainId: number (positive safe integer), nonce: number, gasLimit: bigint, maxFeePerGas: bigint, and maxPriorityFeePerGas: bigint. The access list is empty. There is no embedded cancellation signal.
  • TransactionSigner: a readonly address and signTransaction(transaction, signal?) method returning SignedTransaction, synchronously or asynchronously. The host implements signing and must preserve all supplied fields, use the advertised account, and return without broadcasting.
  • TransactionPreparer: a callback from TransactionRequest to SignedTransaction, synchronously or asynchronously, without broadcasting.
  • TransactionStage: 'prepare' | 'submit' | 'receipt' | 'verify'. Verification means the operation's checks after receiving a receipt, such as execution status and expected events.

Ledger uses these shared types and always requests value: 0n. Other callers can use the same host preparation callback with a nonzero value. These names replace LedgerTransactionRequest, PreparedLedgerTransaction, PrepareLedgerTransaction, and LogCidStage; update type imports accordingly.

Default Transaction Preparation

createTransactionPreparer(options) in src/transaction-preparer.ts returns a TransactionPreparer compatible with logCid and the messages package's publishAndLogSignedMessage. The host supplies a TransactionSigner; no local wallet adapter, private-key handling, ethers, viem, or new dependency is included.

import { createTransactionPreparer, logCid } from '@oyaprotocol/ethereum';
import type { TransactionSigner } from '@oyaprotocol/ethereum';

declare const signer: TransactionSigner; // Implemented by the host's wallet/signing service.
const transactionPreparer = createTransactionPreparer({
    config: rpcConfig,
    fetch: rpcFetch,
    chainId: 1, // The operator's expected network; a positive safe integer.
    signer,
    gasLimitMarginPercent: 20, // Default: 20% above the estimate, rounded up.
    baseFeeMultiplier: 2, // Default: 2 * base fee + suggested priority fee.
    limits: { gasLimit: 200_000n, feePerGas: 30_000_000_000n }, // Illustrative operator-chosen caps.
    timeoutMs: 30_000, // Default: overall preparation deadline, including signing.
    id: 'prepare-42', // Optional; preparation RPCs default to ID 1.
});

const logging = await logCid(cid, {
    config: rpcConfig, fetch: rpcFetch, ledgerContract,
    nodeAddress: signer.address, // Direct account call in this example.
    transactionPreparer, timeoutMs: 60_000, pollIntervalMs: 1_000, signal,
});

Construction validates and snapshots configuration without making RPC calls. chainId must be a number satisfying Number.isSafeInteger(chainId) && chainId > 0; bigint, string, fractional, nonpositive, and unsafe values are rejected. Update previous configurations such as chainId: 1n to chainId: 1. The signer receives the validated number and can use BigInt(transaction.chainId) if its wallet API requires bigint.

Each invocation validates and snapshots the call, parses eth_chainId losslessly as bigint, and requires an exact match with BigInt(chainId). Unsupported or mismatched RPC chain IDs reject before further RPC calls or signing; they are never rounded to a number. The preparer then reads eth_getTransactionCount(address, "pending"), obtains the pending block's base fee and gas limit, and reads eth_maxPriorityFeePerGas. It calls eth_estimateGas against pending state with the signing address, recipient, calldata, value, chain ID, nonce, and selected fees. Fee selection and gas estimation both use pending state, although that state can advance between separate RPC calls. These methods follow the Ethereum execution API; the fee fields follow EIP-1559.

The gas limit is ceil(estimate * (100 + gasLimitMarginPercent) / 100). The maximum fee per gas is baseFee * baseFeeMultiplier + suggestedPriorityFee; the priority fee is the RPC suggestion. The multiplier and margin are policy choices, not protocol requirements. Arithmetic uses bigint, and fee values are in wei per gas. Nonces above Number.MAX_SAFE_INTEGER reject before conversion for the signer. Both optional limits fields are ceilings: the factory rejects a selected value above a ceiling instead of reducing the gas buffer or fee suggestion. Omitting them adds no operator ceiling; the buffered gas limit must still fit the pending block's gas limit. A configured gas cap must be positive; a fee cap of zero is permitted. Networks without EIP-1559 base-fee data, unsupported RPC methods, malformed quantities, estimation errors, and out-of-range values reject before signing.

The signer receives a frozen transaction and a separate signal combining caller cancellation with the overall deadline. Each RPC read uses the existing transport timeout/retry policy. The whole preparation has no retries, and the signer is called at most once per invocation. Signer errors propagate; timeout/cancellation stops waiting even when the signer ignores its signal, and late results are discarded. The host should honor the signal where its signing API permits. Aborting cannot undo an external signing request already started.

The returned result is a frozen snapshot. The factory checks byte formatting, the type-2 prefix, and that the supplied hash equals Keccak-256 of the returned bytes, using the existing @noble/hashes dependency. It does not decode the transaction or recover the signing account: the host signer remains responsible for a valid signature, the advertised account, and exact transaction fields. Only ordinary type-2 calls with an empty access list are supported; contract creation, legacy transactions, blobs, and smart-wallet execution wrapping require a custom preparer. A wallet API that only signs and broadcasts together cannot implement this signing contract.

The host coordinates nonces across the complete prepare/submit/receipt lifecycle for each account. Serialize that lifecycle for the initial implementation, including other users of the account, and reconcile uncertain submissions before proceeding. The factory does not reserve, cache, or increment nonces and does not coordinate different processes. It refreshes chain values on every call; preparation alone cannot guarantee unique nonces before submission. Existing submission retries resend retained signed bytes rather than calling the preparer again. The factory's id applies to its read RPCs; Ledger's optional id separately controls submission and receipt requests.

Transaction Receipts

The host passes the hash returned by ethSendRawTransaction(...) to either receipt function, with the same explicit RPC config and injected fetch:

import {
    ethWaitForTransactionReceipt,
    EthereumTransactionReceiptTimeoutError,
} from '@oyaprotocol/ethereum';
import type { EthGetTransactionReceiptOptions } from '@oyaprotocol/ethereum';

async function observeTransaction(options: EthGetTransactionReceiptOptions) {
    try {
        const { receipt } = await ethWaitForTransactionReceipt({
            ...options, // config, fetch, transactionHash, optional signal/id
            timeoutMs: 60_000,
            pollIntervalMs: 1_000,
        });
        if (receipt.status !== 'success') {
            throw new Error(`Transaction execution status: ${receipt.status ?? 'unknown'}`);
        }
        return receipt;
    } catch (error) {
        if (error instanceof EthereumTransactionReceiptTimeoutError) {
            // The host can retain error.transactionHash and resume observation later.
        }
        throw error;
    }
}

Both functions return an EthereumTransactionReceipt whose quantities (block number, gas, fees, transaction/log indexes, and transaction type) are bigint. Hashes, addresses, topics, and data preserve RPC hex casing. Hash comparison is case-insensitive. Receipts and their EthereumReceiptLog entries are validated, including log block/transaction identity, byte lengths, and canonical quantity encoding. to and contractAddress are nullable; type, effectiveGasPrice, blob fees/gas, log removed, and log blockTimestamp are optional and validated when present. The original JSON-RPC envelope remains available as response: unknown; provider-specific fields are not copied into normalized receipts. Normalized quantities need explicit conversion before JSON serialization.

The normalized status is 'success' or 'reverted'. Historical receipts containing a state root instead of an execution status return status: null and preserve the root; the wrapper does not infer whether those transactions succeeded. These status distinctions follow EIP-658 and the Ethereum receipt schema.

Waiting performs the first lookup immediately and pauses for pollIntervalMs after each null result. Each lookup uses the existing bounded RPC retry policy for transient failures. pollCount counts logical lookups, while attemptCount totals their HTTP attempts; response is the final lookup's envelope. A reverted receipt ends the wait normally, letting the host choose its failure policy. Malformed receipts, JSON-RPC errors, and exhausted transport retries reject the wait immediately.

The wait's timeoutMs covers all lookups, request retries, and intervening delays. config.timeoutMs still limits each individual HTTP attempt. Both wait durations must be positive integers no greater than 2,147,483,647 ms. Caller cancellation interrupts requests and delays; its error preserves signal.reason as cause and takes precedence if the caller and deadline have both aborted. Timers and signal listeners are cleaned up on completion or failure, including when an injected transport ignores its signal.

The wait returns the first mined receipt reported by the RPC endpoint. It does not track confirmation depth, chain reorganizations, or replacement transactions, and a timeout does not establish that a transaction failed. Hosts can use the Ledger helper below to inspect receipt events.

Ledger ABI Helpers

The pure, synchronous helpers in src/ledger.ts target Ledger.sol. They use its fixed function selector and event signature, checked with Foundry, and implement its single-string ABI layout directly. Keccak-256 uses @noble/hashes 2.2.0, also used by the messages package. The encoding follows the Solidity ABI specification.

encodeLedgerCall(cid) returns the complete 0x-prefixed calldata. Both this helper and hashLedgerCid(cid) require CIDv1 in lowercase unpadded Base32 with a 32-byte SHA-256 digest, using assertCanonicalCid from @oyaprotocol/utils. They preserve the CID exactly and reject alternate representations, whitespace, URIs, paths, and malformed identifiers instead of normalizing them. This is the same format enforced by @oyaprotocol/ipfs. The host supplies the Ledger address and prepares the remaining transaction fields, signs, and submits the transaction.

decodeLedgerEvent(log, ledgerContract) accepts LedgerEventInput, which selects address, topics, data, and optional removed from EthereumReceiptLog. Pass receipt logs directly. The expected address is required and must be 20-byte hex. Other emitter addresses or event signatures, including logs with no topics, return null. Malformed input or a matching event with invalid topics, address padding, data offset, length, padding, or UTF-8 throws. Decoding checks lengths before allocating from an untrusted declared length, and requires the canonical layout emitted by Solidity, without extra trailing data.

The decoded LedgerEvent preserves the indexed node and cidKeccak256Hash hex casing and exact canonical CID text. The decoder requires exactly three topics, validates the same strict CID format, and verifies that the supplied 32-byte hash equals keccak256(bytes(cid)). A matching event containing a noncanonical CID or a mismatched hash throws. The Solidity contract still accepts arbitrary strings, so historical or external events outside the kernel's CID policy require a separate raw ABI decoder. Address and event-signature matching is case-insensitive. Optional removed metadata is preserved, including true; decoding alone does not establish successful execution or finality. The expected node is the immediate caller of Ledger, which may be a contract wallet and may differ from the signed message's signer.

To find events for a known CID, the host uses hashLedgerCid(cid) and supplies an eth_getLogs filter with the Ledger address, the desired block range, and topics: ['0xce2d845fcf02211a951a2153c1ddf64ec48ef6d54644ea188101f10018b871dc', null, hashLedgerCid(cid)]. The null permits any node address. This hash is separate from the SHA-256 digest embedded in the CID. Enforcing one CID spelling gives all nodes the same lookup hash for that CID. The previous two-topic Log(address,string) event is unrelated to this signature and decodes to null.

For example, a host can prepare the call and later verify its receipt:

import { encodeLedgerCall, decodeLedgerEvent } from '@oyaprotocol/ethereum';

const transaction = {
    to: ledgerContract,
    data: encodeLedgerCall(publication.cid),
    value: 0n,
};
// The host prepares, signs, and submits transaction, then obtains its receipt.

if (receipt.status !== 'success') {
    throw new Error('Ledger transaction did not succeed.');
}
const event = receipt.logs
    .map((log) => decodeLedgerEvent(log, ledgerContract))
    .find((entry) => entry !== null && entry.removed !== true &&
        entry.node.toLowerCase() === nodeAddress.toLowerCase() &&
        entry.cid === publication.cid);
if (!event) {
    throw new Error('Receipt did not contain the expected Ledger event.');
}

These helpers require standard TextEncoder and TextDecoder, consistent with the package's ECMAScript 2025 target. Hosts choose confirmation policy and compose publication with transaction submission; the helpers perform no network calls.

Logging a CID

logCid(cid, options) in src/ledger.ts composes the existing ABI, raw submission, and receipt helpers. It validates the canonical CID, Ledger address, expected node, HTTP config, and polling durations before asking the host to prepare a transaction:

import { logCid } from '@oyaprotocol/ethereum';

const logging = await logCid(publication.cid, {
    config: rpcConfig,
    fetch: rpcFetch,
    ledgerContract,
    nodeAddress,
    transactionPreparer,
    timeoutMs: 60_000,
    pollIntervalMs: 1_000,
    id: 'message-42', // Optional JSON-RPC request ID.
    signal,
});
// logging: { cid, transactionHash, receipt, event }

transactionPreparer is a host-supplied TransactionPreparer function, which can be created with createTransactionPreparer above. It receives a frozen { to, data, value: 0n, signal? } request describing the Ledger call and returns { rawTransaction, transactionHash }, synchronously or asynchronously. It must prepare and sign without broadcasting. The host selects the chain and preparation policy, supplies wallet access, and coordinates nonces across concurrent messages. It must return the correct hash for the signed transaction and preserve the requested call, including when routing through a contract wallet. The Ledger helper validates the returned hex shapes and checks the RPC's returned hash; it does not parse or independently verify the signed transaction.

The optional id accepts a nonempty string or a safe integer, including zero. It uses the existing RPC validation and defaults to 1 when omitted. The same ID is forwarded to submission, retries and recovery lookups, and every receipt poll. Invalid IDs reject before transaction preparation. This identifier is RPC metadata and is separate from the signed transaction's hash.

The helper snapshots the signed bytes and hash, submits through ethSendRawTransaction, then polls through ethWaitForTransactionReceipt. It prepares only once and adds no outer retry loop; submission retries reuse the exact signed bytes. nodeAddress is Ledger's immediate caller, not necessarily the message signer or the outer transaction sender. A successful result requires receipt status success and a matching event from ledgerContract, with the expected node and exact CID, valid CID/hash correspondence, and removed !== true.

timeoutMs bounds receipt observation after submission; config.timeoutMs bounds each RPC request. The host bounds transaction preparation/signing. The optional signal covers preparation, submission, and polling through the existing async utilities. Cancellation stops subsequent stages even if preparation ignores its signal and eventually returns signed bytes. It cannot undo a submitted transaction. The returned receipt establishes mined execution as reported by the RPC, with confirmation depth and reorganization policy left to the host.

Invalid configuration rejects before preparation. Once preparation begins, failures throw LogCidError with the original cause, cid, transactionHash (or null before a valid hash is known), receipt (or null), and one of these stages:

| Stage | Operation that failed | | --- | --- | | prepare | Host preparation/signing, returned-value validation, or cancellation before submission | | submit | Signed transaction submission or its response/recovery checks | | receipt | Receipt lookup, parsing, cancellation, or deadline | | verify | Receipt execution status or expected Ledger event checks |

The hash is retained before broadcasting, including when submission fails ambiguously. Its presence does not prove that the transaction was accepted. A receipt timeout is available as error.cause instanceof EthereumTransactionReceiptTimeoutError; it does not imply transaction failure. Resume observation using the retained hash and ethWaitForTransactionReceipt, then apply the status/event checks shown above. Calling logCid again prepares a new transaction and can create another event. Hosts must persist progress themselves if recovery must survive a process crash.

Validation

Run from the repository root; tests use injected transports and require no live RPC:

npm --prefix packages run build
node --test packages/ethereum/test/*.test.js
packages/node_modules/.bin/tsc -p packages/ethereum/tsconfig.type-test.json

test/fixtures/ledger-abi.json contains nine independent Foundry-generated ABI vectors for the canonical CIDs in packages/test/fixtures/cids.json. Exact CID bytes were encoded with cast abi-encode 'f(bytes)' <hexBytes>; ABI bytes and string share the same layout. Calldata prefixes the selector obtained from forge inspect --root contracts --offline Ledger methodIdentifiers --json. The shared signature and node topics were generated with cast keccak 'Log(address,bytes32,string)' and cast abi-encode 'f(address)' <node>. Each case adds its own cidKeccak256Hash, generated with cast keccak <hexBytes> over the exact CID bytes. Runtime tests use the committed fixtures and require no Foundry installation.