ox
Version:
Ethereum Standard Library
462 lines (436 loc) • 16.6 kB
text/typescript
import type * as AccessList from './AccessList.js'
import type * as Address from './Address.js'
import * as Authorization from './Authorization.js'
import * as Blobs from './Blobs.js'
import * as Errors from './Errors.js'
import * as Hex from './Hex.js'
import * as Quantity from './internal/quantity.js'
import type { Compute } from './internal/types.js'
import type * as Kzg from './Kzg.js'
import * as Transaction from './Transaction.js'
import * as TxEnvelope from './TxEnvelope.js'
import type * as TxEnvelopeEip4844 from './TxEnvelopeEip4844.js'
/** A Transaction Request that is generic to all transaction types, as defined in the [Execution API specification](https://github.com/ethereum/execution-apis/blob/4aca1d7a3e5aab24c8f6437131289ad386944eaa/src/schemas/transaction.yaml#L358-L423). */
export type TransactionRequest<
bigintType = bigint,
numberType = number,
type extends string = string,
> = Compute<{
/** EIP-2930 Access List. */
accessList?: AccessList.AccessList | undefined
/** EIP-7702 Authorization List. */
authorizationList?:
| Authorization.ListSigned<bigintType, numberType>
| undefined
/** Versioned hashes of blobs to be included in the transaction. */
blobVersionedHashes?: readonly Hex.Hex[] | undefined
/** Raw blob data. */
blobs?: readonly Hex.Hex[] | undefined
/** EIP-155 Chain ID. */
chainId?: numberType | undefined
/** Contract code or a hashed method call with encoded args */
data?: Hex.Hex | undefined
/** @alias `data` – added for TransactionEnvelope - Transaction compatibility. */
input?: Hex.Hex | undefined
/** Sender of the transaction. */
from?: Address.Address | undefined
/** Gas provided for transaction execution */
gas?: bigintType | undefined
/** Base fee per gas. */
gasPrice?: bigintType | undefined
/** Maximum total fee per gas sender is willing to pay for blob gas (in wei). */
maxFeePerBlobGas?: bigintType | undefined
/** Total fee per gas in wei (gasPrice/baseFeePerGas + maxPriorityFeePerGas). */
maxFeePerGas?: bigintType | undefined
/** Max priority fee per gas (in wei). */
maxPriorityFeePerGas?: bigintType | undefined
/** Unique number identifying this transaction */
nonce?: bigintType | undefined
/** Transaction recipient */
to?: Address.Address | null | undefined
/** Transaction type */
type?: type | undefined
/** Value in wei sent with this transaction */
value?: bigintType | undefined
/** ECDSA signature r. */
r?: Hex.Hex | undefined
/** ECDSA signature s. */
s?: Hex.Hex | undefined
/** ECDSA signature yParity. */
yParity?: numberType | undefined
/** @deprecated ECDSA signature v (for backwards compatibility). */
v?: numberType | undefined
}>
/** RPC representation of a {@link ox#TransactionRequest.TransactionRequest}. */
export type Rpc = TransactionRequest<Hex.Hex, Hex.Hex, string>
/**
* Converts a {@link ox#TransactionRequest.Rpc} to a {@link ox#TransactionRequest.TransactionRequest}.
*
* @example
* ```ts twoslash
* import { TransactionRequest } from 'ox'
*
* const request = TransactionRequest.fromRpc({
* to: '0x0000000000000000000000000000000000000000',
* value: '0x2386f26fc10000'
* })
* ```
*
* @param request - The RPC request to convert.
* @returns A transaction request.
*/
export function fromRpc(request: Rpc): TransactionRequest {
const request_ = { ...request } as TransactionRequest
if (typeof request.authorizationList !== 'undefined')
request_.authorizationList = Authorization.fromRpcList(
request.authorizationList,
)
if (typeof request.chainId !== 'undefined')
request_.chainId = Hex.toNumber(request.chainId)
if (typeof request.gas !== 'undefined')
request_.gas = Hex.toBigInt(request.gas)
if (typeof request.gasPrice !== 'undefined')
request_.gasPrice = Hex.toBigInt(request.gasPrice)
if (typeof request.maxFeePerBlobGas !== 'undefined')
request_.maxFeePerBlobGas = Hex.toBigInt(request.maxFeePerBlobGas)
if (typeof request.maxFeePerGas !== 'undefined')
request_.maxFeePerGas = Hex.toBigInt(request.maxFeePerGas)
if (typeof request.maxPriorityFeePerGas !== 'undefined')
request_.maxPriorityFeePerGas = Hex.toBigInt(request.maxPriorityFeePerGas)
if (typeof request.nonce !== 'undefined')
request_.nonce = Hex.toBigInt(request.nonce)
if (typeof request.type !== 'undefined')
request_.type =
Transaction.fromRpcType[
request.type as keyof typeof Transaction.fromRpcType
] || request.type
if (typeof request.value !== 'undefined')
request_.value = Hex.toBigInt(request.value)
if (typeof request.yParity !== 'undefined')
request_.yParity = Hex.toNumber(request.yParity)
if (typeof request.v !== 'undefined') request_.v = Hex.toNumber(request.v)
return request_
}
export declare namespace fromRpc {
export type ErrorType =
| Authorization.fromRpcList.ErrorType
| Hex.toNumber.ErrorType
| Hex.toBigInt.ErrorType
| Errors.GlobalErrorType
}
/**
* Converts a {@link ox#TransactionRequest.TransactionRequest} to a {@link ox#TransactionRequest.Rpc}.
*
* @example
* ```ts twoslash
* import { TransactionRequest, Value } from 'ox'
*
* const request = TransactionRequest.toRpc({
* to: '0x0000000000000000000000000000000000000000',
* value: Value.fromEther('0.01')
* })
* ```
*
* @example
* ### Using with a Provider
*
* You can use {@link ox#Provider.(from:function)} to instantiate an EIP-1193 Provider and
* send a transaction to the Wallet using the `eth_sendTransaction` method.
*
* ```ts twoslash
* import 'ox/window'
* import { Provider, TransactionRequest, Value } from 'ox'
*
* const provider = Provider.from(window.ethereum!)
*
* const request = TransactionRequest.toRpc({
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
* value: Value.fromEther('0.01')
* })
*
* const hash = await provider.request({
* // [!code focus]
* method: 'eth_sendTransaction', // [!code focus]
* params: [request] // [!code focus]
* }) // [!code focus]
* ```
*
* @param request - The request to convert.
* @returns An RPC request.
*/
export function toRpc(request: toRpc.Input): Rpc {
const request_rpc: Rpc = {}
if (typeof request.accessList !== 'undefined')
request_rpc.accessList = request.accessList
if (typeof request.authorizationList !== 'undefined')
request_rpc.authorizationList = Authorization.toRpcList(
request.authorizationList,
)
if (typeof request.blobVersionedHashes !== 'undefined')
request_rpc.blobVersionedHashes = request.blobVersionedHashes
if (typeof request.blobs !== 'undefined') request_rpc.blobs = request.blobs
if (typeof request.chainId !== 'undefined')
request_rpc.chainId = Quantity.fromNumberish(request.chainId)
if (typeof request.data !== 'undefined') {
request_rpc.data = request.data
request_rpc.input = request.data
} else if (typeof request.input !== 'undefined') {
request_rpc.data = request.input
request_rpc.input = request.input
}
if (typeof request.from !== 'undefined') request_rpc.from = request.from
if (typeof request.gas !== 'undefined')
request_rpc.gas = Quantity.fromNumberish(request.gas)
if (typeof request.gasPrice !== 'undefined')
request_rpc.gasPrice = Quantity.fromNumberish(request.gasPrice)
if (typeof request.maxFeePerBlobGas !== 'undefined')
request_rpc.maxFeePerBlobGas = Quantity.fromNumberish(
request.maxFeePerBlobGas,
)
if (typeof request.maxFeePerGas !== 'undefined')
request_rpc.maxFeePerGas = Quantity.fromNumberish(request.maxFeePerGas)
if (typeof request.maxPriorityFeePerGas !== 'undefined')
request_rpc.maxPriorityFeePerGas = Quantity.fromNumberish(
request.maxPriorityFeePerGas,
)
if (typeof request.nonce !== 'undefined')
request_rpc.nonce = Quantity.fromNumberish(request.nonce)
if (typeof request.to !== 'undefined') request_rpc.to = request.to
if (typeof request.type !== 'undefined')
request_rpc.type =
Transaction.toRpcType[
request.type as keyof typeof Transaction.toRpcType
] || request.type
if (typeof request.value !== 'undefined')
request_rpc.value = Quantity.fromNumberish(request.value)
if (typeof request.r !== 'undefined') request_rpc.r = request.r
if (typeof request.s !== 'undefined') request_rpc.s = request.s
if (typeof request.yParity !== 'undefined')
request_rpc.yParity = Quantity.fromNumberish(request.yParity)
if (typeof request.v !== 'undefined')
request_rpc.v = Quantity.fromNumberish(request.v)
return request_rpc
}
export declare namespace toRpc {
/** Numberish input accepted by {@link ox#TransactionRequest.(toRpc:function)}. */
export type Input = TransactionRequest<
Hex.Hex | bigint | number,
Hex.Hex | number,
string
>
export type ErrorType =
| Authorization.toRpcList.ErrorType
| Hex.fromNumber.ErrorType
| Errors.GlobalErrorType
}
/**
* Converts a {@link ox#TransactionRequest.TransactionRequest} to a {@link ox#(TransactionEnvelope:namespace).TxEnvelope}.
*
* Dispatches to the correct concrete envelope type via
* {@link ox#(TransactionEnvelope:namespace).(getType:function)} (using `request.type`
* when present, otherwise inferring from fee/blob/authorization fields), and
* drops fields that do not belong to the chosen type.
*
* For EIP-4844, if `blobs` is provided without `sidecars`, sidecars and
* `blobVersionedHashes` are derived via the `kzg` option. If `sidecars` is
* already provided, it is passed through unchanged.
*
* Inputs are expected to be in canonical form (`bigint` numerics and
* `'eip1559'`-style `type` strings). Pass RPC-shaped payloads through
* {@link ox#TransactionRequest.(fromRpc:function)} first.
*
* @example
* ```ts twoslash
* import { TransactionRequest } from 'ox'
*
* const envelope = TransactionRequest.toEnvelope({
* chainId: 1,
* maxFeePerGas: 1n,
* to: '0x0000000000000000000000000000000000000000',
* value: 1n
* })
* // @log: {
* // @log: chainId: 1,
* // @log: maxFeePerGas: 1n,
* // @log: to: '0x0000000000000000000000000000000000000000',
* // @log: type: 'eip1559',
* // @log: value: 1n,
* // @log: }
* ```
*
* @param request - The transaction request to convert.
* @param options - Options.
* @returns A transaction envelope.
*/
export function toEnvelope(
request: TransactionRequest,
options: toEnvelope.Options = {},
): TxEnvelope.TxEnvelope {
const type = TxEnvelope.getType(request as never) as TxEnvelope.Type | string
if (type === 'legacy')
return TxEnvelope.from({
type: 'legacy',
...pickBase(request),
...(typeof request.chainId !== 'undefined'
? { chainId: request.chainId }
: {}),
...(typeof request.gasPrice !== 'undefined'
? { gasPrice: request.gasPrice }
: {}),
}) as never
if (type === 'eip2930')
return TxEnvelope.from({
type: 'eip2930',
...pickBase(request),
chainId: request.chainId!,
...(typeof request.accessList !== 'undefined'
? { accessList: request.accessList }
: {}),
...(typeof request.gasPrice !== 'undefined'
? { gasPrice: request.gasPrice }
: {}),
}) as never
if (type === 'eip4844') {
const { kzg } = options
const blobs = request.blobs
const hasSidecars =
(request as { sidecars?: unknown }).sidecars !== undefined
// Derive sidecars + versioned hashes from raw blobs when possible.
const sidecars = (() => {
if (hasSidecars)
return (
request as unknown as {
sidecars: TxEnvelopeEip4844.Sidecars<Hex.Hex>
}
).sidecars
if (!blobs || blobs.length === 0) return undefined
if (!kzg) throw new MissingKzgError()
const commitments = Blobs.toCommitments(blobs, { kzg, as: 'Hex' })
const cellProofs = Blobs.toCellProofs(blobs, { kzg, as: 'Hex' })
return {
blobs,
commitments,
cellProofs,
} satisfies TxEnvelopeEip4844.Sidecars<Hex.Hex>
})()
const blobVersionedHashes = (() => {
if (request.blobVersionedHashes) return request.blobVersionedHashes
if (sidecars) {
if (!kzg) throw new MissingKzgError()
return Blobs.commitmentsToVersionedHashes(sidecars.commitments, {
as: 'Hex',
})
}
return []
})()
return TxEnvelope.from({
type: 'eip4844',
...pickBase(request),
chainId: request.chainId!,
blobVersionedHashes,
...(typeof request.accessList !== 'undefined'
? { accessList: request.accessList }
: {}),
...(typeof request.maxFeePerBlobGas !== 'undefined'
? { maxFeePerBlobGas: request.maxFeePerBlobGas }
: {}),
...(typeof request.maxFeePerGas !== 'undefined'
? { maxFeePerGas: request.maxFeePerGas }
: {}),
...(typeof request.maxPriorityFeePerGas !== 'undefined'
? { maxPriorityFeePerGas: request.maxPriorityFeePerGas }
: {}),
...(sidecars ? { sidecars } : {}),
}) as never
}
if (type === 'eip7702') {
if (!request.authorizationList) throw new MissingAuthorizationListError()
return TxEnvelope.from({
type: 'eip7702',
...pickBase(request),
chainId: request.chainId!,
authorizationList: request.authorizationList,
...(typeof request.accessList !== 'undefined'
? { accessList: request.accessList }
: {}),
...(typeof request.maxFeePerGas !== 'undefined'
? { maxFeePerGas: request.maxFeePerGas }
: {}),
...(typeof request.maxPriorityFeePerGas !== 'undefined'
? { maxPriorityFeePerGas: request.maxPriorityFeePerGas }
: {}),
}) as never
}
// EIP-1559 (default)
return TxEnvelope.from({
type: 'eip1559',
...pickBase(request),
chainId: request.chainId!,
...(typeof request.accessList !== 'undefined'
? { accessList: request.accessList }
: {}),
...(typeof request.maxFeePerGas !== 'undefined'
? { maxFeePerGas: request.maxFeePerGas }
: {}),
...(typeof request.maxPriorityFeePerGas !== 'undefined'
? { maxPriorityFeePerGas: request.maxPriorityFeePerGas }
: {}),
}) as never
}
export declare namespace toEnvelope {
type Options = {
/**
* KZG context used to derive EIP-4844 `sidecars` and `blobVersionedHashes`
* from raw `blobs`. Required when `blobs` is provided without `sidecars`
* or `blobVersionedHashes`.
*/
kzg?: Kzg.Kzg | undefined
}
type ErrorType =
| TxEnvelope.getType.ErrorType
| TxEnvelope.from.ErrorType
| Blobs.toCommitments.ErrorType
| Blobs.toCellProofs.ErrorType
| Blobs.commitmentsToVersionedHashes.ErrorType
| MissingKzgError
| MissingAuthorizationListError
| Errors.GlobalErrorType
}
// Picks the fields common to every `TxEnvelope.Base` shape off a
// `TransactionRequest`, dropping anything that is `undefined`.
function pickBase(request: TransactionRequest) {
const base: Record<string, unknown> = {}
if (typeof request.data !== 'undefined') base.data = request.data
if (typeof request.input !== 'undefined') base.input = request.input
if (typeof request.from !== 'undefined') base.from = request.from
if (typeof request.gas !== 'undefined') base.gas = request.gas
if (typeof request.nonce !== 'undefined') base.nonce = request.nonce
if (typeof request.to !== 'undefined') base.to = request.to
if (typeof request.value !== 'undefined') base.value = request.value
if (typeof request.r !== 'undefined') base.r = request.r
if (typeof request.s !== 'undefined') base.s = request.s
if (typeof request.yParity !== 'undefined') base.yParity = request.yParity
if (typeof request.v !== 'undefined') base.v = request.v
return base
}
/** Thrown when a 4844 conversion is requested but no `kzg` context is provided. */
export class MissingKzgError extends Errors.BaseError {
override readonly name = 'TransactionRequest.MissingKzgError'
constructor() {
super(
'A `kzg` option is required to derive 4844 sidecars or `blobVersionedHashes` from `blobs`.',
{
docsPath: '/api/TransactionRequest/toEnvelope',
},
)
}
}
/** Thrown when a 7702 conversion is requested but no `authorizationList` is provided. */
export class MissingAuthorizationListError extends Errors.BaseError {
override readonly name = 'TransactionRequest.MissingAuthorizationListError'
constructor() {
super(
'An `authorizationList` is required to convert a TransactionRequest into an EIP-7702 transaction envelope.',
)
}
}