Skip to content

Commit

Permalink
feat: add custom transaction schema to formatTransaction (#7227)
Browse files Browse the repository at this point in the history
* feat: add custom transaction schema to formatTransaction

* docs: add entries to changelogs

* tests: initial tests

* fix: unused vars

* tests: lint issues

* refactor: pr review

* fix: type errors

* fix: dependency cycle

* refactor: revert whitespaces changes

* fix: types

* fix: types

* test: fix web3-eth-personal tests

* refactor: remove config from manager

* fix: types

* fix: build issue

* fix: CustomTransactionSchema type
  • Loading branch information
nicolasbrugneaux authored Sep 17, 2024
1 parent b3cb1b7 commit a21078b
Show file tree
Hide file tree
Showing 21 changed files with 207 additions and 25 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2702,3 +2702,24 @@ If there are any bugs, improvements, optimizations or any new feature proposal f
- The callback function provided to the static `Web3.onNewProviderDiscovered` function expects a parameter of type `EIP6963ProvidersMapUpdateEvent` as opposed to `EIP6963AnnounceProviderEvent`. (#7242)

## [Unreleased]

### Added

#### web3-core

- Adds a new property (`customTransactionSchema`) to `Web3ConfigOptions`
- Adds a new property (`config`) to `Web3RequestManager`

#### web3-eth

- Adds the same `{transactionSchema?: ValidationSchemaInput}` that exists in `formatTransaction` to `validateTransactionForSigning`

### Changed

#### web3-eth

- Forwards the new `web3Context.config.customTransactionSchema` to `formatTransaction`

#### web3-eth-personal

- Forwards the new `web3Context.config.customTransactionSchema` to `formatTransaction`
4 changes: 4 additions & 0 deletions docs/docs/guides/web3_config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ There is list of configuration params that can be set for modifying behavior of
- [defaultCommon](/guides/web3_config/#defaultcommon)
- [defaultTransactionType](/guides/web3_config/#defaulttransactiontype)
- [defaultMaxPriorityFeePerGas](/guides/web3_config/#defaultmaxpriorityfeepergas)
- [customTransactionSchema](/guides/web3_config/#customTransactionSchema)
- [defaultReturnFormat](/guides/web3_config/#defaultreturnformat)

## Global level Config
Expand Down Expand Up @@ -411,6 +412,9 @@ The `defaultMaxPriorityFeePerGas` option is used to set the [`defaultMaxPriority

The default value of `defaultMaxPriorityFeePerGas` is 2500000000 (2.5gwei) in hexstring format.

### [customTransactionSchema](/api/web3-core/class/Web3Config#customTransactionSchema)
The `customTransactionSchema` option is used to allow [`formatTransaction`](/api/web3-eth/function/formatTransaction) to accept a custom schema to validate transactions. A use-case could be: your chain has an extra field in its transactions and you want to write a plugin that makes sending these transactions easier.

### [defaultReturnFormat](/api/web3-core/class/Web3Config#defaultReturnFormat)
The `defaultReturnFormat` option allows users to specify the format in which certain types of data should be returned by default. It is a configuration parameter that can be set at the global level and affects how data is returned across the entire library.
```ts
Expand Down
5 changes: 5 additions & 0 deletions packages/web3-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,8 @@ Documentation:
- `setConfig()` fix for `setMaxListenerWarningThreshold` fix (#5079)

## [Unreleased]

### Added

- Adds a new property (`customTransactionSchema`) to `Web3ConfigOptions`
- Adds a new property (`config`) to `Web3RequestManager`
6 changes: 6 additions & 0 deletions packages/web3-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Web3APIMethod,
Web3APIReturnType,
} from 'web3-types';
import { Schema } from 'web3-validator';

export type TransactionTypeParser = (transaction: Transaction) => HexString | undefined;

Expand Down Expand Up @@ -50,3 +51,8 @@ export interface RequestManagerMiddleware<API> {
options?: { [key: string]: unknown },
): Promise<JsonRpcResponse<ResponseType>>;
}

export type CustomTransactionSchema = {
type: string;
properties: Record<string, Schema>;
};
13 changes: 12 additions & 1 deletion packages/web3-core/src/web3_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from 'web3-types';
import { ConfigHardforkMismatchError, ConfigChainMismatchError } from 'web3-errors';
import { isNullish, toHex } from 'web3-utils';
import { TransactionTypeParser } from './types.js';
import { CustomTransactionSchema, TransactionTypeParser } from './types.js';
// eslint-disable-next-line import/no-cycle
import { TransactionBuilder } from './web3_context.js';
import { Web3EventEmitter } from './web3_event_emitter.js';
Expand Down Expand Up @@ -59,6 +59,7 @@ export interface Web3ConfigOptions {
};
transactionBuilder?: TransactionBuilder;
transactionTypeParser?: TransactionTypeParser;
customTransactionSchema?: CustomTransactionSchema;
defaultReturnFormat: DataFormat;
}

Expand Down Expand Up @@ -101,6 +102,7 @@ export abstract class Web3Config
},
transactionBuilder: undefined,
transactionTypeParser: undefined,
customTransactionSchema: undefined,
defaultReturnFormat: DEFAULT_RETURN_FORMAT,
};

Expand Down Expand Up @@ -520,6 +522,15 @@ export abstract class Web3Config
this.config.transactionTypeParser = val;
}

public get customTransactionSchema(): CustomTransactionSchema | undefined {
return this.config.customTransactionSchema;
}

public set customTransactionSchema(schema: CustomTransactionSchema | undefined) {
this._triggerConfigChange('customTransactionSchema', schema);
this.config.customTransactionSchema = schema;
}

private _triggerConfigChange<K extends keyof Web3ConfigOptions>(
config: K,
newValue: Web3ConfigOptions[K],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`Web3Context getContextObject should return correct context object 1`] =
"config": {
"blockHeaderTimeout": 10,
"contractDataInputFill": "data",
"customTransactionSchema": undefined,
"defaultAccount": undefined,
"defaultBlock": "latest",
"defaultChain": "mainnet",
Expand Down
1 change: 1 addition & 0 deletions packages/web3-core/test/unit/web3_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const defaultConfig = {
defaultReturnFormat: DEFAULT_RETURN_FORMAT,
transactionBuilder: undefined,
transactionTypeParser: undefined,
customTransactionSchema: undefined,
};
const setValue = {
string: 'newValue',
Expand Down
4 changes: 4 additions & 0 deletions packages/web3-eth-personal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,7 @@ Documentation:
- Dependencies updated

## [Unreleased]

### Changed

- Forwards the new `web3Context.config.customTransactionSchema` to `formatTransaction`
4 changes: 2 additions & 2 deletions packages/web3-eth-personal/src/personal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class Personal extends Web3Context<EthPersonalAPI> {
* ```
*/
public async sendTransaction(tx: Transaction, passphrase: string) {
return rpcWrappers.sendTransaction(this.requestManager, tx, passphrase);
return rpcWrappers.sendTransaction(this.requestManager, tx, passphrase, this.config);
}
/**
* Signs a transaction. This account needs to be unlocked.
Expand Down Expand Up @@ -204,7 +204,7 @@ export class Personal extends Web3Context<EthPersonalAPI> {
* ```
*/
public async signTransaction(tx: Transaction, passphrase: string) {
return rpcWrappers.signTransaction(this.requestManager, tx, passphrase);
return rpcWrappers.signTransaction(this.requestManager, tx, passphrase, this.config);
}
/**
* Calculates an Ethereum specific signature with:
Expand Down
12 changes: 9 additions & 3 deletions packages/web3-eth-personal/src/rpc_method_wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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 <http://www.gnu.org/licenses/>.
*/
import { Web3RequestManager } from 'web3-core';
import { Web3RequestManager, Web3ConfigOptions } from 'web3-core';
import { toChecksumAddress, utf8ToHex } from 'web3-utils';
import { formatTransaction } from 'web3-eth';
import { Address, EthPersonalAPI, ETH_DATA_FORMAT, HexString, Transaction } from 'web3-types';
Expand Down Expand Up @@ -72,8 +72,11 @@ export const sendTransaction = async (
requestManager: Web3RequestManager<EthPersonalAPI>,
tx: Transaction,
passphrase: string,
config?: Web3ConfigOptions,
) => {
const formattedTx = formatTransaction(tx, ETH_DATA_FORMAT);
const formattedTx = formatTransaction(tx, ETH_DATA_FORMAT, {
transactionSchema: config?.customTransactionSchema,
});

return personalRpcMethods.sendTransaction(requestManager, formattedTx, passphrase);
};
Expand All @@ -82,8 +85,11 @@ export const signTransaction = async (
requestManager: Web3RequestManager<EthPersonalAPI>,
tx: Transaction,
passphrase: string,
config?: Web3ConfigOptions,
) => {
const formattedTx = formatTransaction(tx, ETH_DATA_FORMAT);
const formattedTx = formatTransaction(tx, ETH_DATA_FORMAT, {
transactionSchema: config?.customTransactionSchema,
});

return personalRpcMethods.signTransaction(requestManager, formattedTx, passphrase);
};
Expand Down
12 changes: 10 additions & 2 deletions packages/web3-eth-personal/test/unit/eth_personal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ describe('Personal', () => {
await personal.sendTransaction(tx, 'password');

expect(eth.formatTransaction).toHaveBeenCalledTimes(1);
expect(eth.formatTransaction).toHaveBeenCalledWith(tx, ETH_DATA_FORMAT);
expect(eth.formatTransaction).toHaveBeenCalledWith(
tx,
ETH_DATA_FORMAT,
expect.anything(),
);
});
});

Expand Down Expand Up @@ -215,7 +219,11 @@ describe('Personal', () => {
await personal.signTransaction(tx, 'password');

expect(eth.formatTransaction).toHaveBeenCalledTimes(1);
expect(eth.formatTransaction).toHaveBeenCalledWith(tx, ETH_DATA_FORMAT);
expect(eth.formatTransaction).toHaveBeenCalledWith(
tx,
ETH_DATA_FORMAT,
expect.anything(),
);
});
});

Expand Down
8 changes: 8 additions & 0 deletions packages/web3-eth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,11 @@ Documentation:
- Change method `getTransactionReceipt` to not be casted as `TransactionReceipt` to give proper return type (#7159)

## [Unreleased]

### Changed

- Forwards the new `web3Context.config.customTransactionSchema` to `formatTransaction`

### Added

- Adds the same `{transactionSchema?: ValidationSchemaInput}` that exists in `formatTransaction` to `validateTransactionForSigning`
23 changes: 19 additions & 4 deletions packages/web3-eth/src/rpc_method_wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ export async function getTransaction<ReturnFormat extends DataFormat>(
return isNullish(response)
? response
: formatTransaction(response, returnFormat, {
transactionSchema: web3Context.config.customTransactionSchema,
fillInputAndData: true,
});
}
Expand All @@ -448,6 +449,7 @@ export async function getPendingTransactions<ReturnFormat extends DataFormat>(
transaction as unknown as Transaction,
returnFormat ?? web3Context.defaultReturnFormat,
{
transactionSchema: web3Context.config.customTransactionSchema,
fillInputAndData: true,
},
),
Expand Down Expand Up @@ -488,6 +490,7 @@ export async function getTransactionFromBlock<ReturnFormat extends DataFormat>(
return isNullish(response)
? response
: formatTransaction(response, returnFormat ?? web3Context.defaultReturnFormat, {
transactionSchema: web3Context.config.customTransactionSchema,
fillInputAndData: true,
});
}
Expand Down Expand Up @@ -606,6 +609,9 @@ export function sendTransaction<
to: getTransactionFromOrToAttr('to', web3Context, transaction),
},
ETH_DATA_FORMAT,
{
transactionSchema: web3Context.config.customTransactionSchema,
},
);

try {
Expand Down Expand Up @@ -847,7 +853,9 @@ export async function signTransaction<ReturnFormat extends DataFormat>(
) {
const response = await ethRpcMethods.signTransaction(
web3Context.requestManager,
formatTransaction(transaction, ETH_DATA_FORMAT),
formatTransaction(transaction, ETH_DATA_FORMAT, {
transactionSchema: web3Context.config.customTransactionSchema,
}),
);
// Some clients only return the encoded signed transaction (e.g. Ganache)
// while clients such as Geth return the desired SignedTransactionInfoAPI object
Expand All @@ -862,6 +870,7 @@ export async function signTransaction<ReturnFormat extends DataFormat>(
returnFormat,
),
tx: formatTransaction((response as SignedTransactionInfoAPI).tx, returnFormat, {
transactionSchema: web3Context.config.customTransactionSchema,
fillInputAndData: true,
}),
};
Expand All @@ -885,7 +894,9 @@ export async function call<ReturnFormat extends DataFormat>(

const response = await ethRpcMethods.call(
web3Context.requestManager,
formatTransaction(transaction, ETH_DATA_FORMAT),
formatTransaction(transaction, ETH_DATA_FORMAT, {
transactionSchema: web3Context.config.customTransactionSchema,
}),
blockNumberFormatted,
);

Expand All @@ -903,7 +914,9 @@ export async function estimateGas<ReturnFormat extends DataFormat>(
blockNumber: BlockNumberOrTag = web3Context.defaultBlock,
returnFormat: ReturnFormat,
) {
const transactionFormatted = formatTransaction(transaction, ETH_DATA_FORMAT);
const transactionFormatted = formatTransaction(transaction, ETH_DATA_FORMAT, {
transactionSchema: web3Context.config.customTransactionSchema,
});
const blockNumberFormatted = isBlockTag(blockNumber as string)
? (blockNumber as BlockTag)
: format({ format: 'uint' }, blockNumber as Numbers, ETH_DATA_FORMAT);
Expand Down Expand Up @@ -1074,7 +1087,9 @@ export async function createAccessList<ReturnFormat extends DataFormat>(

const response = (await ethRpcMethods.createAccessList(
web3Context.requestManager,
formatTransaction(transaction, ETH_DATA_FORMAT),
formatTransaction(transaction, ETH_DATA_FORMAT, {
transactionSchema: web3Context.config.customTransactionSchema,
}),
blockNumberFormatted,
)) as unknown as AccessListResult;

Expand Down
6 changes: 6 additions & 0 deletions packages/web3-eth/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
TransactionWithFromLocalWalletIndex,
TransactionWithToLocalWalletIndex,
} from 'web3-types';
import { Schema } from 'web3-validator';

export type InternalTransaction = FormatType<Transaction, typeof ETH_DATA_FORMAT>;

Expand Down Expand Up @@ -105,3 +106,8 @@ export interface TransactionMiddleware {
options?: { [key: string]: unknown },
): Promise<TransactionMiddlewareData>;
}

export type CustomTransactionSchema = {
type: string;
properties: Record<string, Schema>;
};
10 changes: 8 additions & 2 deletions packages/web3-eth/src/utils/decode_signed_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { bytesToHex, format, hexToBytes, keccak256 } from 'web3-utils';
import { TransactionFactory } from 'web3-eth-accounts';
import { detectRawTransactionType } from './detect_transaction_type.js';
import { formatTransaction } from './format_transaction.js';
import { type CustomTransactionSchema } from '../types.js';

/**
* Decodes an [RLP](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/#top) encoded transaction.
Expand All @@ -35,7 +36,9 @@ import { formatTransaction } from './format_transaction.js';
export function decodeSignedTransaction<ReturnFormat extends DataFormat>(
encodedSignedTransaction: HexStringBytes,
returnFormat: ReturnFormat,
options: { fillInputAndData?: boolean } = { fillInputAndData: false },
options: { fillInputAndData?: boolean; transactionSchema?: CustomTransactionSchema } = {
fillInputAndData: false,
},
): SignedTransactionInfoAPI {
return {
raw: format({ format: 'bytes' }, encodedSignedTransaction, returnFormat),
Expand All @@ -48,7 +51,10 @@ export function decodeSignedTransaction<ReturnFormat extends DataFormat>(
type: detectRawTransactionType(hexToBytes(encodedSignedTransaction)),
} as TransactionSignedAPI,
returnFormat,
{ fillInputAndData: options.fillInputAndData },
{
fillInputAndData: options.fillInputAndData,
transactionSchema: options.transactionSchema,
},
),
};
}
5 changes: 3 additions & 2 deletions packages/web3-eth/src/utils/format_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { isNullish, ValidationSchemaInput } from 'web3-validator';
import { mergeDeep, format, bytesToHex, toHex } from 'web3-utils';
import { TransactionDataAndInputError } from 'web3-errors';

import { transactionInfoSchema, transactionSchema } from '../schemas.js';
import { transactionInfoSchema } from '../schemas.js';
import { type CustomTransactionSchema } from '../types.js';

export function formatTransaction<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT,
Expand All @@ -29,7 +30,7 @@ export function formatTransaction<
transaction: TransactionType,
returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat,
options: {
transactionSchema?: ValidationSchemaInput | typeof transactionSchema;
transactionSchema?: ValidationSchemaInput | CustomTransactionSchema | undefined;
fillInputAndData?: boolean;
} = {
transactionSchema: transactionInfoSchema,
Expand Down
12 changes: 8 additions & 4 deletions packages/web3-eth/src/utils/prepare_transaction_for_signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,16 @@ export const prepareTransactionForSigning = async (
fillGasPrice,
fillGasLimit,
})) as unknown as PopulatedUnsignedTransaction;
const formattedTransaction = formatTransaction(
populatedTransaction,
ETH_DATA_FORMAT,
) as unknown as FormatType<PopulatedUnsignedTransaction, typeof ETH_DATA_FORMAT>;
const formattedTransaction = formatTransaction(populatedTransaction, ETH_DATA_FORMAT, {
transactionSchema: web3Context.config.customTransactionSchema,
}) as unknown as FormatType<PopulatedUnsignedTransaction, typeof ETH_DATA_FORMAT>;

validateTransactionForSigning(
formattedTransaction as unknown as FormatType<Transaction, typeof ETH_DATA_FORMAT>,
undefined,
{
transactionSchema: web3Context.config.customTransactionSchema,
},
);

return TransactionFactory.fromTxData(
Expand Down
Loading

1 comment on commit a21078b

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: a21078b Previous: b3cb1b7 Ratio
processingTx 22494 ops/sec (±8.39%) 21696 ops/sec (±8.85%) 0.96
processingContractDeploy 38470 ops/sec (±7.11%) 40328 ops/sec (±6.93%) 1.05
processingContractMethodSend 16121 ops/sec (±6.53%) 17416 ops/sec (±6.78%) 1.08
processingContractMethodCall 28342 ops/sec (±6.29%) 28028 ops/sec (±7.80%) 0.99
abiEncode 43839 ops/sec (±6.81%) 46457 ops/sec (±6.59%) 1.06
abiDecode 30836 ops/sec (±5.71%) 31537 ops/sec (±7.34%) 1.02
sign 1545 ops/sec (±3.43%) 1584 ops/sec (±0.53%) 1.03
verify 370 ops/sec (±0.49%) 364 ops/sec (±2.47%) 0.98

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.