diff --git a/CHANGELOG.md b/CHANGELOG.md
index d793b37b407..7db914cfc86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1783,6 +1783,24 @@ If there are any bugs, improvements, optimizations or any new feature proposal f
- `RpcErrorMessages` that contains mapping for standard RPC Errors and their messages. (#6230)
+#### web3-eth
+
+- A `rpc_method_wrapper` (`signTypedData`) for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286)
+- A `signTypedData` method to the `Web3Eth` class (#6286)
+
+#### web3-eth-abi
+
+- A `getEncodedEip712Data` method that takes an EIP-712 typed data object and returns the encoded data with the option to also keccak256 hash it (#6286)
+
+#### web3-rpc-methods
+
+- A `signTypedData` method to `eth_rpc_methods` for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286)
+
+#### web3-types
+
+- `eth_signTypedData` and `eth_signTypedData_v4` to `web3_eth_execution_api` (#6286)
+- `Eip712TypeDetails` and `Eip712TypedData` to `eth_types` (#6286)
+
#### web3-validator
- Added `json-schema` as a main json schema type (#6264)
@@ -1815,4 +1833,9 @@ If there are any bugs, improvements, optimizations or any new feature proposal f
#### web3-validator
+- Rpc method `getPastLogs` accept blockHash as a parameter https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs (#6181)
+
+#### web3-eth
+
+- Missing `blockHeaderSchema` properties causing some properties to not appear in response of `newHeads` subscription (#6243)
- Type `RawValidationError` was removed (#6264)
diff --git a/docs/docs/guides/events_subscriptions/custom_subscriptions.md b/docs/docs/guides/events_subscriptions/custom_subscriptions.md
new file mode 100644
index 00000000000..cab85f8cc5e
--- /dev/null
+++ b/docs/docs/guides/events_subscriptions/custom_subscriptions.md
@@ -0,0 +1,238 @@
+---
+sidebar_position: 2
+sidebar_label: 'Custom Subscriptions'
+---
+
+# Custom Subscription
+
+You can extend the `Web3Subscription` class to create custom subscriptions. This way you can subscribe to custom events emitted by the provider.
+
+:::note
+This guide is most likely for advanced users who are connecting to a node that provides additional custom subscriptions. For normal users, the standard subscriptions are supported out of the box as you can find in [Supported Subscriptions](/guides/events_subscriptions/supported_subscriptions).
+:::
+
+:::important
+If you are the developer who provides custom subscriptions to users. We encourage you to develop a web3.js Plugin after you follow the guide below. However, you can find how to develop a plugin at [web3.js Plugin Developer Guide](/guides/web3_plugin_guide/plugin_authors).
+
+And even if you are not the developer who provides this custom subscription, we encourage you to write a web3.js plugin for the custom subscription, and publish it to the npm package registry. This way you can help the community. And they might contribute to your repository helping for things like: feature addition, maintenance, and bug detection.
+:::
+
+## Implementing the Subscription
+
+### Extending `Web3Subscription`
+
+To create a custom subscription, start by extending the `Web3Subscription` class. However, `Web3Subscription` is generically typed. And, generally, you need only to provide the first two types which are:
+
+- `EventMap` - The event map for events emitted by the subscription
+- `ArgsType` - The arguments passed to the subscription
+
+For example:
+
+```ts
+class MyCustomSubscription extends Web3Subscription<
+ {
+ // here provide the type of the `data` that will be emitted by the node
+ data: string;
+ },
+ // here specify the types of the arguments that will be passed to the node when subscribing
+ {
+ customArg: string;
+ }
+> {
+ // ...
+}
+```
+
+### Specify the Subscription Arguments
+
+You need to specify the exact data that will be passed to the provider. You do this by overriding `_buildSubscriptionParams` in your class. It could be something as follow:
+
+```ts
+ protected _buildSubscriptionParams() {
+ // the `someCustomSubscription` below is the name of the subscription provided by the node you are connected to.
+ return ['someCustomSubscription', this.args];
+ }
+```
+
+With the implementation above, the call that will be made to the provider will be as follow:
+
+```ts
+{
+ id: "[GUID-STRING]", // something like: '3f839900-afdd-4553-bca7-b4e2b835c687'
+ jsonrpc: '2.0',
+ method: 'eth_subscribe',
+ // The `someCustomSubscription` below is the name of the subscription provided by the node you are connected to.
+ // And the `args` is the variable that has the type you provided at the second generic type
+ // at your class definition. That is in the snippet above: `{customArg: string}`.
+ // And its value is what you provided when you will call:
+ // `web3.subscriptionManager.subscribe('custom', args)`
+ params: ['someCustomSubscription', args],
+}
+```
+
+## Additional Custom Processing
+
+You may need to do some processing in the constructor. Or you may need to do some formatting for the data before it will be fired by the event emitter. In this section you can check how to do either one of those or both.
+
+### Custom Constructor
+
+You can optionally write a constructor, in case you need to do some additional initialization or processing.
+And here is an example constructor implementation:
+
+```ts
+constructor(
+ args: {customArg: string},
+ options: {
+ subscriptionManager: Web3SubscriptionManager;
+ returnFormat?: DataFormat;
+ }
+) {
+ super(args, options);
+
+ // Additional initialization
+}
+```
+
+The constructor passes the arguments to the Web3Subscription parent class.
+
+You can access the subscription manager via `this.subscriptionManager`.
+
+### Custom formatting
+
+In case you need to format the data received from the node before it will be emitted, you just need to override the protected method `formatSubscriptionResult` in your class. It will be something like the following. However, the data type could be whatever provided by the node and it is what you should already provided at the first generic type when you extended `Web3Subscription`:
+
+```ts
+protected formatSubscriptionResult(data: string) {
+ const formattedData = format(data);
+ return formattedData;
+}
+```
+
+## Subscribing and Unsubscribing
+
+To subscribe, you need to pass the custom subscriptions to the `Web3`. And then you can call the `subscribe` method for your custom subscription, as in the following sample:
+
+```ts
+const CustomSubscriptions = {
+ // the key (`custom`) is what you chose to use when you call `web3.subscriptionManager.subscribe`.
+ // the value (`CustomSubscription`) is your class name.
+ custom: MyCustomSubscription,
+ // you can have as many custom subscriptions as you like...
+ // custom2: MyCustomSubscription2,
+ // custom3: MyCustomSubscription3,
+};
+
+const web3 = new Web3({
+ provider, // the provider that support the custom event that you like to subscribe to.
+ registeredSubscriptions: CustomSubscriptions,
+});
+
+// subscribe at the provider:
+// Note: this will internally initialize a new instance of `MyCustomSubscription`,
+// call `_buildSubscriptionParams`, and then send the `eth_subscribe` RPC call.
+const sub = web3.subscriptionManager.subscribe('custom', args);
+
+// listen to the emitted event:
+// Note: the data will be optionally formatted at `formatSubscriptionResult`, before it is emitted here.
+sub.on('data', result => {
+ // This will be called every time a new data arrived from the provider to this subscription
+});
+```
+
+To unsubscribe:
+
+```ts
+// this will send `eth_unsubscribe` to stop the subscription.
+await sub.unsubscribe();
+```
+
+## Putting it Together
+
+Here is the full example for a custom subscription implementation:
+
+```ts
+// Subscription class
+class MyCustomSubscription extends Web3Subscription<
+ {
+ // here provide the type of the `data` that will be emitted by the node
+ data: string;
+ },
+ // here specify the types of the arguments that will be passed to the node when subscribing
+ {
+ customArg: string;
+ }
+> {
+ protected _buildSubscriptionParams() {
+ // the `someCustomSubscription` below is the name of the subscription provided by the node your are connected to.
+ return ['someCustomSubscription', this.args];
+ }
+
+ protected formatSubscriptionResult(data: string) {
+ return format(data);
+ }
+
+ constructor(
+ args: { customArg: string },
+ options: {
+ subscriptionManager: Web3SubscriptionManager;
+ returnFormat?: DataFormat;
+ },
+ ) {
+ super(args, options);
+
+ // Additional initialization
+ }
+}
+
+// Usage
+
+const args = {
+ customArg: 'hello custom',
+};
+
+const CustomSubscriptions = {
+ // the key (`custom`) is what you chose to use when you call `web3.subscriptionManager.subscribe`.
+ // the value (`MyCustomSubscription`) is your class name.
+ custom: MyCustomSubscription,
+ // you can have as many custom subscriptions as you like...
+ // custom2: MyCustomSubscription2,
+ // custom3: MyCustomSubscription3,
+};
+
+const web3 = new Web3({
+ provider, // the provider that support the custom event that you like to subscribe to.
+ registeredSubscriptions: CustomSubscriptions,
+});
+
+const sub = web3.subscriptionManager.subscribe('custom', args);
+
+sub.on('data', result => {
+ // New data
+});
+
+// Unsubscribe:
+// If you want to subscribe later based on some code logic:
+// if () {
+// await sub.subscribe();
+// }
+```
+
+## Key points
+
+### Subscription definition
+
+- Extend the `Web3Subscription` class to create a custom subscription.
+- Specify in the generic typing the event data and subscription argument types.
+- Override `_buildSubscriptionParams()` to define the RPC parameters.
+- Optionally add a custom constructor for initialization logic.
+- Optionally format results with `format SubscriptionResult()` before emitting data.
+
+### Subscription usage
+
+- Register the subscription by passing it in `Web3` constructor options.
+- Subscribe/unsubscribe using the `subscriptionManager`.
+- Listen to subscription events like `'data'` for new results.
+
+## Conclusion
+
+In summary, web3.js subscriptions provide a flexible way to subscribe to custom provider events. By extending `Web3Subscription`, implementing the key methods, and registering with `Web3`, you can create tailored subscriptions for whatever custom events the provider can emit. The subscriptions API handles the underlying JSON-RPC calls and allows custom processing and formatting of results.
diff --git a/docs/docs/guides/events_subscriptions/index.md b/docs/docs/guides/events_subscriptions/index.md
new file mode 100644
index 00000000000..7e1dee9d0b2
--- /dev/null
+++ b/docs/docs/guides/events_subscriptions/index.md
@@ -0,0 +1,17 @@
+---
+sidebar_position: 5
+sidebar_label: 'Events Subscription'
+---
+
+# Events Subscription
+
+A standard Ethereum node like [Geth supports subscribing to specific events](https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub#supported-subscriptions). Additionally, there are some Ethereum nodes that provide additional custom subscriptions. As you can find in [Supported Subscriptions](/guides/events_subscriptions/supported_subscriptions) guide, web3.js enables you to subscribe to the standard events out of the box. And it also provides you with the capability to subscribe to custom subscriptions as you can find in the [Custom Subscriptions](/guides/events_subscriptions/custom_subscriptions) guide.
+
+:::important
+If you are the developer who provides custom subscriptions to users. We encourage you to develop a web3.js Plugin after you go through the [Custom Subscription](#custom-subscription) section below. You can find how to develop a plugin at [web3.js Plugin Developer Guide](/guides/web3_plugin_guide/plugin_authors)
+:::
+
+## Here are the guides for events subscription
+
+- [Supported Subscriptions Guide](/guides/events_subscriptions/supported_subscriptions)
+- [Custom Subscriptions Guide](/guides/events_subscriptions/custom_subscriptions)
diff --git a/docs/docs/guides/events_subscriptions/supported_subscriptions.md b/docs/docs/guides/events_subscriptions/supported_subscriptions.md
new file mode 100644
index 00000000000..9e2df8c89b8
--- /dev/null
+++ b/docs/docs/guides/events_subscriptions/supported_subscriptions.md
@@ -0,0 +1,15 @@
+---
+sidebar_position: 1
+sidebar_label: 'Supported Subscriptions'
+---
+
+# Supported Subscriptions
+
+web3.js supports the standard Ethereum subscriptions out of the box. And they are the ones registered inside [registeredSubscriptions](/api/web3-eth#registeredSubscriptions) object. Here are a list of them:
+
+- `logs`: implemented in the class [`LogsSubscription`](/api/web3-eth/class/LogsSubscription).
+- `newBlockHeaders`: implemented in the class [`NewHeadsSubscription`](/api/web3-eth/class/NewHeadsSubscription).
+- `newHeads` same as `newBlockHeaders`.
+- `newPendingTransactions`: implemented in the class [`NewPendingTransactionsSubscription`](/api/web3-eth/class/NewPendingTransactionsSubscription).
+- `pendingTransactions`: same as `newPendingTransactions`.
+- `syncing`: implemented in the class [`SyncingSubscription`](/api/web3-eth/class/SyncingSubscription)
diff --git a/packages/web3-core/CHANGELOG.md b/packages/web3-core/CHANGELOG.md
index 9532ee987fc..1c97ca972e1 100644
--- a/packages/web3-core/CHANGELOG.md
+++ b/packages/web3-core/CHANGELOG.md
@@ -138,6 +138,10 @@ Documentation:
## [4.0.3]
+### Added
+
+- Expose `subscriptionManager` as a `protected get` at `Web3Subscription` to be able to use it inside custom subscriptions, if needed. (#6285)
+
### Changed
- Dependencies updated
diff --git a/packages/web3-core/src/web3_subscriptions.ts b/packages/web3-core/src/web3_subscriptions.ts
index 6a8a6b503ba..3cf4cee9f50 100644
--- a/packages/web3-core/src/web3_subscriptions.ts
+++ b/packages/web3-core/src/web3_subscriptions.ts
@@ -146,6 +146,11 @@ export abstract class Web3Subscription<
protected get returnFormat() {
return this._returnFormat;
}
+
+ protected get subscriptionManager() {
+ return this._subscriptionManager;
+ }
+
public async resubscribe() {
await this.unsubscribe();
await this.subscribe();
diff --git a/packages/web3-core/test/unit/web3_subscription.test.ts b/packages/web3-core/test/unit/web3_subscription.test.ts
index 7a5efedaa04..cab940b0f00 100644
--- a/packages/web3-core/test/unit/web3_subscription.test.ts
+++ b/packages/web3-core/test/unit/web3_subscription.test.ts
@@ -36,6 +36,22 @@ describe('Web3Subscription', () => {
subscriptionManager = new Web3SubscriptionManager(requestManager, subscriptions);
});
+ describe('subscriptionManager', () => {
+ it('subscriptionManager is accessible in inherited subscription', async () => {
+ class InheritedExampleSubscription extends ExampleSubscription {
+ public verifyAccessToSubscriptionManager(
+ originalSubscriptionManager: Web3SubscriptionManager,
+ ) {
+ expect(this.subscriptionManager).toBe(originalSubscriptionManager);
+ }
+ }
+ new InheritedExampleSubscription(
+ { param1: 'value' },
+ { subscriptionManager },
+ ).verifyAccessToSubscriptionManager(subscriptionManager);
+ });
+ });
+
describe('subscribe', () => {
beforeEach(() => {
sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager });
diff --git a/packages/web3-eth-abi/CHANGELOG.md b/packages/web3-eth-abi/CHANGELOG.md
index 20d0819bccd..4cc31c0f92c 100644
--- a/packages/web3-eth-abi/CHANGELOG.md
+++ b/packages/web3-eth-abi/CHANGELOG.md
@@ -125,3 +125,7 @@ Documentation:
- Dependencies updated
## [Unreleased]
+
+### Added
+
+- A `getEncodedEip712Data` method that takes an EIP-712 typed data object and returns the encoded data with the option to also keccak256 hash it (#6286)
diff --git a/packages/web3-eth-abi/src/eip_712.ts b/packages/web3-eth-abi/src/eip_712.ts
new file mode 100644
index 00000000000..948591bb474
--- /dev/null
+++ b/packages/web3-eth-abi/src/eip_712.ts
@@ -0,0 +1,201 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+
+/**
+ * @note This code was taken from: https://github.com/Mrtenz/eip-712/tree/master
+ */
+
+import { Eip712TypedData } from 'web3-types';
+import { isNullish, keccak256 } from 'web3-utils';
+
+import ethersAbiCoder from './ethers_abi_coder.js';
+
+const TYPE_REGEX = /^\w+/;
+const ARRAY_REGEX = /^(.*)\[([0-9]*?)]$/;
+
+/**
+ * Get the dependencies of a struct type. If a struct has the same dependency multiple times, it's only included once
+ * in the resulting array.
+ */
+const getDependencies = (
+ typedData: Eip712TypedData,
+ type: string,
+ dependencies: string[] = [],
+): string[] => {
+ const match = type.match(TYPE_REGEX)!;
+ const actualType = match[0];
+ if (dependencies.includes(actualType)) {
+ return dependencies;
+ }
+
+ if (!typedData.types[actualType]) {
+ return dependencies;
+ }
+
+ return [
+ actualType,
+ ...typedData.types[actualType].reduce(
+ (previous, _type) => [
+ ...previous,
+ ...getDependencies(typedData, _type.type, previous).filter(
+ dependency => !previous.includes(dependency),
+ ),
+ ],
+ [],
+ ),
+ ];
+};
+
+/**
+ * Encode a type to a string. All dependant types are alphabetically sorted.
+ *
+ * @param {TypedData} typedData
+ * @param {string} type
+ * @param {Options} [options]
+ * @return {string}
+ */
+const encodeType = (typedData: Eip712TypedData, type: string): string => {
+ const [primary, ...dependencies] = getDependencies(typedData, type);
+ // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
+ const types = [primary, ...dependencies.sort()];
+
+ return types
+ .map(
+ dependency =>
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${dependency}(${typedData.types[dependency].map(
+ _type => `${_type.type} ${_type.name}`,
+ )})`,
+ )
+ .join('');
+};
+
+/**
+ * Get a type string as hash.
+ */
+const getTypeHash = (typedData: Eip712TypedData, type: string) =>
+ keccak256(encodeType(typedData, type));
+
+/**
+ * Get encoded data as a hash. The data should be a key -> value object with all the required values. All dependant
+ * types are automatically encoded.
+ */
+const getStructHash = (
+ typedData: Eip712TypedData,
+ type: string,
+ data: Record,
+ // eslint-disable-next-line no-use-before-define
+): string => keccak256(encodeData(typedData, type, data));
+
+/**
+ * Get the EIP-191 encoded message to sign, from the typedData object. If `hash` is enabled, the message will be hashed
+ * with Keccak256.
+ */
+export const getMessage = (typedData: Eip712TypedData, hash?: boolean): string => {
+ const EIP_191_PREFIX = '1901';
+ const message = `0x${EIP_191_PREFIX}${getStructHash(
+ typedData,
+ 'EIP712Domain',
+ typedData.domain as Record,
+ ).substring(2)}${getStructHash(typedData, typedData.primaryType, typedData.message).substring(
+ 2,
+ )}`;
+
+ if (hash) {
+ return keccak256(message);
+ }
+
+ return message;
+};
+
+/**
+ * Encodes a single value to an ABI serialisable string, number or Buffer. Returns the data as tuple, which consists of
+ * an array of ABI compatible types, and an array of corresponding values.
+ */
+const encodeValue = (
+ typedData: Eip712TypedData,
+ type: string,
+ data: unknown,
+): [string, string | Uint8Array | number] => {
+ const match = type.match(ARRAY_REGEX);
+
+ // Checks for array types
+ if (match) {
+ const arrayType = match[1];
+ const length = Number(match[2]) || undefined;
+
+ if (!Array.isArray(data)) {
+ throw new Error('Cannot encode data: value is not of array type');
+ }
+
+ if (length && data.length !== length) {
+ throw new Error(
+ `Cannot encode data: expected length of ${length}, but got ${data.length}`,
+ );
+ }
+
+ const encodedData = data.map(item => encodeValue(typedData, arrayType, item));
+ const types = encodedData.map(item => item[0]);
+ const values = encodedData.map(item => item[1]);
+
+ return ['bytes32', keccak256(ethersAbiCoder.encode(types, values))];
+ }
+
+ if (typedData.types[type]) {
+ return ['bytes32', getStructHash(typedData, type, data as Record)];
+ }
+
+ // Strings and arbitrary byte arrays are hashed to bytes32
+ if (type === 'string') {
+ return ['bytes32', keccak256(data as string)];
+ }
+
+ if (type === 'bytes') {
+ return ['bytes32', keccak256(data as string)];
+ }
+
+ return [type, data as string];
+};
+
+/**
+ * Encode the data to an ABI encoded Buffer. The data should be a key -> value object with all the required values. All
+ * dependant types are automatically encoded.
+ */
+const encodeData = (
+ typedData: Eip712TypedData,
+ type: string,
+ data: Record,
+): string => {
+ const [types, values] = typedData.types[type].reduce<[string[], unknown[]]>(
+ ([_types, _values], field) => {
+ if (isNullish(data[field.name]) || isNullish(data[field.name])) {
+ throw new Error(`Cannot encode data: missing data for '${field.name}'`);
+ }
+
+ const value = data[field.name];
+ const [_type, encodedValue] = encodeValue(typedData, field.type, value);
+
+ return [
+ [..._types, _type],
+ [..._values, encodedValue],
+ ];
+ },
+ [['bytes32'], [getTypeHash(typedData, type)]],
+ );
+
+ return ethersAbiCoder.encode(types, values);
+};
diff --git a/packages/web3-eth-abi/src/index.ts b/packages/web3-eth-abi/src/index.ts
index 6a8754e486a..baa57947847 100644
--- a/packages/web3-eth-abi/src/index.ts
+++ b/packages/web3-eth-abi/src/index.ts
@@ -25,3 +25,4 @@ export * from './api/logs_api.js';
export * from './api/parameters_api.js';
export * from './utils.js';
export * from './decode_contract_error_data.js';
+export { getMessage as getEncodedEip712Data } from './eip_712.js';
diff --git a/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts b/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts
new file mode 100644
index 00000000000..ff7474aa33f
--- /dev/null
+++ b/packages/web3-eth-abi/test/fixtures/get_encoded_eip712_data.ts
@@ -0,0 +1,758 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { Eip712TypedData } from 'web3-types';
+
+/**
+ * string is the test title
+ * Eip712TypedData is the entire EIP-712 typed data object
+ * boolean is whether the EIP-712 encoded data is keccak256 hashed
+ * string is the encoded data expected to be returned by getEncodedEip712Data
+ */
+export const testData: [string, Eip712TypedData, boolean | undefined, string][] = [
+ [
+ 'should get encoded message without hashing, hash = undefined',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+ },
+ undefined,
+ '0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
+ ],
+ [
+ 'should get encoded message without hashing, hash = false',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+ },
+ false,
+ '0x1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
+ ],
+ [
+ 'should get the hashed encoded message, hash = true',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+ },
+ true,
+ '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2',
+ ],
+ [
+ 'should get encoded message with array types',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: [
+ '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ ],
+ array3: [123456, 654321, 42],
+ },
+ },
+ false,
+ '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058596a0bafab67b5b49cfe99456c50dd5b6294b1383e4f17c6e5c3c14afee96ac3',
+ ],
+ [
+ 'should get encoded message with array types',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: [
+ '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ ],
+ array3: [123456, 654321, 42],
+ },
+ },
+ true,
+ '0x3e4d581a408c8c2fa8775017c26e0127df030593d83a8202e6c19b3380bde3da',
+ ],
+ [
+ 'should get encoded message with fixed array',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[3]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: [
+ '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ ],
+ array3: [123456, 654321, 42],
+ },
+ },
+ false,
+ '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058b068b45d685c16bc9ef637106b4fd3a4fb9aa259f53218491a3d9eb65b1b574c',
+ ],
+ [
+ 'should get encoded message with fixed array',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[3]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: [
+ '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ ],
+ array3: [123456, 654321, 42],
+ },
+ },
+ true,
+ '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e',
+ ],
+ [
+ 'should get encoded message with bytes32',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'bytes32',
+ type: 'bytes32',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e',
+ },
+ },
+ false,
+ '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba000587c9d26380d51aac5dc2ff6f794d1c043ea4259bb42068f70f79d2e4849133ac3',
+ ],
+ [
+ 'should get encoded message with bytes32',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'bytes32',
+ type: 'bytes',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e',
+ },
+ },
+ false,
+ '0x1901928e4773f1f7243172cd0dd213906be49eb9d275e09c8bd0575921c51ba00058353ed034fd1df0cd409a19133f4a89f5e99ddc735ad3fbb767d0bb72c97ef175',
+ ],
+ [
+ 'should get encoded message with bytes32',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'bytes32',
+ type: 'bytes32',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ bytes32: '0x133d00e67f2390ce846a631aeb6718a674a3923f5320b79b6d3e2f5bf146319e',
+ },
+ },
+ true,
+ '0xa6cd048c02ef3cb70feee1bd9795decbbc8b431b976dfc86e3b09e55e0d2a3f3',
+ ],
+];
+
+/**
+ * string is the test title
+ * Eip712TypedData is the entire EIP-712 typed data object
+ * boolean is whether the EIP-712 encoded data is keccak256 hashed
+ * string is the encoded data expected to be returned by getEncodedEip712Data
+ */
+export const erroneousTestData: [string, Eip712TypedData, boolean | undefined, Error][] = [
+ [
+ 'should throw error: Cannot encode data: value is not of array type',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ array3: [123456, 654321, 42],
+ },
+ },
+ false,
+ new Error('Cannot encode data: value is not of array type'),
+ ],
+ [
+ 'should throw error: Cannot encode data: expected length of 3, but got 1',
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[3]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: ['0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'],
+ array3: [123456, 654321, 42],
+ },
+ },
+ false,
+ new Error('Cannot encode data: expected length of 3, but got 1'),
+ ],
+ [
+ "should throw error: Cannot encode data: missing data for 'array3'",
+ {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ ArrayData: [
+ {
+ name: 'array1',
+ type: 'string[]',
+ },
+ {
+ name: 'array2',
+ type: 'address[]',
+ },
+ {
+ name: 'array3',
+ type: 'uint256[]',
+ },
+ ],
+ },
+ primaryType: 'ArrayData',
+ domain: {
+ name: 'Array Data',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ array1: ['string', 'string2', 'string3'],
+ array2: ['0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'],
+ array3: undefined,
+ },
+ },
+ false,
+ new Error("Cannot encode data: missing data for 'array3'"),
+ ],
+];
diff --git a/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts b/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts
new file mode 100644
index 00000000000..d40c5f25511
--- /dev/null
+++ b/packages/web3-eth-abi/test/unit/get_encoded_eip712_data.test.ts
@@ -0,0 +1,29 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { getEncodedEip712Data } from '../../src/index';
+import { erroneousTestData, testData } from '../fixtures/get_encoded_eip712_data';
+
+describe('getEncodedEip712Data', () => {
+ it.each(testData)('%s', (_, typedData, hashEncodedData, expectedResponse) => {
+ const encodedMessage = getEncodedEip712Data(typedData, hashEncodedData);
+ expect(encodedMessage).toBe(expectedResponse);
+ });
+
+ it.each(erroneousTestData)('%s', (_, typedData, hashEncodedData, expectedError) => {
+ expect(() => getEncodedEip712Data(typedData, hashEncodedData)).toThrowError(expectedError);
+ });
+});
diff --git a/packages/web3-eth/CHANGELOG.md b/packages/web3-eth/CHANGELOG.md
index 17ce2a4440c..0fcf7735900 100644
--- a/packages/web3-eth/CHANGELOG.md
+++ b/packages/web3-eth/CHANGELOG.md
@@ -166,7 +166,13 @@ Documentation:
### Fixed
- sendTransaction will have gas filled by default using method `estimateGas` unless transaction builder `options.fillGas` is false. (#6249)
+- Missing `blockHeaderSchema` properties causing some properties to not appear in response of `newHeads` subscription (#6243)
### Changed
- `MissingGasError` error message changed for clarity (#6215)
+-
+### Added
+
+- A `rpc_method_wrapper` (`signTypedData`) for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286)
+- A `signTypedData` method to the `Web3Eth` class (#6286)
diff --git a/packages/web3-eth/package.json b/packages/web3-eth/package.json
index 97fa69ac0ac..4475b2acaa3 100644
--- a/packages/web3-eth/package.json
+++ b/packages/web3-eth/package.json
@@ -36,6 +36,8 @@
"test": "jest --config=./test/unit/jest.config.js",
"test:coverage:unit": "jest --config=./test/unit/jest.config.js --coverage=true --coverage-reporters=text",
"test:ci": "jest --coverage=true --coverage-reporters=json --verbose",
+ "test:e2e:mainnet": "jest --config=./test/e2e/jest.config.js --forceExit",
+ "test:e2e:sepolia": "jest --config=./test/e2e/jest.config.js --forceExit",
"test:watch": "npm test -- --watch",
"test:unit": "jest --config=./test/unit/jest.config.js",
"test:integration": "jest --config=./test/integration/jest.config.js --runInBand --forceExit",
diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts
index 246cbf67ca0..1d7d1915405 100644
--- a/packages/web3-eth/src/rpc_method_wrappers.ts
+++ b/packages/web3-eth/src/rpc_method_wrappers.ts
@@ -47,6 +47,7 @@ import {
TransactionWithFromAndToLocalWalletIndex,
TransactionForAccessList,
AccessListResult,
+ Eip712TypedData,
} from 'web3-types';
import { Web3Context, Web3PromiEvent } from 'web3-core';
import { format, hexToBytes, bytesToUint8Array, numberToHex } from 'web3-utils';
@@ -1125,3 +1126,24 @@ export async function createAccessList(
return format(accessListResultSchema, response, returnFormat);
}
+
+/**
+ * View additional documentations here: {@link Web3Eth.signTypedData}
+ * @param web3Context ({@link Web3Context}) Web3 configuration object that contains things such as the provider, request manager, wallet, etc.
+ */
+export async function signTypedData(
+ web3Context: Web3Context,
+ address: Address,
+ typedData: Eip712TypedData,
+ useLegacy: boolean,
+ returnFormat: ReturnFormat,
+) {
+ const response = await ethRpcMethods.signTypedData(
+ web3Context.requestManager,
+ address,
+ typedData,
+ useLegacy,
+ );
+
+ return format({ format: 'bytes' }, response, returnFormat);
+}
diff --git a/packages/web3-eth/src/schemas.ts b/packages/web3-eth/src/schemas.ts
index f5b200204db..3a25068f049 100644
--- a/packages/web3-eth/src/schemas.ts
+++ b/packages/web3-eth/src/schemas.ts
@@ -321,13 +321,37 @@ export const blockSchema = {
},
};
+export const withdrawalsSchema = {
+ type: 'object',
+ properties: {
+ index: {
+ format: 'uint',
+ },
+ validatorIndex: {
+ format: 'uint',
+ },
+ address: {
+ format: 'bytes32',
+ },
+ amount: {
+ format: 'uint',
+ },
+ },
+};
+
export const blockHeaderSchema = {
type: 'object',
properties: {
+ author: {
+ format: 'bytes32',
+ },
+ hash: {
+ format: 'bytes32',
+ },
parentHash: {
format: 'bytes32',
},
- receiptRoot: {
+ receiptsRoot: {
format: 'bytes32',
},
miner: {
@@ -339,12 +363,18 @@ export const blockHeaderSchema = {
transactionsRoot: {
format: 'bytes32',
},
+ withdrawalsRoot: {
+ format: 'bytes32',
+ },
logsBloom: {
format: 'bytes256',
},
difficulty: {
format: 'uint',
},
+ totalDifficulty: {
+ format: 'uint',
+ },
number: {
format: 'uint',
},
@@ -366,6 +396,36 @@ export const blockHeaderSchema = {
sha3Uncles: {
format: 'bytes32',
},
+ size: {
+ format: 'uint',
+ },
+ baseFeePerGas: {
+ format: 'uint',
+ },
+ excessDataGas: {
+ format: 'uint',
+ },
+ mixHash: {
+ format: 'bytes32',
+ },
+ transactions: {
+ type: 'array',
+ items: {
+ format: 'bytes32',
+ },
+ },
+ uncles: {
+ type: 'array',
+ items: {
+ format: 'bytes32',
+ },
+ },
+ withdrawals: {
+ type: 'array',
+ items: {
+ ...withdrawalsSchema,
+ },
+ },
},
};
diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts
index cd1c57bcc2b..c4b3b6a92f8 100644
--- a/packages/web3-eth/src/web3_eth.ts
+++ b/packages/web3-eth/src/web3_eth.ts
@@ -37,6 +37,7 @@ import {
TransactionForAccessList,
DataFormat,
DEFAULT_RETURN_FORMAT,
+ Eip712TypedData,
} from 'web3-types';
import { isSupportedProvider, Web3Context, Web3ContextInitOptions } from 'web3-core';
import { TransactionNotFound } from 'web3-errors';
@@ -1534,6 +1535,24 @@ export class Web3Eth extends Web3Context(
+ address: Address,
+ typedData: Eip712TypedData,
+ useLegacy = false,
+ returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat,
+ ) {
+ return rpcMethodsWrappers.signTypedData(this, address, typedData, useLegacy, returnFormat);
+ }
+
/**
* Lets you subscribe to specific events in the blockchain.
*
diff --git a/packages/web3-eth/test/e2e/accounts.json b/packages/web3-eth/test/e2e/accounts.json
new file mode 120000
index 00000000000..7dbcddb60a2
--- /dev/null
+++ b/packages/web3-eth/test/e2e/accounts.json
@@ -0,0 +1 @@
+../../../../scripts/accounts.json
\ No newline at end of file
diff --git a/packages/web3-eth/test/e2e/e2e_utils.ts b/packages/web3-eth/test/e2e/e2e_utils.ts
new file mode 100644
index 00000000000..6cc6d49e60e
--- /dev/null
+++ b/packages/web3-eth/test/e2e/e2e_utils.ts
@@ -0,0 +1,85 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+
+/**
+ * @NOTE This Util method is kept seperate from shared system_test_utils.ts file because
+ * of it's import of .secrets.json. For this method to exist in shared system_test_utils.ts
+ * file, the import path would be ../.secrets.json which doesn't resolve when the file is
+ * copied over to each package's test directory. Because web3 package is the only package
+ * running these E2E tests that use Sepolia and Mainnet, this util exists here for now.
+ */
+
+import { getSystemTestBackend } from '../fixtures/system_test_utils';
+// eslint-disable-next-line import/no-relative-packages
+import secrets from '../../../../.secrets.json';
+
+export const getSystemE2ETestProvider = (): string => {
+ if (process.env.WEB3_SYTEM_TEST_MODE === 'http') {
+ return getSystemTestBackend() === 'sepolia'
+ ? process.env.INFURA_SEPOLIA_HTTP ?? secrets.SEPOLIA.HTTP
+ : process.env.INFURA_MAINNET_HTTP ?? secrets.MAINNET.HTTP;
+ }
+ return getSystemTestBackend() === 'sepolia'
+ ? process.env.INFURA_SEPOLIA_WS ?? secrets.SEPOLIA.WS
+ : process.env.INFURA_MAINNET_WS ?? secrets.MAINNET.WS;
+};
+
+export const getE2ETestAccountAddress = (): string => {
+ if (process.env.TEST_ACCOUNT_ADDRESS !== undefined) {
+ return process.env.TEST_ACCOUNT_ADDRESS;
+ // eslint-disable-next-line no-else-return
+ } else if (getSystemTestBackend() === 'sepolia' || getSystemTestBackend() === 'mainnet') {
+ return secrets[getSystemTestBackend().toUpperCase() as 'SEPOLIA' | 'MAINNET'].ACCOUNT
+ .address;
+ }
+
+ throw new Error('Unable to get test account address');
+};
+
+export const getE2ETestContractAddress = () =>
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ secrets[getSystemTestBackend().toUpperCase() as 'SEPOLIA' | 'MAINNET']
+ .DEPLOYED_TEST_CONTRACT_ADDRESS as string;
+
+export const getAllowedSendTransaction = (): boolean => {
+ if (process.env.ALLOWED_SEND_TRANSACTION !== undefined) {
+ // https://github.com/actions/runner/issues/1483
+ if (process.env.ALLOWED_SEND_TRANSACTION === 'false') {
+ return false;
+ }
+
+ return Boolean(process.env.ALLOWED_SEND_TRANSACTION);
+ // eslint-disable-next-line no-else-return
+ } else if (getSystemTestBackend() === 'sepolia' || getSystemTestBackend() === 'mainnet') {
+ return secrets[getSystemTestBackend().toUpperCase() as 'SEPOLIA' | 'MAINNET']
+ .ALLOWED_SEND_TRANSACTION;
+ }
+
+ return false;
+};
+
+export const getE2ETestAccountPrivateKey = (): string => {
+ if (process.env.TEST_ACCOUNT_PRIVATE_KEY !== undefined) {
+ return process.env.TEST_ACCOUNT_PRIVATE_KEY;
+ // eslint-disable-next-line no-else-return
+ } else if (getSystemTestBackend() === 'sepolia' || getSystemTestBackend() === 'mainnet') {
+ return secrets[getSystemTestBackend().toUpperCase() as 'SEPOLIA' | 'MAINNET'].ACCOUNT
+ .privateKey;
+ }
+
+ throw new Error('Unable to get test account private key');
+};
diff --git a/packages/web3-eth/test/e2e/jest.config.js b/packages/web3-eth/test/e2e/jest.config.js
new file mode 100644
index 00000000000..cc3c69f72ff
--- /dev/null
+++ b/packages/web3-eth/test/e2e/jest.config.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const base = require('../config/jest.config');
+
+module.exports = {
+ ...base,
+ setupFilesAfterEnv: ['/test/e2e/setup.js'],
+ testMatch: [
+ `/test/e2e/*.(spec|test).(js|ts)`,
+ `/test/e2e/${process.env.WEB3_SYSTEM_TEST_BACKEND}/**/*.(spec|test).(js|ts)`,
+ ],
+ /**
+ * restoreMocks [boolean]
+ *
+ * Default: false
+ *
+ * Automatically restore mock state between every test.
+ * Equivalent to calling jest.restoreAllMocks() between each test.
+ * This will lead to any mocks having their fake implementations removed
+ * and restores their initial implementation.
+ */
+ restoreMocks: true,
+
+ /**
+ * resetModules [boolean]
+ *
+ * Default: false
+ *
+ * By default, each test file gets its own independent module registry.
+ * Enabling resetModules goes a step further and resets the module registry before running each individual test.
+ * This is useful to isolate modules for every test so that local module state doesn't conflict between tests.
+ * This can be done programmatically using jest.resetModules().
+ */
+ resetModules: true,
+ coverageDirectory: `.coverage/e2e/${process.env.WEB3_SYSTEM_TEST_BACKEND}`,
+};
diff --git a/packages/web3-eth/test/e2e/setup.js b/packages/web3-eth/test/e2e/setup.js
new file mode 100644
index 00000000000..fddbec59a1e
--- /dev/null
+++ b/packages/web3-eth/test/e2e/setup.js
@@ -0,0 +1,24 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+
+// Have to use `require` because of Jest issue https://jestjs.io/docs/ecmascript-modules
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+require('../config/setup');
+
+const jestTimeout = 30000; // Sometimes `in3` takes long time because of its decentralized nature.
+
+jest.setTimeout(jestTimeout);
diff --git a/packages/web3-eth/test/e2e/subscription_new_heads.test.ts b/packages/web3-eth/test/e2e/subscription_new_heads.test.ts
new file mode 100644
index 00000000000..01bb6b5fc4e
--- /dev/null
+++ b/packages/web3-eth/test/e2e/subscription_new_heads.test.ts
@@ -0,0 +1,88 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+// eslint-disable-next-line import/no-extraneous-dependencies
+import Web3, { BlockHeaderOutput } from 'web3';
+
+import {
+ closeOpenConnection,
+ getSystemTestBackend,
+ itIf,
+ waitForOpenConnection,
+} from '../fixtures/system_test_utils';
+import { getSystemE2ETestProvider } from './e2e_utils';
+
+describe(`${getSystemTestBackend()} tests - subscription newHeads`, () => {
+ const provider = getSystemE2ETestProvider();
+ const expectedNumberOfNewHeads = 1;
+
+ let web3: Web3;
+
+ beforeAll(() => {
+ web3 = new Web3(provider);
+ });
+
+ afterAll(async () => {
+ await closeOpenConnection(web3);
+ });
+
+ itIf(provider.startsWith('ws'))(
+ `should subscribe to newHeads and receive ${expectedNumberOfNewHeads}`,
+ async () => {
+ const newHeadsSubscription = await web3.eth.subscribe('newHeads');
+
+ let numberOfNewHeadsReceived = 0;
+
+ await waitForOpenConnection(web3.eth);
+ const assertionPromise = new Promise((resolve, reject) => {
+ newHeadsSubscription.on('data', (data: BlockHeaderOutput) => {
+ try {
+ expect(data).toMatchObject({
+ hash: expect.any(String),
+ parentHash: expect.any(String),
+ receiptsRoot: expect.any(String),
+ miner: expect.any(String),
+ stateRoot: expect.any(String),
+ transactionsRoot: expect.any(String),
+ logsBloom: expect.any(String),
+ difficulty: expect.any(BigInt),
+ number: expect.any(BigInt),
+ gasLimit: expect.any(BigInt),
+ gasUsed: expect.any(BigInt),
+ timestamp: expect.any(BigInt),
+ extraData: expect.any(String),
+ nonce: expect.any(BigInt),
+ sha3Uncles: expect.any(String),
+ baseFeePerGas: expect.any(BigInt),
+ mixHash: expect.any(String),
+ withdrawalsRoot: expect.any(String),
+ });
+ } catch (error) {
+ reject(error);
+ }
+
+ numberOfNewHeadsReceived += 1;
+ if (numberOfNewHeadsReceived === expectedNumberOfNewHeads) resolve(undefined);
+ });
+
+ newHeadsSubscription.on('error', error => reject(error));
+ });
+
+ await assertionPromise;
+ await web3.eth.subscriptionManager?.removeSubscription(newHeadsSubscription);
+ },
+ );
+});
diff --git a/packages/web3-eth/test/integration/subscription_heads.test.ts b/packages/web3-eth/test/integration/subscription_heads.test.ts
index 44ef600efc1..c801e5e83ef 100644
--- a/packages/web3-eth/test/integration/subscription_heads.test.ts
+++ b/packages/web3-eth/test/integration/subscription_heads.test.ts
@@ -43,7 +43,27 @@ describeIf(isSocket)('subscription', () => {
let times = 0;
const pr = new Promise((resolve: Resolve, reject) => {
sub.on('data', (data: BlockHeaderOutput) => {
- expect(typeof data.parentHash).toBe('string');
+ try {
+ expect(typeof data.hash).toBe('string');
+ expect(typeof data.parentHash).toBe('string');
+ expect(typeof data.receiptsRoot).toBe('string');
+ expect(typeof data.miner).toBe('string');
+ expect(typeof data.stateRoot).toBe('string');
+ expect(typeof data.transactionsRoot).toBe('string');
+ expect(typeof data.logsBloom).toBe('string');
+ expect(typeof data.difficulty).toBe('bigint');
+ expect(typeof data.number).toBe('bigint');
+ expect(typeof data.gasLimit).toBe('bigint');
+ expect(typeof data.gasUsed).toBe('bigint');
+ expect(typeof data.timestamp).toBe('bigint');
+ expect(typeof data.extraData).toBe('string');
+ expect(typeof data.nonce).toBe('bigint');
+ expect(typeof data.sha3Uncles).toBe('string');
+ expect(typeof data.baseFeePerGas).toBe('bigint');
+ expect(typeof data.mixHash).toBe('string');
+ } catch (error) {
+ reject(error);
+ }
times += 1;
expect(times).toBeGreaterThanOrEqual(times);
diff --git a/packages/web3-eth/test/integration/subscription_logs.test.ts b/packages/web3-eth/test/integration/subscription_logs.test.ts
index eaaeaa4847e..469f3bb88eb 100644
--- a/packages/web3-eth/test/integration/subscription_logs.test.ts
+++ b/packages/web3-eth/test/integration/subscription_logs.test.ts
@@ -93,7 +93,7 @@ describeIf(isSocket)('subscription', () => {
let count = 0;
- const pr = new Promise((resolve: Resolve) => {
+ const pr = new Promise((resolve: Resolve, reject) => {
sub.on('data', (data: any) => {
count += 1;
const decodedData = decodeEventABI(
@@ -106,9 +106,14 @@ describeIf(isSocket)('subscription', () => {
resolve();
}
});
- });
+ sub.on('error', reject);
- await makeFewTxToContract({ contract: contractDeployed, sendOptions, testDataString });
+ makeFewTxToContract({
+ contract: contractDeployed,
+ sendOptions,
+ testDataString,
+ }).catch(e => reject(e));
+ });
await pr;
await web3Eth.clearSubscriptions();
diff --git a/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts b/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts
new file mode 100644
index 00000000000..741690bc47f
--- /dev/null
+++ b/packages/web3-eth/test/integration/web3_eth/sign_typed_data.test.ts
@@ -0,0 +1,206 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { getEncodedEip712Data } from 'web3-eth-abi';
+import { ecrecover, toUint8Array } from 'web3-eth-accounts';
+import { bytesToHex, hexToNumber, keccak256 } from 'web3-utils';
+
+import Web3Eth from '../../../src';
+import {
+ closeOpenConnection,
+ createTempAccount,
+ getSystemTestBackend,
+ getSystemTestProvider,
+ itIf,
+} from '../../fixtures/system_test_utils';
+
+describe('Web3Eth.signTypedData', () => {
+ let web3Eth: Web3Eth;
+ let tempAcc: { address: string; privateKey: string };
+
+ beforeAll(async () => {
+ web3Eth = new Web3Eth(getSystemTestProvider());
+ tempAcc = await createTempAccount();
+ });
+
+ afterAll(async () => {
+ await closeOpenConnection(web3Eth);
+ });
+
+ itIf(getSystemTestBackend() === 'ganache')(
+ 'should sign the typed data, return the signature, and recover the correct ETH address',
+ async () => {
+ const typedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+ };
+ const encodedTypedDataHash = getEncodedEip712Data(typedData, true);
+ const signature = await web3Eth.signTypedData(tempAcc.address, typedData);
+ const r = toUint8Array(signature.slice(0, 66));
+ const s = toUint8Array(`0x${signature.slice(66, 130)}`);
+ const v = BigInt(hexToNumber(`0x${signature.slice(130, 132)}`));
+ const recoveredPublicKey = bytesToHex(
+ ecrecover(toUint8Array(encodedTypedDataHash), v, r, s),
+ );
+
+ const recoveredAddress = `0x${keccak256(bytesToHex(recoveredPublicKey)).slice(-40)}`;
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(recoveredAddress).toBe(tempAcc.address);
+ },
+ );
+
+ itIf(getSystemTestBackend() === 'ganache')(
+ 'should sign the typed data (using legacy RPC method), return the signature, and recover the correct ETH address',
+ async () => {
+ const typedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+ };
+ const encodedTypedDataHash = getEncodedEip712Data(typedData, true);
+ const signature = await web3Eth.signTypedData(tempAcc.address, typedData, true);
+ const r = toUint8Array(signature.slice(0, 66));
+ const s = toUint8Array(`0x${signature.slice(66, 130)}`);
+ const v = BigInt(hexToNumber(`0x${signature.slice(130, 132)}`));
+ const recoveredPublicKey = bytesToHex(
+ ecrecover(toUint8Array(encodedTypedDataHash), v, r, s),
+ );
+
+ const recoveredAddress = `0x${keccak256(bytesToHex(recoveredPublicKey)).slice(-40)}`;
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(recoveredAddress).toBe(tempAcc.address);
+ },
+ );
+});
diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts
new file mode 100644
index 00000000000..b01f33f698e
--- /dev/null
+++ b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/sign_typed_data.ts
@@ -0,0 +1,100 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { Address, Eip712TypedData } from 'web3-types';
+
+const address = '0x407d73d8a49eeb85d32cf465507dd71d507100c1';
+
+const typedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+};
+
+export const mockRpcResponse =
+ '0xf326421b6b34e1e59a8a34c986861e8790a9402a9e51e012718872cd51dad4e23c590bd170be23c51cff4b44d8d4eba54120431ca6a04940098dae62d97677da1c';
+
+/**
+ * Array consists of:
+ * - Test title
+ * - Input parameters:
+ * - address
+ * - message
+ */
+type TestData = [string, [Address, Eip712TypedData, boolean]];
+export const testData: TestData[] = [
+ ['useLegacy = false', [address, typedData, false]],
+ ['useLegacy = true', [address, typedData, true]],
+];
diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts
new file mode 100644
index 00000000000..8256beb7f78
--- /dev/null
+++ b/packages/web3-eth/test/unit/rpc_method_wrappers/sign_typed_data.test.ts
@@ -0,0 +1,64 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { Web3Context } from 'web3-core';
+import { DEFAULT_RETURN_FORMAT, FMT_BYTES, FMT_NUMBER, Web3EthExecutionAPI } from 'web3-types';
+import { ethRpcMethods } from 'web3-rpc-methods';
+import { format } from 'web3-utils';
+
+import { signTypedData } from '../../../src/rpc_method_wrappers';
+import { testData, mockRpcResponse } from './fixtures/sign_typed_data';
+
+jest.mock('web3-rpc-methods');
+
+describe('signTypedData', () => {
+ let web3Context: Web3Context;
+
+ beforeAll(() => {
+ web3Context = new Web3Context('http://127.0.0.1:8545');
+ });
+
+ it.each(testData)(
+ `should call rpcMethods.signTypedData with expected parameters\nTitle: %s\nInput parameters: %s\n`,
+ async (_, inputParameters) => {
+ await signTypedData(web3Context, ...inputParameters, DEFAULT_RETURN_FORMAT);
+ expect(ethRpcMethods.signTypedData).toHaveBeenCalledWith(
+ web3Context.requestManager,
+ ...inputParameters,
+ );
+ },
+ );
+
+ it.each(testData)(
+ `should format mockRpcResponse using provided return format\nTitle: %s\nInput parameters: %s\n`,
+ async (_, inputParameters) => {
+ const expectedReturnFormat = { number: FMT_NUMBER.STR, bytes: FMT_BYTES.UINT8ARRAY };
+ const expectedFormattedResult = format(
+ { format: 'bytes' },
+ mockRpcResponse,
+ expectedReturnFormat,
+ );
+ (ethRpcMethods.signTypedData as jest.Mock).mockResolvedValueOnce(mockRpcResponse);
+
+ const result = await signTypedData(
+ web3Context,
+ ...inputParameters,
+ expectedReturnFormat,
+ );
+ expect(result).toStrictEqual(expectedFormattedResult);
+ },
+ );
+});
diff --git a/packages/web3-rpc-methods/CHANGELOG.md b/packages/web3-rpc-methods/CHANGELOG.md
index 56772ed7164..e30d8e79193 100644
--- a/packages/web3-rpc-methods/CHANGELOG.md
+++ b/packages/web3-rpc-methods/CHANGELOG.md
@@ -103,3 +103,7 @@ Documentation:
- Rpc method `getPastLogs` accept blockHash as a parameter https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs (#6181)
## [Unreleased]
+
+### Added
+
+- A `signTypedData` method to `eth_rpc_methods` for the rpc calls `eth_signTypedData` and `eth_signTypedData_v4` (#6286)
diff --git a/packages/web3-rpc-methods/src/eth_rpc_methods.ts b/packages/web3-rpc-methods/src/eth_rpc_methods.ts
index 5209302903a..2a027733dfb 100644
--- a/packages/web3-rpc-methods/src/eth_rpc_methods.ts
+++ b/packages/web3-rpc-methods/src/eth_rpc_methods.ts
@@ -28,6 +28,7 @@ import {
Uint256,
Web3EthExecutionAPI,
} from 'web3-types';
+import { Eip712TypedData } from 'web3-types/src/eth_types';
import { validator } from 'web3-validator';
export async function getProtocolVersion(requestManager: Web3RequestManager) {
@@ -575,3 +576,18 @@ export async function createAccessList(
params: [transaction, blockNumber],
});
}
+
+export async function signTypedData(
+ requestManager: Web3RequestManager,
+ address: Address,
+ typedData: Eip712TypedData,
+ useLegacy = false,
+): Promise {
+ // TODO Add validation for typedData
+ validator.validate(['address'], [address]);
+
+ return requestManager.send({
+ method: `eth_signTypedData${useLegacy ? '' : '_v4'}`,
+ params: [address, typedData],
+ });
+}
diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts
new file mode 100644
index 00000000000..37a5ee3171e
--- /dev/null
+++ b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/fixtures/sign_typed_data.ts
@@ -0,0 +1,98 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { Address, Eip712TypedData } from 'web3-types';
+
+const address = '0x407d73d8a49eeb85d32cf465507dd71d507100c1';
+
+const typedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'chainId',
+ type: 'uint256',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'wallet',
+ type: 'address',
+ },
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person',
+ },
+ {
+ name: 'to',
+ type: 'Person',
+ },
+ {
+ name: 'contents',
+ type: 'string',
+ },
+ ],
+ },
+ primaryType: 'Mail',
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 1,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ },
+ message: {
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ },
+ contents: 'Hello, Bob!',
+ },
+};
+
+/**
+ * Array consists of:
+ * - Test title
+ * - Input parameters:
+ * - address
+ * - message
+ */
+type TestData = [string, [Address, Eip712TypedData, boolean | undefined]];
+export const testData: TestData[] = [
+ ['useLegacy = undefined', [address, typedData, undefined]],
+ ['useLegacy = false', [address, typedData, false]],
+ ['useLegacy = true', [address, typedData, true]],
+];
diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts
new file mode 100644
index 00000000000..a9674a8d381
--- /dev/null
+++ b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/sign_typed_data.test.ts
@@ -0,0 +1,54 @@
+/*
+This file is part of web3.js.
+
+web3.js is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+web3.js is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with web3.js. If not, see .
+*/
+import { Web3RequestManager } from 'web3-core';
+import { validator } from 'web3-validator';
+
+import { ethRpcMethods } from '../../../src/index';
+import { testData } from './fixtures/sign_typed_data';
+
+jest.mock('web3-validator');
+
+describe('signTypedData', () => {
+ let requestManagerSendSpy: jest.Mock;
+ let requestManager: Web3RequestManager;
+
+ beforeAll(() => {
+ requestManager = new Web3RequestManager('http://127.0.0.1:8545');
+ requestManagerSendSpy = jest.fn();
+ requestManager.send = requestManagerSendSpy;
+ });
+
+ it.each(testData)(
+ 'should call requestManager.send with signTypedData method and expect parameters\n Title: %s\n Input parameters: %s',
+ async (_, inputParameters) => {
+ await ethRpcMethods.signTypedData(requestManager, ...inputParameters);
+ expect(requestManagerSendSpy).toHaveBeenCalledWith({
+ method: `eth_signTypedData${inputParameters[2] ? '' : '_v4'}`,
+ params: [inputParameters[0], inputParameters[1]],
+ });
+ },
+ );
+
+ it.each(testData)(
+ 'should call validator.validate with expected params\n Title: %s\n Input parameters: %s',
+ async (_, inputParameters) => {
+ const validatorSpy = jest.spyOn(validator, 'validate');
+ await ethRpcMethods.signTypedData(requestManager, ...inputParameters);
+ expect(validatorSpy).toHaveBeenCalledWith(['address'], [inputParameters[0]]);
+ },
+ );
+});
diff --git a/packages/web3-types/CHANGELOG.md b/packages/web3-types/CHANGELOG.md
index 3607abfae24..da55982895e 100644
--- a/packages/web3-types/CHANGELOG.md
+++ b/packages/web3-types/CHANGELOG.md
@@ -141,3 +141,8 @@ Documentation:
- type `Filter` includes `blockHash` (#6206)
## [Unreleased]
+
+### Added
+
+- `eth_signTypedData` and `eth_signTypedData_v4` to `web3_eth_execution_api` (#6286)
+- `Eip712TypeDetails` and `Eip712TypedData` to `eth_types` (#6286)
diff --git a/packages/web3-types/src/apis/web3_eth_execution_api.ts b/packages/web3-types/src/apis/web3_eth_execution_api.ts
index 161e73f8d39..fde1250b4a3 100644
--- a/packages/web3-types/src/apis/web3_eth_execution_api.ts
+++ b/packages/web3-types/src/apis/web3_eth_execution_api.ts
@@ -19,6 +19,8 @@ import {
AccountObject,
Address,
BlockNumberOrTag,
+ Eip712TypedData,
+ HexString256Bytes,
HexString32Bytes,
TransactionInfo,
Uint,
@@ -41,4 +43,18 @@ export type Web3EthExecutionAPI = EthExecutionAPI & {
storageKeys: HexString32Bytes[],
blockNumber: BlockNumberOrTag,
) => AccountObject;
+
+ // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
+ eth_signTypedData: (
+ address: Address,
+ typedData: Eip712TypedData,
+ useLegacy: true,
+ ) => HexString256Bytes;
+
+ // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
+ eth_signTypedData_v4: (
+ address: Address,
+ typedData: Eip712TypedData,
+ useLegacy: false | undefined,
+ ) => HexString256Bytes;
};
diff --git a/packages/web3-types/src/eth_types.ts b/packages/web3-types/src/eth_types.ts
index f8452e5471f..2865428b092 100644
--- a/packages/web3-types/src/eth_types.ts
+++ b/packages/web3-types/src/eth_types.ts
@@ -144,18 +144,42 @@ export interface BlockOutput {
readonly parentHash?: HexString32Bytes;
}
+export interface Withdrawals {
+ readonly index: Numbers;
+ readonly validatorIndex: Numbers;
+ readonly address: Address;
+ readonly amount: Numbers;
+}
+
export interface BlockHeaderOutput {
+ readonly hash?: HexString32Bytes;
+ readonly parentHash?: HexString32Bytes;
+ readonly receiptsRoot?: HexString32Bytes;
+ readonly miner?: HexString;
+ readonly stateRoot?: HexString32Bytes;
+ readonly transactionsRoot?: HexString32Bytes;
+ readonly withdrawalsRoot?: HexString32Bytes;
+ readonly logsBloom?: Bytes;
+ readonly difficulty?: Numbers;
+ readonly number?: Numbers;
readonly gasLimit: Numbers;
readonly gasUsed: Numbers;
readonly timestamp: Numbers;
- readonly number?: Numbers;
- readonly difficulty?: Numbers;
+ readonly extraData?: Bytes;
+ readonly nonce?: Numbers;
+ readonly sha3Uncles: HexString32Bytes[];
+ readonly baseFeePerGas?: Numbers;
+
+ // These fields are returned when the RPC client is Nethermind,
+ // but aren't available in other clients such as Geth
+ readonly author?: Address;
readonly totalDifficulty?: Numbers;
+ readonly size?: Numbers;
+ readonly excessDataGas?: Numbers;
+ readonly mixHash?: HexString32Bytes;
readonly transactions?: TransactionOutput[];
- readonly miner?: HexString;
- readonly baseFeePerGas?: Numbers;
- readonly parentHash?: HexString32Bytes;
- readonly sha3Uncles: HexString32Bytes[];
+ readonly uncles?: Uncles;
+ readonly withdrawals?: Withdrawals[];
}
export interface ReceiptInput {
@@ -472,3 +496,17 @@ export interface AccountObject {
readonly accountProof: Bytes[];
readonly storageProof: StorageProof[];
}
+
+export interface Eip712TypeDetails {
+ name: string;
+ type: string;
+}
+export interface Eip712TypedData {
+ readonly types: {
+ EIP712Domain: Eip712TypeDetails[];
+ [key: string]: Eip712TypeDetails[];
+ };
+ readonly primaryType: string;
+ readonly domain: Record;
+ readonly message: Record;
+}
diff --git a/packages/web3/test/unit/web3-custom-subscriptions.test.ts b/packages/web3/test/unit/web3-custom-subscriptions.test.ts
index 2a7652d740c..9da93f35504 100644
--- a/packages/web3/test/unit/web3-custom-subscriptions.test.ts
+++ b/packages/web3/test/unit/web3-custom-subscriptions.test.ts
@@ -15,27 +15,39 @@ You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see .
*/
-import { Web3Subscription } from 'web3-core';
+import { Web3Subscription, Web3SubscriptionManager } from 'web3-core';
import { Web3 } from '../../src/web3';
+class CustomSubscription extends Web3Subscription<
+ {
+ data: string;
+ },
+ {
+ readonly customArgs?: string;
+ }
+> {
+ protected _buildSubscriptionParams() {
+ return ['someCustomSubscription', this.args];
+ }
+
+ public get subscriptionManager() {
+ return super.subscriptionManager;
+ }
+}
+
+const CustomSub = {
+ custom: CustomSubscription,
+};
+
describe('Web3 Custom Subscriptions', () => {
- it('should be able to define and subscribe to custom subscription', async () => {
- class CustomSubscription extends Web3Subscription<
- {
- data: string;
- },
- {
- readonly customArgs?: string;
- }
- > {
- protected _buildSubscriptionParams() {
- return ['customArgs', this.args];
- }
- }
+ let web3: Web3<{ custom: typeof CustomSubscription }>;
+ beforeAll(() => {
+ web3 = new Web3({
+ registeredSubscriptions: CustomSub,
+ });
+ });
- const CustomSub = {
- custom: CustomSubscription,
- };
+ it('should be able to define and subscribe to custom subscription', async () => {
const args = {
customArgs: 'hello custom',
};
@@ -49,7 +61,7 @@ describe('Web3 Custom Subscriptions', () => {
),
jsonrpc: '2.0',
method: 'eth_subscribe',
- params: ['customArgs', args],
+ params: ['someCustomSubscription', args],
});
resolve(true);
}),
@@ -59,17 +71,28 @@ describe('Web3 Custom Subscriptions', () => {
};
try {
- const web3NoProvider = new Web3({
- provider,
- registeredSubscriptions: CustomSub,
- });
+ web3.provider = provider;
// eslint-disable-next-line no-void
- void web3NoProvider.subscriptionManager.subscribe('custom', args);
+ void web3.subscriptionManager.subscribe('custom', args);
} catch (error) {
reject(error);
}
});
await expect(exec).resolves.toBe(true);
});
+
+ it('should access subscriptionManager from derived class', async () => {
+ const sub = new CustomSubscription(
+ { customArgs: undefined },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ subscriptionManager: web3.subscriptionManager as Web3SubscriptionManager<
+ unknown,
+ any
+ >,
+ },
+ );
+ expect(web3.subscriptionManager).toBe(sub.subscriptionManager);
+ });
});