diff --git a/packages/web3-eth-accounts/CHANGELOG.md b/packages/web3-eth-accounts/CHANGELOG.md index b28e2c9b047..b682b5e34bb 100644 --- a/packages/web3-eth-accounts/CHANGELOG.md +++ b/packages/web3-eth-accounts/CHANGELOG.md @@ -172,3 +172,6 @@ Documentation: ### Added - Added public function `signMessageWithPrivateKey` (#7174) + +### Fixed +- Fix `TransactionFactory.registerTransactionType` not working, if there is a version mistatch between `web3-eth` and `web3-eth-accounts` by saving `extraTxTypes` at `globals`. (#7197) diff --git a/packages/web3-eth-accounts/src/tx/transactionFactory.ts b/packages/web3-eth-accounts/src/tx/transactionFactory.ts index a677543c64f..98a465c0973 100644 --- a/packages/web3-eth-accounts/src/tx/transactionFactory.ts +++ b/packages/web3-eth-accounts/src/tx/transactionFactory.ts @@ -31,7 +31,15 @@ import type { } from './types.js'; import { BaseTransaction } from './baseTransaction.js'; -const extraTxTypes: Map> = new Map(); +let extraTxTypes: Map>; +// use the global object, to work fine even if web3-eth and web3-eth-accounts was on a different versions: +const typedGlobal = global as unknown as {extraTxTypes: Map>} +if (!typedGlobal.extraTxTypes) { + extraTxTypes = new Map(); + typedGlobal.extraTxTypes = extraTxTypes; +} else { + extraTxTypes = typedGlobal.extraTxTypes; +} // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class TransactionFactory { diff --git a/packages/web3-eth-contract/CHANGELOG.md b/packages/web3-eth-contract/CHANGELOG.md index bab02912320..01e6f2f8a68 100644 --- a/packages/web3-eth-contract/CHANGELOG.md +++ b/packages/web3-eth-contract/CHANGELOG.md @@ -395,3 +395,11 @@ Documentation: ## [Unreleased] +### Added + +- Added `populateTransaction` to the `contract.deploy(...)` properties. (#7197) + +### Changed + +- The returnred properties of `contract.deploy(...)` are structured with a newly created class named `DeployerMethodClass`. (#7197) +- Add a missed accepted type for the `abi` parameter, at `dataInputEncodeMethodHelper` and `getSendTxParams`. (#7197) diff --git a/packages/web3-eth-contract/src/contract-deployer-method-class.ts b/packages/web3-eth-contract/src/contract-deployer-method-class.ts new file mode 100644 index 00000000000..d57f7ae05b2 --- /dev/null +++ b/packages/web3-eth-contract/src/contract-deployer-method-class.ts @@ -0,0 +1,240 @@ +/* +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 { + Web3ContractError, +} from 'web3-errors'; +import { + sendTransaction, + SendTransactionEvents, + SendTransactionOptions, +} from 'web3-eth'; +import { + AbiConstructorFragment, + AbiFunctionFragment, + ContractAbi, + ContractConstructorArgs, + Bytes, + HexString, + PayableCallOptions, + DataFormat, + DEFAULT_RETURN_FORMAT, + ContractOptions, + TransactionReceipt, + TransactionCall, +} from 'web3-types'; +import { + format, +} from 'web3-utils'; +import { + isNullish, +} from 'web3-validator'; +import { Web3PromiEvent } from 'web3-core'; +import { + decodeMethodParams, + encodeMethodABI, +} from './encoding.js'; +import { + NonPayableTxOptions, + PayableTxOptions, +} from './types.js'; +import { + getSendTxParams, +} from './utils.js'; +// eslint-disable-next-line import/no-cycle +import { Contract } from './contract.js'; + +export type ContractDeploySend = Web3PromiEvent< + // eslint-disable-next-line no-use-before-define + Contract, + SendTransactionEvents +>; + +/* + * This class is only supposed to be used for the return of `new Contract(...).deploy(...)` method. + */ +export class DeployerMethodClass { + + protected readonly args: never[] | ContractConstructorArgs; + protected readonly constructorAbi: AbiConstructorFragment; + protected readonly contractOptions: ContractOptions; + protected readonly deployData?: string; + + protected _contractMethodDeploySend( + tx: TransactionCall, + ) { + // eslint-disable-next-line no-use-before-define + const returnTxOptions: SendTransactionOptions> = { + transactionResolver: (receipt: TransactionReceipt) => { + if (receipt.status === BigInt(0)) { + throw new Web3ContractError("code couldn't be stored", receipt); + } + + const newContract = this.parent.clone(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + newContract.options.address = receipt.contractAddress; + return newContract; + }, + + contractAbi: this.parent.options.jsonInterface, + // TODO Should make this configurable by the user + checkRevertBeforeSending: false, + }; + + return isNullish(this.parent.getTransactionMiddleware()) + ? sendTransaction(this.parent, tx, this.parent.defaultReturnFormat, returnTxOptions) // not calling this with undefined Middleware because it will not break if Eth package is not updated + : sendTransaction( + this.parent, + tx, + this.parent.defaultReturnFormat, + returnTxOptions, + this.parent.getTransactionMiddleware(), + ); + } + + public constructor( + // eslint-disable-next-line no-use-before-define + public parent: Contract, + public deployOptions: + | { + /** + * The byte code of the contract. + */ + data?: HexString; + input?: HexString; + /** + * The arguments which get passed to the constructor on deployment. + */ + arguments?: ContractConstructorArgs; + } + | undefined, + ) { + + const { args, abi, contractOptions, deployData} = this.calculateDeployParams(); + + this.args = args; + this.constructorAbi = abi; + this.contractOptions = contractOptions; + this.deployData = deployData; + } + + public send(options?: PayableTxOptions): ContractDeploySend { + const modifiedOptions = { ...options }; + + const tx = this.populateTransaction(modifiedOptions); + + return this._contractMethodDeploySend(tx); + } + + public populateTransaction( + txOptions?: PayableTxOptions | NonPayableTxOptions, + ) { + const modifiedContractOptions = { + ...this.contractOptions, + from: this.contractOptions.from ?? this.parent.defaultAccount ?? undefined, + }; + + // args, abi, contractOptions, deployData + + const tx = getSendTxParams({ + abi: this.constructorAbi, + params: this.args as unknown[], + options: { ...txOptions, dataInputFill: this.parent.contractDataInputFill }, + contractOptions: modifiedContractOptions, + }); + + // @ts-expect-error remove unnecessary field + if (tx.dataInputFill) { + // @ts-expect-error remove unnecessary field + delete tx.dataInputFill; + } + return tx; + } + + protected calculateDeployParams() { + let abi = this.parent.options.jsonInterface.find( + j => j.type === 'constructor', + ) as AbiConstructorFragment; + if (!abi) { + abi = { + type: 'constructor', + stateMutability: '', + } as AbiConstructorFragment; + } + + const _input = format( + { format: 'bytes' }, + this.deployOptions?.input ?? this.parent.options.input, + DEFAULT_RETURN_FORMAT, + ); + + const _data = format( + { format: 'bytes' }, + this.deployOptions?.data ?? this.parent.options.data, + DEFAULT_RETURN_FORMAT, + ); + + if ((!_input || _input.trim() === '0x') && (!_data || _data.trim() === '0x')) { + throw new Web3ContractError('contract creation without any data provided.'); + } + + const args = this.deployOptions?.arguments ?? []; + + const contractOptions: ContractOptions = { + ...this.parent.options, + input: _input, + data: _data, + }; + const deployData = _input ?? _data; + + return { args, abi, contractOptions, deployData} + } + + public async estimateGas( + options?: PayableCallOptions, + returnFormat: ReturnFormat = this.parent.defaultReturnFormat as ReturnFormat, + ) { + const modifiedOptions = { ...options }; + return this.parent.contractMethodEstimateGas({ + abi: this.constructorAbi as AbiFunctionFragment, + params: this.args as unknown[], + returnFormat, + options: modifiedOptions, + contractOptions: this.contractOptions, + }); + } + + public encodeABI() { + return encodeMethodABI( + this.constructorAbi, + this.args as unknown[], + format( + { format: 'bytes' }, + this.deployData as Bytes, + this.parent.defaultReturnFormat as typeof DEFAULT_RETURN_FORMAT, + ), + ); + } + + public decodeData(data: HexString) { + return { + ...decodeMethodParams(this.constructorAbi, data.replace(this.deployData as string, ''), false), + __method__: this.constructorAbi.type, + }; + } +}; diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index cba5043df95..906cd3c8794 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -40,7 +40,6 @@ import { ALL_EVENTS_ABI, SendTransactionEvents, TransactionMiddleware, - SendTransactionOptions, } from 'web3-eth'; import { encodeEventSignature, @@ -52,7 +51,6 @@ import { jsonInterfaceMethodToString, } from 'web3-eth-abi'; import { - AbiConstructorFragment, AbiErrorFragment, AbiEventFragment, AbiFragment, @@ -67,7 +65,6 @@ import { Address, BlockNumberOrTag, BlockTags, - Bytes, EthExecutionAPI, Filter, FilterAbis, @@ -124,6 +121,8 @@ import { getSendTxParams, isWeb3ContractContext, } from './utils.js'; +// eslint-disable-next-line import/no-cycle +import { DeployerMethodClass } from './contract-deployer-method-class.js'; type ContractBoundMethod< Abi extends AbiFunctionFragment, @@ -168,11 +167,6 @@ export type ContractMethodSend = Web3PromiEvent< FormatType, SendTransactionEvents >; -export type ContractDeploySend = Web3PromiEvent< - // eslint-disable-next-line no-use-before-define - Contract, - SendTransactionEvents ->; /** * @hidden @@ -215,6 +209,8 @@ const contractSubscriptions = { newBlockHeaders: NewHeadsSubscription, }; + + /** * 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. @@ -515,11 +511,7 @@ export class Contract ); public constructor( jsonInterface: Abi, - addressOrOptionsOrContext?: - | Address - | ContractInitOptions - | Web3ContractContext - | Web3Context, + addressOrOptionsOrContext?: Address | ContractInitOptions | Web3ContractContext | Web3Context, optionsOrContextOrReturnFormat?: | ContractInitOptions | Web3ContractContext @@ -545,20 +537,14 @@ export class Contract } let provider; - if ( - typeof addressOrOptionsOrContext === 'object' && - 'provider' in addressOrOptionsOrContext - ) { + if (typeof addressOrOptionsOrContext === 'object' && 'provider' in addressOrOptionsOrContext) { provider = addressOrOptionsOrContext.provider; } else if ( typeof optionsOrContextOrReturnFormat === 'object' && 'provider' in optionsOrContextOrReturnFormat ) { provider = optionsOrContextOrReturnFormat.provider; - } else if ( - typeof contextOrReturnFormat === 'object' && - 'provider' in contextOrReturnFormat - ) { + } else if (typeof contextOrReturnFormat === 'object' && 'provider' in contextOrReturnFormat) { provider = contextOrReturnFormat.provider; } else { provider = Contract.givenProvider; @@ -852,80 +838,8 @@ export class Contract * The arguments which get passed to the constructor on deployment. */ arguments?: ContractConstructorArgs; - }) { - let abi = this._jsonInterface.find(j => j.type === 'constructor') as AbiConstructorFragment; - if (!abi) { - abi = { - type: 'constructor', - stateMutability: '', - } as AbiConstructorFragment; - } - - const _input = format( - { format: 'bytes' }, - deployOptions?.input ?? this.options.input, - DEFAULT_RETURN_FORMAT, - ); - - const _data = format( - { format: 'bytes' }, - deployOptions?.data ?? this.options.data, - DEFAULT_RETURN_FORMAT, - ); - - if ((!_input || _input.trim() === '0x') && (!_data || _data.trim() === '0x')) { - throw new Web3ContractError('contract creation without any data provided.'); - } - - const args = deployOptions?.arguments ?? []; - - const contractOptions: ContractOptions = { ...this.options, input: _input, data: _data }; - const deployData = _input ?? _data; - return { - arguments: args, - send: (options?: PayableTxOptions): ContractDeploySend => { - const modifiedOptions = { ...options }; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this._contractMethodDeploySend( - abi as AbiFunctionFragment, - args as unknown[], - modifiedOptions, - contractOptions, - ); - }, - estimateGas: async ( - options?: PayableCallOptions, - returnFormat: ReturnFormat = this.defaultReturnFormat as ReturnFormat, - ) => { - const modifiedOptions = { ...options }; - return this._contractMethodEstimateGas({ - abi: abi as AbiFunctionFragment, - params: args as unknown[], - returnFormat, - options: modifiedOptions, - contractOptions, - }); - }, - encodeABI: () => - encodeMethodABI( - abi as AbiFunctionFragment, - args as unknown[], - format( - { format: 'bytes' }, - deployData as Bytes, - this.defaultReturnFormat as typeof DEFAULT_RETURN_FORMAT, - ), - ), - decodeData: (data: HexString) => ({ - ...decodeMethodParams( - abi as AbiFunctionFragment, - data.replace(deployData as string, ''), - false, - ), - __method__: abi.type, // abi.type is constructor - }), - }; + }): DeployerMethodClass { + return new DeployerMethodClass(this, deployOptions); } /** @@ -1046,8 +960,7 @@ export class Contract if (Array.isArray(filter[key])) { return (filter[key] as Numbers[]).some( (v: Numbers) => - String(log.returnValues[key]).toUpperCase() === - String(v).toUpperCase(), + String(log.returnValues[key]).toUpperCase() === String(v).toUpperCase(), ); } @@ -1057,10 +970,7 @@ export class Contract if (hashedIndexedString === String(log.returnValues[key])) return true; } - return ( - String(log.returnValues[key]).toUpperCase() === - String(filter[key]).toUpperCase() - ); + return String(log.returnValues[key]).toUpperCase() === String(filter[key]).toUpperCase(); }); }); } @@ -1103,9 +1013,7 @@ export class Contract let result: ContractAbi = []; const functionsAbi = abis.filter(abi => abi.type !== 'error'); - const errorsAbi = abis.filter(abi => - isAbiErrorFragment(abi), - ) as unknown as AbiErrorFragment[]; + const errorsAbi = abis.filter(abi => isAbiErrorFragment(abi)) as unknown as AbiErrorFragment[]; for (const a of functionsAbi) { const abi: Mutable = { @@ -1121,9 +1029,7 @@ export class Contract // make constant and payable backwards compatible abi.constant = - abi.stateMutability === 'view' ?? - abi.stateMutability === 'pure' ?? - abi.constant; + abi.stateMutability === 'view' ?? abi.stateMutability === 'pure' ?? abi.constant; abi.payable = abi.stateMutability === 'payable' ?? abi.payable; this._overloadedMethodAbis.set(abi.name, [ @@ -1131,10 +1037,10 @@ export class Contract abi, ]); const abiFragment = this._overloadedMethodAbis.get(abi.name) ?? []; - const contractMethod = this._createContractMethod< - typeof abiFragment, - AbiErrorFragment - >(abiFragment, errorsAbi); + const contractMethod = this._createContractMethod( + abiFragment, + errorsAbi, + ); const exactContractMethod = this._createContractMethod< typeof abiFragment, @@ -1147,8 +1053,7 @@ export class Contract }; // We don't know a particular type of the Abi method so can't type check - this._methods[abi.name as keyof ContractMethodsInterface] = - contractMethod as never; + this._methods[abi.name as keyof ContractMethodsInterface] = contractMethod as never; // We don't know a particular type of the Abi method so can't type check this._methods[methodName as keyof ContractMethodsInterface] = @@ -1224,10 +1129,7 @@ export class Contract for (const _abi of arrayOfAbis) { try { abiParams = this._getAbiParams(_abi, params); - validator.validate( - _abi.inputs as unknown as ValidationSchemaInput, - abiParams, - ); + validator.validate(_abi.inputs as unknown as ValidationSchemaInput, abiParams); applicableMethodAbi.push(_abi); } catch (e) { errors.push(e as Web3ValidationErrorObject); @@ -1243,9 +1145,9 @@ export class Contract } compatible methods: ${JSON.stringify( applicableMethodAbi.map( m => - `${ - (m as { methodNameWithInputs: string }).methodNameWithInputs - } (signature: ${(m as { signature: string }).signature})`, + `${(m as { methodNameWithInputs: string }).methodNameWithInputs} (signature: ${ + (m as { signature: string }).signature + })`, ), )} \n\tThe first one will be used: ${ (methodAbi as { methodNameWithInputs: string }).methodNameWithInputs @@ -1267,14 +1169,7 @@ export class Contract call: async ( options?: PayableCallOptions | NonPayableCallOptions, block?: BlockNumberOrTag, - ) => - this._contractMethodCall( - methodAbi, - abiParams, - internalErrorsAbis, - options, - block, - ), + ) => this._contractMethodCall(methodAbi, abiParams, internalErrorsAbis, options, block), send: (options?: PayableTxOptions | NonPayableTxOptions): ContractMethodSend => this._contractMethodSend(methodAbi, abiParams, internalErrorsAbis, options), @@ -1303,10 +1198,9 @@ export class Contract }, estimateGas: async ( options?: PayableCallOptions | NonPayableCallOptions, - returnFormat: ReturnFormat = this - .defaultReturnFormat as unknown as ReturnFormat, + returnFormat: ReturnFormat = this.defaultReturnFormat as unknown as ReturnFormat, ) => - this._contractMethodEstimateGas({ + this.contractMethodEstimateGas({ abi: methodAbi, params: abiParams, returnFormat, @@ -1428,17 +1322,23 @@ export class Contract contractOptions: modifiedContractOptions, }); - const transactionToSend = (isNullish(this.transactionMiddleware)) ? - sendTransaction(this, tx, this.defaultReturnFormat, { - // TODO Should make this configurable by the user - checkRevertBeforeSending: false, - contractAbi: this._jsonInterface, // explicitly not passing middleware so if some one is using old eth package it will not break - }) : - sendTransaction(this, tx, this.defaultReturnFormat, { - // TODO Should make this configurable by the user - checkRevertBeforeSending: false, - contractAbi: this._jsonInterface, - }, this.transactionMiddleware); + const transactionToSend = isNullish(this.transactionMiddleware) + ? sendTransaction(this, tx, this.defaultReturnFormat, { + // TODO Should make this configurable by the user + checkRevertBeforeSending: false, + contractAbi: this._jsonInterface, // explicitly not passing middleware so if some one is using old eth package it will not break + }) + : sendTransaction( + this, + tx, + this.defaultReturnFormat, + { + // TODO Should make this configurable by the user + checkRevertBeforeSending: false, + contractAbi: this._jsonInterface, + }, + this.transactionMiddleware, + ); // eslint-disable-next-line no-void void transactionToSend.on('error', (error: unknown) => { @@ -1450,48 +1350,7 @@ export class Contract return transactionToSend; } - private _contractMethodDeploySend( - abi: AbiFunctionFragment, - params: unknown[], - options?: Options, - contractOptions?: ContractOptions, - ) { - let modifiedContractOptions = contractOptions ?? this.options; - modifiedContractOptions = { - ...modifiedContractOptions, - from: modifiedContractOptions.from ?? this.defaultAccount ?? undefined, - }; - const tx = getSendTxParams({ - abi, - params, - options: { ...options, dataInputFill: this.contractDataInputFill }, - contractOptions: modifiedContractOptions, - }); - - const returnTxOptions: SendTransactionOptions> = { - transactionResolver: (receipt: TransactionReceipt) => { - if (receipt.status === BigInt(0)) { - throw new Web3ContractError("code couldn't be stored", receipt); - } - - const newContract = this.clone(); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - newContract.options.address = receipt.contractAddress; - return newContract; - }, - contractAbi: this._jsonInterface, - // TODO Should make this configurable by the user - checkRevertBeforeSending: false, - }; - - return ( - (isNullish(this.transactionMiddleware)) ? - sendTransaction(this, tx, this.defaultReturnFormat, returnTxOptions) : // not calling this with undefined Middleware because it will not break if Eth package is not updated - sendTransaction(this, tx, this.defaultReturnFormat, returnTxOptions, this.transactionMiddleware)); - } - - private async _contractMethodEstimateGas< + public async contractMethodEstimateGas< Options extends PayableCallOptions | NonPayableCallOptions, ReturnFormat extends DataFormat, >({ @@ -1522,11 +1381,7 @@ export class Contract returnFormat: DataFormat = this.defaultReturnFormat, ): ContractBoundEvent { return (...params: unknown[]) => { - const { topics, fromBlock } = encodeEventABI( - this.options, - abi, - params[0] as EventParameters, - ); + const { topics, fromBlock } = encodeEventABI(this.options, abi, params[0] as EventParameters); const sub = new LogsSubscription( { address: this.options.address, @@ -1536,10 +1391,7 @@ export class Contract }, { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - subscriptionManager: this.subscriptionManager as Web3SubscriptionManager< - unknown, - any - >, + subscriptionManager: this.subscriptionManager as Web3SubscriptionManager, returnFormat, }, ); @@ -1552,10 +1404,7 @@ export class Contract } }) .catch((error: Error) => { - sub.emit( - 'error', - new SubscriptionError('Failed to get past events.', error), - ); + sub.emit('error', new SubscriptionError('Failed to get past events.', error)); }); } this.subscriptionManager?.addSubscription(sub).catch((error: Error) => { diff --git a/packages/web3-eth-contract/src/index.ts b/packages/web3-eth-contract/src/index.ts index 09052f859a1..ad7b5ab7fb7 100644 --- a/packages/web3-eth-contract/src/index.ts +++ b/packages/web3-eth-contract/src/index.ts @@ -45,6 +45,7 @@ import { Contract } from './contract.js'; export * from './encoding.js'; export * from './contract.js'; +export * from './contract-deployer-method-class.js'; export * from './log_subscription.js'; export * from './types.js'; export * from './utils.js'; diff --git a/packages/web3-eth-contract/src/utils.ts b/packages/web3-eth-contract/src/utils.ts index 748203ffdf7..fc475a5a813 100644 --- a/packages/web3-eth-contract/src/utils.ts +++ b/packages/web3-eth-contract/src/utils.ts @@ -28,6 +28,7 @@ import { PayableCallOptions, ContractOptions, Numbers, + AbiConstructorFragment, } from 'web3-types'; import { isNullish, mergeDeep, isContractInitOptions, keccak256, toChecksumAddress, hexToNumber } from 'web3-utils'; import { isAddress, isHexString } from 'web3-validator'; @@ -36,7 +37,7 @@ import { Web3ContractContext } from './types.js'; const dataInputEncodeMethodHelper = ( txParams: TransactionCall | TransactionForAccessList, - abi: AbiFunctionFragment, + abi: AbiFunctionFragment | AbiConstructorFragment, params: unknown[], dataInputFill?: 'data' | 'input' | 'both', ): { data?: HexString; input?: HexString } => { @@ -60,7 +61,7 @@ export const getSendTxParams = ({ options, contractOptions, }: { - abi: AbiFunctionFragment; + abi: AbiFunctionFragment | AbiConstructorFragment; params: unknown[]; options?: (PayableCallOptions | NonPayableCallOptions) & { input?: HexString;