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
39 changes: 18 additions & 21 deletions packages/web3-eth/src/utils/prepare_transaction_for_signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
import {
EthExecutionAPI,
HexString,
PopulatedUnsignedEip1559Transaction,
PopulatedUnsignedEip2930Transaction,
PopulatedUnsignedTransaction,
Transaction,
ValidChains,
Expand All @@ -36,25 +34,24 @@ import { transactionBuilder } from './transaction_builder.js';

const getEthereumjsTxDataFromTransaction = (
transaction: FormatType<PopulatedUnsignedTransaction, typeof ETH_DATA_FORMAT>,
) => ({
nonce: transaction.nonce,
gasPrice: transaction.gasPrice,
gasLimit: transaction.gasLimit ?? transaction.gas,
to: transaction.to,
value: transaction.value,
data: transaction.data ?? transaction.input,
type: transaction.type,
chainId: transaction.chainId,
accessList: (
transaction as FormatType<PopulatedUnsignedEip2930Transaction, typeof ETH_DATA_FORMAT>
).accessList,
maxPriorityFeePerGas: (
transaction as FormatType<PopulatedUnsignedEip1559Transaction, typeof ETH_DATA_FORMAT>
).maxPriorityFeePerGas,
maxFeePerGas: (
transaction as FormatType<PopulatedUnsignedEip1559Transaction, typeof ETH_DATA_FORMAT>
).maxFeePerGas,
});
) => {
const txData = { ...transaction };
nicolasbrugneaux marked this conversation as resolved.
Show resolved Hide resolved

const aliases = [
['input', 'data'],
['gas', 'gasLimit'],
] as const;

for (const [oldField, newField] of aliases) {
if (typeof txData[oldField] !== 'undefined') {
// @ts-expect-error
txData[newField] = txData[oldField];
delete txData[oldField];
}
}

return txData;
};

const getEthereumjsTransactionOptions = (
transaction: FormatType<PopulatedUnsignedTransaction, typeof ETH_DATA_FORMAT>,
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,54 @@ 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
expect(spy.mock.lastCall[0].feeCurrency).toEqual(
'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
16 changes: 10 additions & 6 deletions packages/web3/test/integration/web3-plugin-add-tx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@ describe('Add New Tx as a Plugin', () => {
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);
});
});
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 });
});
},
);
expect(Number((await waitForEvent).type)).toBe(TRANSACTION_TYPE);
expect(BigInt((await waitForEvent).customField)).toBe(BigInt(42));
await expect(sub).rejects.toThrow();
});
});
Loading