Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

Commit

Permalink
fix: bundler URL validation (#262)
Browse files Browse the repository at this point in the history
* fix: add bundler url validation

* fix: update BundlerUrl validation

* fix: lint

* fix: update EthBaseUserOperationStruct test to use a baseUserOp

* fix: update url struct
  • Loading branch information
montelaidev authored Mar 6, 2024
1 parent a95c7f9 commit 2164741
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 4 deletions.
57 changes: 56 additions & 1 deletion src/eth/erc4337/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assert } from 'superstruct';

import { EthUserOperationStruct } from './types';
import { EthUserOperationStruct, EthBaseUserOperationStruct } from './types';

describe('types', () => {
it('is a valid UserOperation', () => {
Expand Down Expand Up @@ -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();
});
});
});
6 changes: 3 additions & 3 deletions src/eth/erc4337/types.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -59,7 +59,7 @@ export const EthBaseUserOperationStruct = object({
),
dummyPaymasterAndData: EthBytesStruct,
dummySignature: EthBytesStruct,
bundlerUrl: string(),
bundlerUrl: UrlStruct,
});

export type EthBaseUserOperation = Infer<typeof EthBaseUserOperationStruct>;
Expand Down
30 changes: 30 additions & 0 deletions src/eth/types.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
18 changes: 18 additions & 0 deletions src/superstruct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:';
});

0 comments on commit 2164741

Please sign in to comment.