diff --git a/.changeset/tx-envelope-chain-id-assert.md b/.changeset/tx-envelope-chain-id-assert.md new file mode 100644 index 00000000..babf9a7e --- /dev/null +++ b/.changeset/tx-envelope-chain-id-assert.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `TxEnvelopeLegacy.assert`, `TxEnvelopeEip2930.assert`, `TxEnvelopeEip1559.assert` and `TxEnvelopeTempo.assert` to throw `InvalidChainIdError` for a `chainId` that is not a positive safe integer. diff --git a/src/core/TxEnvelopeEip1559.ts b/src/core/TxEnvelopeEip1559.ts index 35ed409b..fb8db935 100644 --- a/src/core/TxEnvelopeEip1559.ts +++ b/src/core/TxEnvelopeEip1559.ts @@ -71,7 +71,10 @@ export type Type = typeof type */ export function assert(envelope: PartialBy) { const { chainId, maxPriorityFeePerGas, maxFeePerGas, to } = envelope - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (to) Address.assert(to, { strict: false }) if (maxFeePerGas && BigInt(maxFeePerGas) > 2n ** 256n - 1n) diff --git a/src/core/TxEnvelopeEip2930.ts b/src/core/TxEnvelopeEip2930.ts index ddbf6169..ebc67979 100644 --- a/src/core/TxEnvelopeEip2930.ts +++ b/src/core/TxEnvelopeEip2930.ts @@ -69,7 +69,10 @@ export type Type = typeof type */ export function assert(envelope: PartialBy) { const { chainId, gasPrice, to } = envelope - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (to) Address.assert(to, { strict: false }) if (gasPrice && BigInt(gasPrice) > 2n ** 256n - 1n) diff --git a/src/core/TxEnvelopeLegacy.ts b/src/core/TxEnvelopeLegacy.ts index 3f8a5f05..036dc1dc 100644 --- a/src/core/TxEnvelopeLegacy.ts +++ b/src/core/TxEnvelopeLegacy.ts @@ -68,7 +68,10 @@ export type Type = typeof type export function assert(envelope: PartialBy) { const { chainId, gasPrice, to } = envelope if (to) Address.assert(to, { strict: false }) - if (typeof chainId !== 'undefined' && chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (gasPrice && BigInt(gasPrice) > 2n ** 256n - 1n) throw new TransactionEnvelope.GasPriceTooHighError({ gasPrice }) diff --git a/src/core/_test/TransactionEnvelopeEip1559.test.ts b/src/core/_test/TransactionEnvelopeEip1559.test.ts index 250f7593..837d9c41 100644 --- a/src/core/_test/TransactionEnvelopeEip1559.test.ts +++ b/src/core/_test/TransactionEnvelopeEip1559.test.ts @@ -47,6 +47,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeEip1559.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeEip1559.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/core/_test/TransactionEnvelopeEip2930.test.ts b/src/core/_test/TransactionEnvelopeEip2930.test.ts index 15baa6ad..5513c19f 100644 --- a/src/core/_test/TransactionEnvelopeEip2930.test.ts +++ b/src/core/_test/TransactionEnvelopeEip2930.test.ts @@ -23,6 +23,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeEip2930.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeEip2930.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/core/_test/TransactionEnvelopeLegacy.test.ts b/src/core/_test/TransactionEnvelopeLegacy.test.ts index adff927f..6412dea8 100644 --- a/src/core/_test/TransactionEnvelopeLegacy.test.ts +++ b/src/core/_test/TransactionEnvelopeLegacy.test.ts @@ -23,6 +23,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeLegacy.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeLegacy.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/tempo/TxEnvelopeTempo.test.ts b/src/tempo/TxEnvelopeTempo.test.ts index 3c21cfd5..a3f34c0e 100644 --- a/src/tempo/TxEnvelopeTempo.test.ts +++ b/src/tempo/TxEnvelopeTempo.test.ts @@ -174,6 +174,49 @@ describe('assert', () => { ) }) + test('invalid chain id (not a positive safe integer)', () => { + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: Number.NaN, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: 1.5, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: Number.MAX_SAFE_INTEGER + 1, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: '1' as never, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: 1n as never, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid fee payer signature', () => { const envelope = { calls: [{ to: '0x0000000000000000000000000000000000000000' }], diff --git a/src/tempo/TxEnvelopeTempo.ts b/src/tempo/TxEnvelopeTempo.ts index ef2b48ab..a1091b77 100644 --- a/src/tempo/TxEnvelopeTempo.ts +++ b/src/tempo/TxEnvelopeTempo.ts @@ -217,7 +217,10 @@ export function assert(envelope: PartialBy) { if (feeToken) Address.assert(feeToken, { strict: false }) // Validate chain ID - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) // Validate max fee per gas