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

fix: bundler URL validation #262

Merged
merged 6 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 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,73 @@ describe('types', () => {
'At path: nonce -- Expected a value of type `EthUint256`, but received: `"0x01"`',
);
});

describe('EthBaseUserOperationStruct', () => {
it('is a valid BaseUserOperation', () => {
const userOp = {
ccharly marked this conversation as resolved.
Show resolved Hide resolved
nonce: '0x1',
initCode: '0x',
callData: '0x70641a22000000000000000000000000',
gasLimits: {
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
},
dummyPaymasterAndData: '0x1234',
dummySignature: '0x1234',
bundlerUrl: 'https://example.com',
};
expect(() => assert(userOp, EthBaseUserOperationStruct)).not.toThrow();
});

it('has an invalid BaseUserOperation with an incorrect url string', () => {
const userOp = {
nonce: '0x1',
initCode: '0x',
callData: '0x70641a22000000000000000000000000',
gasLimits: {
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
},
dummyPaymasterAndData: '0x1234',
dummySignature: '0x1234',
bundlerUrl: 'random string',
};
expect(() => assert(userOp, EthBaseUserOperationStruct)).toThrow(
'At path: bundlerUrl -- Expected a value of type `string`, but received: `"random string"`',
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
);
});

it('cannot have an empty bundler url', () => {
const userOp = {
nonce: '0x1',
initCode: '0x',
callData: '0x70641a22000000000000000000000000',
gasLimits: {
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
},
dummyPaymasterAndData: '0x1234',
dummySignature: '0x1234',
bundlerUrl: '',
};
expect(() => assert(userOp, EthBaseUserOperationStruct)).toThrow(
'At path: bundlerUrl -- Expected a value of type `string`, 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();
});
});
});
11 changes: 8 additions & 3 deletions src/eth/erc4337/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { string, type Infer } from 'superstruct';
import { type Infer } from 'superstruct';

import { exactOptional, object } from '../../superstruct';
import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types';
import {
BundlerUrlStruct,
EthAddressStruct,
EthBytesStruct,
EthUint256Struct,
} from '../types';

/**
* Struct of a UserOperation as defined by ERC-4337.
Expand Down Expand Up @@ -59,7 +64,7 @@ export const EthBaseUserOperationStruct = object({
),
dummyPaymasterAndData: EthBytesStruct,
dummySignature: EthBytesStruct,
bundlerUrl: string(),
bundlerUrl: BundlerUrlStruct,
});

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 { BundlerUrlStruct } from './types';

describe('types', () => {
it('is a valid BundlerUrl', () => {
const url = 'https://api.example.com';
expect(() => BundlerUrlStruct.assert(url)).not.toThrow();
});

it('is a valid BundlerUrl with query parameters', () => {
const url = 'https://api.example.com?foo=bar';
expect(() => BundlerUrlStruct.assert(url)).not.toThrow();
});

it('accepts path parameters', () => {
const url = 'https://api.example.com/foo/bar';
expect(() => BundlerUrlStruct.assert(url)).not.toThrow();
});

it('fails if it does not start with http or https', () => {
const url = 'ftp://api.example.com';
expect(() => BundlerUrlStruct.assert(url)).toThrow(
'Expected a value of type `string`, but received: `"ftp://api.example.com"`',
);
});

it('has to start with http or https', () => {
const url = 'http://api.example.com';
expect(() => BundlerUrlStruct.assert(url)).not.toThrow();
});
});
18 changes: 18 additions & 0 deletions src/eth/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { refine, string } from 'superstruct';

import { definePattern } from '../superstruct';

export const EthBytesStruct = definePattern('EthBytes', /^0x[0-9a-f]*$/iu);
Expand All @@ -11,3 +13,19 @@ export const EthUint256Struct = definePattern(
'EthUint256',
/^0x([1-9a-f][0-9a-f]*|0)$/iu,
);

export const BundlerUrlStruct = refine(
string(),
'BundlerUrl',
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
(value: string) => {
let url;

try {
url = new URL(value);
} catch (_) {
return false;
}

return url.protocol === 'http:' || url.protocol === 'https:';
},
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
);
Loading