diff --git a/src/transactions.ts b/src/transactions.ts index 0924f86..166d9cb 100644 --- a/src/transactions.ts +++ b/src/transactions.ts @@ -237,6 +237,31 @@ export function buildTransaction(options: BuildTransactionOptions): ShellTransac if (options.maxFeePerBlobGas !== undefined && options.maxFeePerBlobGas !== null) { validateNonNegativeInteger(options.maxFeePerBlobGas, "maxFeePerBlobGas"); } + const txType = options.txType ?? DEFAULT_TX_TYPE; + const hasBlobFee = options.maxFeePerBlobGas !== undefined && options.maxFeePerBlobGas !== null; + const hasBlobHashes = + options.blobVersionedHashes !== undefined && options.blobVersionedHashes !== null; + if (txType === 3) { + if (!hasBlobFee) { + throw new RangeError("type-3 transactions require maxFeePerBlobGas"); + } + if (!hasBlobHashes || options.blobVersionedHashes?.length === 0) { + throw new RangeError("type-3 transactions require at least one blobVersionedHash"); + } + if ((options.blobVersionedHashes?.length ?? 0) > 6) { + throw new RangeError("type-3 transactions support at most 6 blobVersionedHashes"); + } + if (options.to === null) { + throw new RangeError("type-3 transactions cannot create contracts"); + } + } else { + if (hasBlobFee) { + throw new RangeError("non-blob transactions cannot set maxFeePerBlobGas"); + } + if (hasBlobHashes) { + throw new RangeError("non-blob transactions cannot set blobVersionedHashes"); + } + } return { chain_id: options.chainId, @@ -248,7 +273,7 @@ export function buildTransaction(options: BuildTransactionOptions): ShellTransac max_fee_per_gas: maxFeePerGas, max_priority_fee_per_gas: maxPriorityFeePerGas, access_list: options.accessList ?? null, - tx_type: options.txType ?? DEFAULT_TX_TYPE, + tx_type: txType, max_fee_per_blob_gas: options.maxFeePerBlobGas ?? null, blob_versioned_hashes: options.blobVersionedHashes ?? null, }; diff --git a/tests/transactions.test.mjs b/tests/transactions.test.mjs new file mode 100644 index 0000000..ec66988 --- /dev/null +++ b/tests/transactions.test.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { buildTransaction } from '../dist/transactions.js'; + +const recipient = `0x${'11'.repeat(32)}`; +const blobHash = `0x${'22'.repeat(32)}`; + +function baseOptions(overrides = {}) { + return { + chainId: 1337, + nonce: 0, + to: recipient, + ...overrides, + }; +} + +test('buildTransaction rejects blob-only fields on non-blob transactions', () => { + assert.throws( + () => buildTransaction(baseOptions({ maxFeePerBlobGas: 1 })), + /non-blob transactions cannot set maxFeePerBlobGas/, + ); + assert.throws( + () => buildTransaction(baseOptions({ blobVersionedHashes: [blobHash] })), + /non-blob transactions cannot set blobVersionedHashes/, + ); +}); + +test('buildTransaction requires complete type-3 blob fields', () => { + assert.throws( + () => buildTransaction(baseOptions({ txType: 3, blobVersionedHashes: [blobHash] })), + /require maxFeePerBlobGas/, + ); + assert.throws( + () => buildTransaction(baseOptions({ txType: 3, maxFeePerBlobGas: 1 })), + /require at least one blobVersionedHash/, + ); + assert.throws( + () => + buildTransaction( + baseOptions({ + txType: 3, + maxFeePerBlobGas: 1, + blobVersionedHashes: Array(7).fill(blobHash), + }), + ), + /at most 6 blobVersionedHashes/, + ); + assert.throws( + () => + buildTransaction( + baseOptions({ + txType: 3, + to: null, + maxFeePerBlobGas: 1, + blobVersionedHashes: [blobHash], + }), + ), + /cannot create contracts/, + ); +}); + +test('buildTransaction accepts valid type-3 blob fields', () => { + const tx = buildTransaction( + baseOptions({ + txType: 3, + maxFeePerBlobGas: 1, + blobVersionedHashes: [blobHash], + }), + ); + + assert.equal(tx.tx_type, 3); + assert.equal(tx.max_fee_per_blob_gas, 1); + assert.deepEqual(tx.blob_versioned_hashes, [blobHash]); +});