diff --git a/src/chains/celo/formatters.test.ts b/src/chains/celo/formatters.test.ts index 408ae54fde..28b53da76a 100644 --- a/src/chains/celo/formatters.test.ts +++ b/src/chains/celo/formatters.test.ts @@ -819,7 +819,6 @@ describe('transactionRequest', () => { maxFeePerGas: 2n, maxPriorityFeePerGas: 1n, nonce: 1, - type: 'cip42', value: 1n, }), ).toMatchInlineSnapshot(` @@ -846,7 +845,6 @@ describe('transactionRequest', () => { maxFeePerGas: 2n, maxPriorityFeePerGas: 1n, nonce: 1, - type: 'cip64', value: 1n, }), ).toMatchInlineSnapshot(` @@ -862,5 +860,85 @@ describe('transactionRequest', () => { "value": "0x1", } `) + + expect( + transactionRequest.format({ + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // Recipient (illustrative address) + value: 1n, + feeCurrency: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', // cUSD fee currency + maxFeePerGas: 2n, // Special field for dynamic fee transaction type (EIP-1559) + maxPriorityFeePerGas: 2n, // Special field for dynamic fee transaction type (EIP-1559) + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": undefined, + "gasPrice": undefined, + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x2", + "nonce": undefined, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "0x7b", + "value": "0x1", + } + `) + + expect( + transactionRequest.format({ + feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + gas: 1n, + gatewayFee: 4n, + gatewayFeeRecipient: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + maxFeePerGas: 2n, + maxPriorityFeePerGas: 1n, + nonce: 1, + value: 1n, + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": "0x1", + "gasPrice": undefined, + "gatewayFee": "0x4", + "gatewayFeeRecipient": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x1", + "nonce": "0x1", + "type": "0x7c", + "value": "0x1", + } + `) + + expect( + transactionRequest.format({ + feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + gas: 1n, + gatewayFee: 4n, + gatewayFeeRecipient: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + maxFeePerGas: 2n, + maxPriorityFeePerGas: 4n, + nonce: 1, + value: 1n, + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": "0x1", + "gasPrice": undefined, + "gatewayFee": "0x4", + "gatewayFeeRecipient": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x4", + "nonce": "0x1", + "type": "0x7c", + "value": "0x1", + } + `) }) }) diff --git a/src/chains/celo/formatters.ts b/src/chains/celo/formatters.ts index dea192ed9b..4f9201d70d 100644 --- a/src/chains/celo/formatters.ts +++ b/src/chains/celo/formatters.ts @@ -19,22 +19,7 @@ import type { CeloTransactionReceiptOverrides, CeloTransactionRequest, } from './types.js' - -function isTransactionRequestCIP64(args: CeloTransactionRequest): boolean { - if (args.type === 'cip64') return true - if (args.type) return false - return ( - 'feeCurrency' in args && - args.gatewayFee === undefined && - args.gatewayFeeRecipient === undefined - ) -} - -function isTransactionRequestCIP42(args: CeloTransactionRequest): boolean { - if (args.type === 'cip42') return true - if (args.type) return false - return args.gatewayFee !== undefined || args.gatewayFeeRecipient !== undefined -} +import { isCIP42, isCIP64 } from './utils.js' export const formattersCelo = { block: /*#__PURE__*/ defineBlock({ @@ -95,7 +80,7 @@ export const formattersCelo = { transactionRequest: /*#__PURE__*/ defineTransactionRequest({ format(args: CeloTransactionRequest): CeloRpcTransactionRequest { - if (isTransactionRequestCIP64(args)) + if (isCIP64(args)) return { type: '0x7b', feeCurrency: args.feeCurrency, @@ -110,7 +95,7 @@ export const formattersCelo = { gatewayFeeRecipient: args.gatewayFeeRecipient, } as CeloRpcTransactionRequest - if (isTransactionRequestCIP42(args)) request.type = '0x7c' + if (isCIP42(args)) request.type = '0x7c' return request }, diff --git a/src/chains/celo/serializers.test.ts b/src/chains/celo/serializers.test.ts index e49ee283fb..8b729ae312 100644 --- a/src/chains/celo/serializers.test.ts +++ b/src/chains/celo/serializers.test.ts @@ -26,23 +26,22 @@ const commonBaseTx = { feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a', value: parseEther('1'), } + const baseCip42 = { ...commonBaseTx, - type: 'cip42', + gatewayFee: parseGwei('2'), + gatewayFeeRecipient: '0x0000000000000000000000000000000000000000', } as TransactionSerializableCIP42 const baseCip64 = { ...commonBaseTx, - type: 'cip64', + maxFeePerGas: parseGwei('2'), + maxPriorityFeePerGas: parseGwei('2'), } as TransactionSerializableCIP64 describe('cip42', () => { test('should be able to serialize a cip42 transaction', () => { - const transaction: TransactionSerializableCIP42 = { - ...baseCip42, - } - - expect(serializeTransactionCelo(transaction)).toEqual( + expect(serializeTransactionCelo(baseCip42)).toEqual( '0x7cf84682a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', ) }) @@ -443,15 +442,77 @@ describe('invalid params specific to CIP-64', () => { }) }) -describe.each([ - { typeName: 'CIP-42', baseTransaction: baseCip42 }, - { typeName: 'CIP-64', baseTransaction: baseCip64 }, -])('Common invalid params (for $typeName)', ({ typeName, baseTransaction }) => { +describe('Common invalid params (for CIP-42)', () => { + test('invalid to', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + to: '0xdeadbeef', + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + InvalidAddressError, + ) + }) + + test('gatewayFeeRecipient is not an address', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + // @ts-expect-error - (Type '"example"' is not assignable to type "`0x${string}"' + gatewayFeeRecipient: 'example', + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + TipAboveFeeCapError, + ) + }) + + test('maxFeePerGas is too high', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + FeeCapTooHighError, + ) + }) + + test('feeCurrency is not an address', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + // @ts-expect-error - (Type '"CUSD"' is not assignable to type "`0x${string}"' + feeCurrency: 'CUSD', + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + '`feeCurrency` MUST be a token address for CIP-42 transactions.', + ) + }) + + test('gasPrice is defined', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + // @ts-expect-error + gasPrice: BigInt(1), + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + '`gasPrice` is not a valid CIP-42 Transaction attribute.', + ) + }) + + test('chainID is invalid', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + chainId: -1, + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + `Chain ID "${-1}" is invalid.`, + ) + }) +}) + +describe('Common invalid params (for CIP-64)', () => { test('invalid to', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, to: '0xdeadbeef', } expect(() => serializeTransactionCelo(transaction)).toThrowError( @@ -460,10 +521,8 @@ describe.each([ }) test('maxPriorityFeePerGas is higher than maxPriorityFee', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, maxPriorityFeePerGas: parseGwei('5000000000'), maxFeePerGas: parseGwei('1'), } @@ -473,10 +532,8 @@ describe.each([ }) test('maxFeePerGas is too high', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, maxPriorityFeePerGas: parseGwei('5000000000'), maxFeePerGas: 115792089237316195423570985008687907853269984665640564039457584007913129639938n, @@ -487,38 +544,32 @@ describe.each([ }) test('feeCurrency is not an address', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, // @ts-expect-error feeCurrency: 'CUSD', } expect(() => serializeTransactionCelo(transaction)).toThrowError( - `\`feeCurrency\` MUST be a token address for ${typeName} transactions.`, + '`feeCurrency` MUST be a token address for CIP-64 transactions.', ) }) test('gasPrice is defined', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, // @ts-expect-error gasPrice: BigInt(1), } expect(() => serializeTransactionCelo(transaction)).toThrowError( - `\`gasPrice\` is not a valid ${typeName} Transaction attribute.`, + '`gasPrice` is not a valid CIP-64 Transaction attribute.', ) }) test('chainID is invalid', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, chainId: -1, } diff --git a/src/chains/celo/serializers.ts b/src/chains/celo/serializers.ts index 4414e8bae0..17dcecad8c 100644 --- a/src/chains/celo/serializers.ts +++ b/src/chains/celo/serializers.ts @@ -22,20 +22,15 @@ import type { TransactionSerializedCIP42, TransactionSerializedCIP64, } from './types.js' +import { isCIP42, isCIP64 } from './utils.js' export const serializeTransactionCelo: SerializeTransactionFn< CeloTransactionSerializable > = (tx, signature) => { if (isCIP64(tx)) { - return serializeTransactionCIP64( - tx as TransactionSerializableCIP64, - signature, - ) + return serializeTransactionCIP64(tx, signature) } else if (isCIP42(tx)) { - return serializeTransactionCIP42( - tx as TransactionSerializableCIP42, - signature, - ) + return serializeTransactionCIP42(tx, signature) } else { return serializeTransaction(tx as TransactionSerializable, signature) } @@ -148,43 +143,8 @@ function serializeTransactionCIP64( ]) as SerializeTransactionCIP64ReturnType } -////////////////////////////////////////////////////////////////////////////// -// Utilities - -// process as CIP42 if any of these fields are present. realistically gatewayfee is not used but is part of spec -function isCIP42(transaction: CeloTransactionSerializable): boolean { - if (transaction.type === 'cip42') return true - // if the type is defined as anything else, assume it is *not* cip42 - if (transaction.type) return false - - // if the type is undefined, check if the fields match the expectations for cip42 - return ( - 'maxFeePerGas' in transaction && - 'maxPriorityFeePerGas' in transaction && - ('feeCurrency' in transaction || - 'gatewayFee' in transaction || - 'gatewayFeeRecipient' in transaction) - ) -} - -function isCIP64(transaction: CeloTransactionSerializable): boolean { - if (transaction.type === 'cip64') return true - // if the type is defined as anything else, assume it is *not* cip64 - if (transaction.type) return false - - // if the type is undefined, check if the fields match the expectations for cip64 - return ( - 'maxFeePerGas' in transaction && - 'maxPriorityFeePerGas' in transaction && - 'feeCurrency' in transaction && - !('gatewayFee' in transaction) && - !('gatewayFeeRecipient' in transaction) - ) -} - -// maxFeePerGas must be less than 2^256 - 1: however writing like that caused exceptions to be raised -const MAX_MAX_FEE_PER_GAS = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n +// maxFeePerGas must be less than 2^256 - 1 +const MAX_MAX_FEE_PER_GAS = 2n ** 256n - 1n export function assertTransactionCIP42( transaction: TransactionSerializableCIP42, diff --git a/src/chains/celo/types.ts b/src/chains/celo/types.ts index 46cbfc1111..10b727a33f 100644 --- a/src/chains/celo/types.ts +++ b/src/chains/celo/types.ts @@ -233,7 +233,6 @@ export type TransactionSerializableCIP42< > = TransactionSerializableBase & FeeValuesEIP1559 & { accessList?: AccessList - gasPrice?: never feeCurrency?: Address gatewayFeeRecipient?: Address gatewayFee?: TQuantity @@ -247,7 +246,6 @@ export type TransactionSerializableCIP64< > = TransactionSerializableBase & FeeValuesEIP1559 & { accessList?: AccessList - gasPrice?: never feeCurrency?: Address chainId: number type?: 'cip64' diff --git a/src/chains/celo/utils.ts b/src/chains/celo/utils.ts new file mode 100644 index 0000000000..b142f5ed1d --- /dev/null +++ b/src/chains/celo/utils.ts @@ -0,0 +1,66 @@ +////////////////////////////////////////////////////////////////////////////// +// Utilities + +import type { + CeloTransactionRequest, + CeloTransactionSerializable, + TransactionSerializableCIP42, + TransactionSerializableCIP64, +} from './types.js' + +function isEmpty(value: string | undefined | number | BigInt) { + return ( + value === 0 || + value === 0n || + value === undefined || + value === null || + value === '0' || + value === '' || + (typeof value === 'string' && + (value.toLowerCase() === '0x' || value.toLowerCase() === '0x0')) + ) +} + +function isPresent(value: string | undefined | number | BigInt) { + return !isEmpty(value) +} + +function isEIP1559( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): boolean { + return ( + isPresent(transaction.maxFeePerGas) && + isPresent(transaction.maxPriorityFeePerGas) + ) +} + +// process as CIP42 if any of these fields are present. realistically gatewayfee is not used but is part of spec +export function isCIP42( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): transaction is TransactionSerializableCIP42 { + // if the type is undefined, check if the fields match the expectations for cip42 + return ( + isEIP1559(transaction) && + (isPresent((transaction as TransactionSerializableCIP42).feeCurrency) || + isPresent( + (transaction as TransactionSerializableCIP42).gatewayFeeRecipient, + ) || + isPresent((transaction as TransactionSerializableCIP42).gatewayFee)) + ) +} + +export function isCIP64( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): transaction is TransactionSerializableCIP64 { + // if the type is undefined, check if the fields match the expectations for cip64 + return ( + isEIP1559(transaction) && + isPresent((transaction as TransactionSerializableCIP64).feeCurrency) && + !isPresent( + (transaction as TransactionSerializableCIP42).gatewayFeeRecipient, + ) && + !isPresent( + (transaction as TransactionSerializableCIP42).gatewayFeeRecipient, + ) + ) +}