diff --git a/src/eth/erc4337/types.test.ts b/src/eth/erc4337/types.test.ts index 50175e498..b7c065d76 100644 --- a/src/eth/erc4337/types.test.ts +++ b/src/eth/erc4337/types.test.ts @@ -1,6 +1,6 @@ import { assert } from 'superstruct'; -import { EthUserOperationStruct } from './types'; +import { EthUserOperationStruct, EthBaseUserOperationStruct } from './types'; describe('types', () => { it('is a valid UserOperation', () => { @@ -76,4 +76,59 @@ describe('types', () => { 'At path: nonce -- Expected a value of type `EthUint256`, but received: `"0x01"`', ); }); + + describe('EthBaseUserOperationStruct', () => { + const baseUserOp = { + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + gasLimits: { + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + }, + dummyPaymasterAndData: '0x1234', + dummySignature: '0x1234', + }; + + it('is a valid BaseUserOperation', () => { + const userOp = { + ...baseUserOp, + bundlerUrl: 'https://example.com', + }; + expect(() => assert(userOp, EthBaseUserOperationStruct)).not.toThrow(); + }); + + it('has an invalid BaseUserOperation with an incorrect url string', () => { + const userOp = { + ...baseUserOp, + bundlerUrl: 'random string', + }; + expect(() => assert(userOp, EthBaseUserOperationStruct)).toThrow( + 'At path: bundlerUrl -- Expected a value of type `Url`, but received: `"random string"`', + ); + }); + + it('cannot have an empty bundler url', () => { + const userOp = { + ...baseUserOp, + bundlerUrl: '', + }; + expect(() => assert(userOp, EthBaseUserOperationStruct)).toThrow( + 'At path: bundlerUrl -- Expected a value of type `Url`, but received: `""`', + ); + }); + + it('does not throw if gasLimits are undefined', () => { + const userOp = { + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + dummyPaymasterAndData: '0x1234', + dummySignature: '0x1234', + bundlerUrl: 'https://example.com', + }; + expect(() => assert(userOp, EthBaseUserOperationStruct)).not.toThrow(); + }); + }); }); diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index 75afdc037..b1376ac67 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -1,6 +1,6 @@ -import { string, type Infer } from 'superstruct'; +import { type Infer } from 'superstruct'; -import { exactOptional, object } from '../../superstruct'; +import { UrlStruct, exactOptional, object } from '../../superstruct'; import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types'; /** @@ -59,7 +59,7 @@ export const EthBaseUserOperationStruct = object({ ), dummyPaymasterAndData: EthBytesStruct, dummySignature: EthBytesStruct, - bundlerUrl: string(), + bundlerUrl: UrlStruct, }); export type EthBaseUserOperation = Infer; diff --git a/src/eth/types.test.ts b/src/eth/types.test.ts new file mode 100644 index 000000000..7a6c74846 --- /dev/null +++ b/src/eth/types.test.ts @@ -0,0 +1,30 @@ +import { UrlStruct } from '../superstruct'; + +describe('types', () => { + it('is a valid BundlerUrl', () => { + const url = 'https://api.example.com'; + expect(() => UrlStruct.assert(url)).not.toThrow(); + }); + + it('is a valid BundlerUrl with query parameters', () => { + const url = 'https://api.example.com?foo=bar'; + expect(() => UrlStruct.assert(url)).not.toThrow(); + }); + + it('accepts path parameters', () => { + const url = 'https://api.example.com/foo/bar'; + expect(() => UrlStruct.assert(url)).not.toThrow(); + }); + + it('fails if it does not start with http or https', () => { + const url = 'ftp://api.example.com'; + expect(() => UrlStruct.assert(url)).toThrow( + 'Expected a value of type `Url`, but received: `"ftp://api.example.com"`', + ); + }); + + it('has to start with http or https', () => { + const url = 'http://api.example.com'; + expect(() => UrlStruct.assert(url)).not.toThrow(); + }); +}); diff --git a/src/superstruct.ts b/src/superstruct.ts index a3c6e0c1f..0bb8e6568 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -125,3 +125,21 @@ export function definePattern( typeof value === 'string' && pattern.test(value), ); } + +/** + * Validates if a given value is a valid URL. + * + * @param value - The value to be validated. + * @returns A boolean indicating if the value is a valid URL. + */ +export const UrlStruct = define('Url', (value: unknown) => { + let url; + + try { + url = new URL(value as string); + } catch (_) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; +});