From ed85ccecadc97cbacd0ee00dfb1a2ee7497347b8 Mon Sep 17 00:00:00 2001
From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com>
Date: Mon, 21 Oct 2024 18:13:17 +0200
Subject: [PATCH] fix: ensure contractInstance.subscriptionManager.subscribe is
not throwing (#7327)
* rename `LogsSubscription` at `web3-eth-contract` to `ContractLogsSubscription`
* fix issue that `contractInstance.subscriptionManager.subscribe` was throwing
* add a unit test
---
.../src/contract-subscription-manager.ts | 72 +++++++++++++++++++
packages/web3-eth-contract/src/contract.ts | 27 +++++--
...iption.ts => contract_log_subscription.ts} | 10 ++-
packages/web3-eth-contract/src/index.ts | 6 +-
packages/web3-eth-contract/src/types.ts | 4 +-
.../web3-eth-contract/test/fixtures/erc20.ts | 8 +--
.../web3-eth-contract/test/fixtures/erc721.ts | 10 +--
.../test/unit/log_subscription.test.ts | 38 +++++++++-
8 files changed, 153 insertions(+), 22 deletions(-)
create mode 100644 packages/web3-eth-contract/src/contract-subscription-manager.ts
rename packages/web3-eth-contract/src/{log_subscription.ts => contract_log_subscription.ts} (92%)
diff --git a/packages/web3-eth-contract/src/contract-subscription-manager.ts b/packages/web3-eth-contract/src/contract-subscription-manager.ts
new file mode 100644
index 00000000000..3065e8aa7f4
--- /dev/null
+++ b/packages/web3-eth-contract/src/contract-subscription-manager.ts
@@ -0,0 +1,72 @@
+/*
+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 { Web3SubscriptionConstructor, Web3SubscriptionManager } from 'web3-core';
+import { EthExecutionAPI, ContractAbi, DataFormat, DEFAULT_RETURN_FORMAT } from 'web3-types';
+// eslint-disable-next-line import/no-cycle
+import { Contract } from './contract.js';
+
+/**
+ * Similar to `Web3SubscriptionManager` but has a reference to the Contract that uses
+ */
+export class ContractSubscriptionManager<
+ API extends EthExecutionAPI,
+ RegisteredSubs extends {
+ [key: string]: Web3SubscriptionConstructor;
+ } = any, // = ContractSubscriptions
+> extends Web3SubscriptionManager {
+ public readonly parentContract: Contract;
+
+ /**
+ *
+ * @param - Web3SubscriptionManager
+ * @param - parentContract
+ *
+ * @example
+ * ```ts
+ * const requestManager = new Web3RequestManager("ws://localhost:8545");
+ * const contract = new Contract(...)
+ * const subscriptionManager = new Web3SubscriptionManager(requestManager, {}, contract);
+ * ```
+ */
+ public constructor(
+ self: Web3SubscriptionManager,
+ parentContract: Contract,
+ ) {
+ super(self.requestManager, self.registeredSubscriptions);
+
+ this.parentContract = parentContract;
+ }
+
+ /**
+ * Will create a new subscription
+ *
+ * @param name - The subscription you want to subscribe to
+ * @param args - Optional additional parameters, depending on the subscription type
+ * @param returnFormat- ({@link DataFormat} defaults to {@link DEFAULT_RETURN_FORMAT}) - Specifies how the return data from the call should be formatted.
+ *
+ * Will subscribe to a specific topic (note: name)
+ * @returns The subscription object
+ */
+ public async subscribe(
+ name: T,
+ args?: ConstructorParameters[0],
+ returnFormat: DataFormat = DEFAULT_RETURN_FORMAT,
+ ): Promise> {
+ return super.subscribe(name, args ?? this.parentContract.options, returnFormat);
+ }
+}
diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts
index 5566d0840bf..a99dc27628e 100644
--- a/packages/web3-eth-contract/src/contract.ts
+++ b/packages/web3-eth-contract/src/contract.ts
@@ -105,7 +105,7 @@ import {
encodeEventABI,
encodeMethodABI,
} from './encoding.js';
-import { LogsSubscription } from './log_subscription.js';
+import { ContractLogsSubscription } from './contract_log_subscription.js';
import {
ContractEventOptions,
NonPayableMethodObject,
@@ -123,6 +123,8 @@ import {
} from './utils.js';
// eslint-disable-next-line import/no-cycle
import { DeployerMethodClass } from './contract-deployer-method-class.js';
+// eslint-disable-next-line import/no-cycle
+import { ContractSubscriptionManager } from './contract-subscription-manager.js';
type ContractBoundMethod<
Abi extends AbiFunctionFragment,
@@ -179,9 +181,9 @@ export type ContractMethodSend = Web3PromiEvent<
* ```
*
* @param options - The options used to subscribe for the event
- * @returns - A Promise resolved with {@link LogsSubscription} object
+ * @returns - A Promise resolved with {@link ContractLogsSubscription} object
*/
-export type ContractBoundEvent = (options?: ContractEventOptions) => LogsSubscription;
+export type ContractBoundEvent = (options?: ContractEventOptions) => ContractLogsSubscription;
// To avoid circular dependency between types and encoding, declared these types here.
export type ContractEventsInterface<
@@ -204,11 +206,13 @@ export type ContractEventEmitterInterface = {
type EventParameters = Parameters[2];
const contractSubscriptions = {
- logs: LogsSubscription,
+ logs: ContractLogsSubscription,
newHeads: NewHeadsSubscription,
newBlockHeaders: NewHeadsSubscription,
};
+type ContractSubscriptions = typeof contractSubscriptions;
+
/**
* The `web3.eth.Contract` makes it easy to interact with smart contracts on the ethereum blockchain.
* For using contract package, first install Web3 package using: `npm i web3` or `yarn add web3` based on your package manager, after that contracts features can be used as mentioned in following snippet.
@@ -406,9 +410,15 @@ const contractSubscriptions = {
*
*/
export class Contract
- extends Web3Context
+ extends Web3Context
implements Web3EventEmitter>
{
+ protected override _subscriptionManager: ContractSubscriptionManager;
+
+ public override get subscriptionManager(): ContractSubscriptionManager {
+ return this._subscriptionManager;
+ }
+
/**
* The options `object` for the contract instance. `from`, `gas` and `gasPrice` are used as fallback values when sending transactions.
*
@@ -564,6 +574,11 @@ export class Contract
registeredSubscriptions: contractSubscriptions,
});
+ this._subscriptionManager = new ContractSubscriptionManager<
+ EthExecutionAPI,
+ ContractSubscriptions
+ >(super.subscriptionManager, this);
+
// Init protected properties
if ((contractContext as Web3Context)?.wallet) {
this._wallet = (contractContext as Web3Context).wallet;
@@ -1414,7 +1429,7 @@ export class Contract
abi,
params[0] as EventParameters,
);
- const sub = new LogsSubscription(
+ const sub = new ContractLogsSubscription(
{
address: this.options.address,
topics,
diff --git a/packages/web3-eth-contract/src/log_subscription.ts b/packages/web3-eth-contract/src/contract_log_subscription.ts
similarity index 92%
rename from packages/web3-eth-contract/src/log_subscription.ts
rename to packages/web3-eth-contract/src/contract_log_subscription.ts
index 3cb1a527941..8010309c7cb 100644
--- a/packages/web3-eth-contract/src/log_subscription.ts
+++ b/packages/web3-eth-contract/src/contract_log_subscription.ts
@@ -28,9 +28,9 @@ import { Web3RequestManager, Web3Subscription, Web3SubscriptionManager } from 'w
import { decodeEventABI } from 'web3-eth';
/**
- * LogSubscription to be used to subscribe to events logs.
+ * ContractLogsSubscription to be used to subscribe to events logs.
*
- * Following events are supported and can be accessed with either {@link LogsSubscription.once} or ${@link LogsSubscription.on} methods.
+ * Following events are supported and can be accessed with either {@link ContractLogsSubscription.once} or ${@link ContractLogsSubscription.on} methods.
*
* - **connected**: Emitted when the subscription is connected.
* - **data**: Fires on each incoming event with the event object as argument.
@@ -81,7 +81,7 @@ import { decodeEventABI } from 'web3-eth';
* }
* ```
*/
-export class LogsSubscription extends Web3Subscription<
+export class ContractLogsSubscription extends Web3Subscription<
{
data: EventLog;
changed: EventLog & { removed: true };
@@ -162,3 +162,7 @@ export class LogsSubscription extends Web3Subscription<
return decodeEventABI(this.abi, data as LogsInput, this.jsonInterface, super.returnFormat);
}
}
+/**
+ * @deprecated LogsSubscription is renamed to ContractLogsSubscription in this package to not be confused with LogsSubscription at 'web3-eth'.
+ */
+export const LogsSubscription = ContractLogsSubscription;
diff --git a/packages/web3-eth-contract/src/index.ts b/packages/web3-eth-contract/src/index.ts
index ad7b5ab7fb7..93e90f03b96 100644
--- a/packages/web3-eth-contract/src/index.ts
+++ b/packages/web3-eth-contract/src/index.ts
@@ -42,11 +42,15 @@ along with web3.js. If not, see .
*/
import { Contract } from './contract.js';
+import { ContractLogsSubscription } from './contract_log_subscription.js';
+/** @deprecated Use `ContractLogsSubscription` instead. */
+export type LogsSubscription = ContractLogsSubscription;
+
export * from './encoding.js';
export * from './contract.js';
export * from './contract-deployer-method-class.js';
-export * from './log_subscription.js';
+export * from './contract_log_subscription.js';
export * from './types.js';
export * from './utils.js';
diff --git a/packages/web3-eth-contract/src/types.ts b/packages/web3-eth-contract/src/types.ts
index 8f4bf564ca4..6ecff146844 100644
--- a/packages/web3-eth-contract/src/types.ts
+++ b/packages/web3-eth-contract/src/types.ts
@@ -32,7 +32,7 @@ import {
} from 'web3-types';
import { NewHeadsSubscription, SendTransactionEvents } from 'web3-eth';
import type { ContractOptions } from 'web3-types';
-import { LogsSubscription } from './log_subscription.js';
+import { ContractLogsSubscription } from './contract_log_subscription.js';
export type NonPayableTxOptions = NonPayableCallOptions;
export type PayableTxOptions = PayableCallOptions;
@@ -462,7 +462,7 @@ export type Web3ContractContext = Partial<
Web3ContextInitOptions<
EthExecutionAPI,
{
- logs: typeof LogsSubscription;
+ logs: typeof ContractLogsSubscription;
newHeads: typeof NewHeadsSubscription;
newBlockHeaders: typeof NewHeadsSubscription;
}
diff --git a/packages/web3-eth-contract/test/fixtures/erc20.ts b/packages/web3-eth-contract/test/fixtures/erc20.ts
index cf4b595ec8d..18b51748a4e 100644
--- a/packages/web3-eth-contract/test/fixtures/erc20.ts
+++ b/packages/web3-eth-contract/test/fixtures/erc20.ts
@@ -16,7 +16,7 @@ along with web3.js. If not, see .
*/
import { Address, Numbers } from 'web3-types';
-import { LogsSubscription } from '../../src/log_subscription';
+import { ContractLogsSubscription } from '../../src/contract_log_subscription';
import { ContractEventOptions, PayableMethodObject, NonPayableMethodObject } from '../../src/types';
export interface Erc20Interface {
@@ -53,9 +53,9 @@ export interface Erc20Interface {
};
events: {
- [key: string]: (options?: ContractEventOptions) => LogsSubscription;
- Approval: (options?: ContractEventOptions) => LogsSubscription;
- Transfer: (options?: ContractEventOptions) => LogsSubscription;
+ [key: string]: (options?: ContractEventOptions) => ContractLogsSubscription;
+ Approval: (options?: ContractEventOptions) => ContractLogsSubscription;
+ Transfer: (options?: ContractEventOptions) => ContractLogsSubscription;
};
}
diff --git a/packages/web3-eth-contract/test/fixtures/erc721.ts b/packages/web3-eth-contract/test/fixtures/erc721.ts
index 36c87289c24..0dab8017ea4 100644
--- a/packages/web3-eth-contract/test/fixtures/erc721.ts
+++ b/packages/web3-eth-contract/test/fixtures/erc721.ts
@@ -16,7 +16,7 @@ along with web3.js. If not, see .
*/
import { Address, Numbers } from 'web3-types';
-import { LogsSubscription } from '../../src/log_subscription';
+import { ContractLogsSubscription } from '../../src/contract_log_subscription';
import { ContractEventOptions, NonPayableMethodObject, PayableMethodObject } from '../../src/types';
export interface Erc721Interface {
@@ -55,10 +55,10 @@ export interface Erc721Interface {
};
events: {
- [key: string]: (options?: ContractEventOptions) => LogsSubscription;
- Transfer: (options?: ContractEventOptions) => LogsSubscription;
- Approval: (options?: ContractEventOptions) => LogsSubscription;
- ApprovalForAll: (options?: ContractEventOptions) => LogsSubscription;
+ [key: string]: (options?: ContractEventOptions) => ContractLogsSubscription;
+ Transfer: (options?: ContractEventOptions) => ContractLogsSubscription;
+ Approval: (options?: ContractEventOptions) => ContractLogsSubscription;
+ ApprovalForAll: (options?: ContractEventOptions) => ContractLogsSubscription;
};
}
diff --git a/packages/web3-eth-contract/test/unit/log_subscription.test.ts b/packages/web3-eth-contract/test/unit/log_subscription.test.ts
index ccc72acaac6..520157a3cab 100644
--- a/packages/web3-eth-contract/test/unit/log_subscription.test.ts
+++ b/packages/web3-eth-contract/test/unit/log_subscription.test.ts
@@ -17,7 +17,8 @@ along with web3.js. If not, see .
import * as eth from 'web3-eth';
import { WebSocketProvider } from 'web3-providers-ws';
-import { Contract } from '../../src';
+import { Web3SubscriptionManager } from 'web3-core';
+import { Contract, ContractLogsSubscription } from '../../src';
import { GreeterAbi, GreeterBytecode } from '../shared_fixtures/build/Greeter';
jest.mock('web3-eth');
@@ -76,4 +77,39 @@ describe('contract log subscription', () => {
],
});
});
+
+ it('should be able to subscribe to logs with contractInstance.subscriptionManager.subscribe', async () => {
+ const address = '0x407D73d8a49eeb85D32Cf465507dd71d507100c1';
+ const contractInstance = new Contract(GreeterAbi, address);
+
+ jest.spyOn(WebSocketProvider.prototype, 'request').mockImplementation(
+ async (payload: any) => {
+ return {
+ jsonrpc: '2.0',
+ id: payload.id,
+ result: {},
+ };
+ },
+ );
+
+ jest.spyOn(Web3SubscriptionManager.prototype, 'subscribe').mockImplementation(
+ async (name: string | number | symbol, args?: any) => {
+ expect(name).toBe('logs');
+ expect(args.address).toBe(address);
+
+ return new ContractLogsSubscription(args, {
+ subscriptionManager: contractInstance.subscriptionManager,
+ });
+ },
+ );
+
+ contract.setProvider(providerString);
+
+ const sub = contractInstance.subscriptionManager.subscribe('logs');
+ expect(await sub).toBeInstanceOf(ContractLogsSubscription);
+
+ contractInstance.subscriptionManager.clear();
+
+ contractInstance.provider?.disconnect();
+ });
});