Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make getEthereumjsTxDataFromTransaction not trim extra fields #7282

Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2728,3 +2728,9 @@ 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]

### Changed

#### web3-eth

- Allow `getEthereumjsTxDataFrom` to return additional fields that may be passed if using a `customTransactionSchema`.
4 changes: 4 additions & 0 deletions packages/web3-eth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,7 @@ Documentation:
- Adds the same `{transactionSchema?: ValidationSchemaInput}` that exists in `formatTransaction` to `validateTransactionForSigning`

## [Unreleased]

### Changed

- Allow `getEthereumjsTxDataFrom` to return additional fields that may be passed if using a `customTransactionSchema`.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { transactionBuilder } from './transaction_builder.js';
const getEthereumjsTxDataFromTransaction = (
transaction: FormatType<PopulatedUnsignedTransaction, typeof ETH_DATA_FORMAT>,
) => ({
...transaction,
nonce: transaction.nonce,
gasPrice: transaction.gasPrice,
gasLimit: transaction.gasLimit ?? transaction.gas,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ import {
FeeMarketEIP1559Transaction,
Transaction,
Hardfork,
TypedTransaction,
TransactionFactory,
} from 'web3-eth-accounts';
import { prepareTransactionForSigning } from '../../src/utils/prepare_transaction_for_signing';
import { validTransactions } from '../fixtures/prepare_transaction_for_signing';
import { transactionSchema } from '../../src/schemas';
import { CustomFieldTransaction } from '../fixtures/format_transaction';

describe('prepareTransactionForSigning', () => {
const web3Context = new Web3Context<EthExecutionAPI>({
Expand Down Expand Up @@ -317,4 +321,52 @@ describe('prepareTransactionForSigning', () => {
},
);
});

it('should not remove extra fields when using a custom schema', async () => {
const context = new Web3Context<EthExecutionAPI>({
provider: new HttpProvider('http://127.0.0.1'),
config: {
defaultNetworkId: '0x1',
customTransactionSchema: {
type: 'object',
properties: {
...transactionSchema.properties,
feeCurrency: { format: 'address' },
},
},
},
});

async function transactionBuilder<ReturnType = TransactionType>(options: {
transaction: TransactionType;
web3Context: Web3Context<EthExecutionAPI & Web3NetAPI>;
privateKey?: HexString | Uint8Array;
fillGasPrice?: boolean;
fillGasLimit?: boolean;
}): Promise<ReturnType> {
const tx = { ...options.transaction };
return tx as unknown as ReturnType;
}

context.transactionBuilder = transactionBuilder;

const spy = jest.spyOn(TransactionFactory, 'fromTxData');
(await prepareTransactionForSigning(
{
chainId: 1458,
nonce: 1,
gasPrice: BigInt(20000000000),
gasLimit: BigInt(21000),
to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55',
from: '0x2c7536E3605D9C16a7a3D7b1898e529396a65c23',
value: '1000000000',
input: '',
feeCurrency: '0x1234567890123456789012345678901234567890',
} as CustomFieldTransaction,
context,
)) as TypedTransaction & { feeCurrency: string };

// @ts-expect-error feeCurrency is a custom field for testing here
expect(spy.mock.lastCall[0].feeCurrency).toBe('0x1234567890123456789012345678901234567890');
});
});
50 changes: 38 additions & 12 deletions packages/web3/test/fixtures/tx-type-15/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import {
AccessList,
AccessListUint8Array,
FeeMarketEIP1559TxData,
FeeMarketEIP1559ValuesArray,
JsonTx,
TxOptions,
TxValuesArray,
} from 'web3-eth-accounts';

const { getAccessListData, getAccessListJSON, getDataFeeEIP2930, verifyAccessList } = txUtils;
Expand All @@ -43,19 +43,40 @@ const MAX_INTEGER = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffff
export const TRANSACTION_TYPE = 15;
const TRANSACTION_TYPE_UINT8ARRAY = hexToBytes(TRANSACTION_TYPE.toString(16).padStart(2, '0'));

type CustomFieldTxValuesArray = [
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
AccessListUint8Array,
Uint8Array,
Uint8Array?,
Uint8Array?,
Uint8Array?,
];

type SomeNewTxTypeTxData = FeeMarketEIP1559TxData & {
customTestField: bigint;
};

/**
* Typed transaction with a new gas fee market mechanism
*
* - TransactionType: 2
* - EIP: [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)
*/
// eslint-disable-next-line no-use-before-define
export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Transaction> {
export class SomeNewTxTypeTransaction extends BaseTransaction<SomeNewTxTypeTransaction> {
public readonly chainId: bigint;
public readonly accessList: AccessListUint8Array;
public readonly AccessListJSON: AccessList;
public readonly maxPriorityFeePerGas: bigint;
public readonly maxFeePerGas: bigint;
public readonly customTestField: bigint;

public readonly common: Common;

Expand All @@ -77,7 +98,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
* - `chainId` will be set automatically if not provided
* - All parameters are optional and have some basic default values
*/
public static fromTxData(txData: FeeMarketEIP1559TxData, opts: TxOptions = {}) {
public static fromTxData(txData: SomeNewTxTypeTxData, opts: TxOptions = {}) {
return new SomeNewTxTypeTransaction(txData, opts);
}

Expand Down Expand Up @@ -110,10 +131,10 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
* Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data,
* accessList, signatureYParity, signatureR, signatureS]`
*/
public static fromValuesArray(values: FeeMarketEIP1559ValuesArray, opts: TxOptions = {}) {
if (values.length !== 9 && values.length !== 12) {
public static fromValuesArray(values: CustomFieldTxValuesArray, opts: TxOptions = {}) {
if (values.length !== 10 && values.length !== 13) {
throw new Error(
'Invalid EIP-1559 transaction. Only expecting 9 values (for unsigned tx) or 12 values (for signed tx).',
'Invalid CUSTOM TEST transaction. Only expecting 10 values (for unsigned tx) or 13 values (for signed tx).',
);
}

Expand All @@ -127,6 +148,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
value,
data,
accessList,
customTestField,
v,
r,
s,
Expand All @@ -144,7 +166,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
s,
});

return new FeeMarketEIP1559Transaction(
return new SomeNewTxTypeTransaction(
{
chainId: uint8ArrayToBigInt(chainId),
nonce,
Expand All @@ -155,6 +177,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
value,
data,
accessList: accessList ?? [],
customTestField: uint8ArrayToBigInt(customTestField),
v: v !== undefined ? uint8ArrayToBigInt(v) : undefined, // EIP2930 supports v's with value 0 (empty Uint8Array)
r,
s,
Expand All @@ -170,12 +193,13 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
* the static factory methods to assist in creating a Transaction object from
* varying data types.
*/
public constructor(txData: FeeMarketEIP1559TxData, opts: TxOptions = {}) {
public constructor(txData: SomeNewTxTypeTxData, opts: TxOptions = {}) {
super({ ...txData, type: TRANSACTION_TYPE }, opts);
const { chainId, accessList, maxFeePerGas, maxPriorityFeePerGas } = txData;
const { chainId, accessList, maxFeePerGas, maxPriorityFeePerGas, customTestField } = txData;

this.common = this._getCommon(opts.common, chainId);
this.chainId = this.common.chainId();
this.customTestField = customTestField;

if (!this.common.isActivatedEIP(1559)) {
throw new Error('EIP-1559 not enabled on Common');
Expand Down Expand Up @@ -272,7 +296,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
* signature parameters `v`, `r` and `s` for encoding. For an EIP-155 compliant
* representation for external signing use {@link FeeMarketEIP1559Transaction.getMessageToSign}.
*/
public raw(): FeeMarketEIP1559ValuesArray {
public raw(): TxValuesArray {
return [
bigIntToUnpaddedUint8Array(this.chainId),
bigIntToUnpaddedUint8Array(this.nonce),
Expand All @@ -283,10 +307,11 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
bigIntToUnpaddedUint8Array(this.value),
this.data,
this.accessList,
bigIntToUnpaddedUint8Array(this.customTestField),
this.v !== undefined ? bigIntToUnpaddedUint8Array(this.v) : Uint8Array.from([]),
this.r !== undefined ? bigIntToUnpaddedUint8Array(this.r) : Uint8Array.from([]),
this.s !== undefined ? bigIntToUnpaddedUint8Array(this.s) : Uint8Array.from([]),
];
] as TxValuesArray;
}

/**
Expand Down Expand Up @@ -384,7 +409,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
public _processSignature(v: bigint, r: Uint8Array, s: Uint8Array) {
const opts = { ...this.txOptions, common: this.common };

return FeeMarketEIP1559Transaction.fromTxData(
return SomeNewTxTypeTransaction.fromTxData(
{
chainId: this.chainId,
nonce: this.nonce,
Expand All @@ -395,6 +420,7 @@ export class SomeNewTxTypeTransaction extends BaseTransaction<FeeMarketEIP1559Tr
value: this.value,
data: this.data,
accessList: this.accessList,
customTestField: this.customTestField,
v: v - BigInt(27), // This looks extremely hacky: /util actually adds 27 to the value, the recovery bit is either 0 or 1.
r: uint8ArrayToBigInt(r),
s: uint8ArrayToBigInt(s),
Expand Down
28 changes: 21 additions & 7 deletions packages/web3/test/integration/web3-plugin-add-tx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-magic-numbers */

import { Transaction, Web3Account } from 'web3-eth-accounts';
import { transactionSchema } from 'web3-eth';
import { SupportedProviders, Web3, Web3PluginBase } from '../../src';
import {
createAccount,
Expand Down Expand Up @@ -51,23 +52,36 @@ describe('Add New Tx as a Plugin', () => {
});
it('should receive correct type of tx', async () => {
web3.registerPlugin(new Eip4844Plugin());
web3.config.customTransactionSchema = {
type: 'object',
properties: {
...transactionSchema.properties,
customField: { format: 'string' },
},
};
web3.eth.config.customTransactionSchema = web3.config.customTransactionSchema;
const tx = {
from: account1.address,
to: account2.address,
value: '0x1',
type: TRANSACTION_TYPE,
maxPriorityFeePerGas: BigInt(5000000),
maxFeePerGas: BigInt(5000000),
customField: BigInt(42),
};
const sub = web3.eth.sendTransaction(tx);

const waitForEvent: Promise<Transaction> = new Promise(resolve => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sub.on('sending', txData => {
resolve(txData as unknown as Transaction);
});
});
expect(Number((await waitForEvent).type)).toBe(TRANSACTION_TYPE);
const waitForEvent: Promise<Transaction & { customField: bigint }> = new Promise(
resolve => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sub.on('sending', txData => {
resolve(txData as unknown as Transaction & { customField: bigint });
});
},
);
const { type, customField } = await waitForEvent;
expect(Number(type)).toBe(TRANSACTION_TYPE);
expect(BigInt(customField)).toBe(BigInt(42));
await expect(sub).rejects.toThrow();
});
});
Loading