Skip to content

Commit

Permalink
Convert CAIP-10 address validation to schema (#2110)
Browse files Browse the repository at this point in the history
* Don't allow empty/hex strings to be validated as numeric strings

* Convert CAIP-10 address validation to schema

---------

Co-authored-by: Hector Gómez Varela <[email protected]>
  • Loading branch information
iamacook and hectorgomezv authored Nov 14, 2024
1 parent 20e8be8 commit 83f24e9
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 137 deletions.
138 changes: 138 additions & 0 deletions src/routes/safes/entities/caip-10-addresses.entity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Caip10AddressesSchema } from '@/routes/safes/entities/caip-10-addresses.entity';
import { faker } from '@faker-js/faker';
import { getAddress } from 'viem';

describe('Caip10AddressesSchema', () => {
const caip10Addresses = faker.helpers.multiple(
() => {
return {
chainId: faker.string.numeric(),
address: faker.finance.ethereumAddress(),
};
},
{
count: { min: 1, max: 5 },
},
);

it('should parse CAIP-10 addresses', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${address.chainId}:${address.address}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(result.success).toBe(true);
});

it('should checksum addresses', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${address.chainId}:${address.address}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(
result.success && result.data.map(({ address }) => address),
).toStrictEqual(
caip10Addresses.map(({ address }) => {
return getAddress(address);
}),
);
});

it('should throw for incorrectly formatted CAIP-10 addresses', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${address.chainId}-${address.address}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(!result.success && result.error.issues).toStrictEqual(
Array.from({ length: caip10Addresses.length }).flatMap(() => [
{
code: 'custom',
message: 'Invalid base-10 numeric string',
path: ['chainId'],
},
{
code: 'invalid_type',
expected: 'string',
message: 'Required',
path: ['address'],
received: 'undefined',
},
]),
);
});

it('should throw for non-numerical chainIds', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${faker.string.hexadecimal()}:${address.address}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(!result.success && result.error.issues).toStrictEqual(
Array.from({ length: caip10Addresses.length }).map(() => ({
code: 'custom',
message: 'Invalid base-10 numeric string',
path: ['chainId'],
})),
);
});

it('should throw for invalid addresses', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${address.chainId}:${faker.string.numeric()}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(!result.success && result.error.issues).toStrictEqual(
Array.from({ length: caip10Addresses.length }).flatMap(() => [
{
code: 'custom',
message: 'Invalid address',
path: ['address'],
},
]),
);
});

it('should throw for missing chainIds', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `:${address.address}`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(!result.success && result.error.issues).toStrictEqual(
Array.from({ length: caip10Addresses.length }).flatMap(() => [
{
code: 'custom',
message: 'Invalid base-10 numeric string',
path: ['chainId'],
},
]),
);
});

it('should throw for missing addresses', () => {
const caip10AddressesString = caip10Addresses
.map((address) => `${address.chainId}:`)
.join(',');

const result = Caip10AddressesSchema.safeParse(caip10AddressesString);

expect(!result.success && result.error.issues).toStrictEqual(
Array.from({ length: caip10Addresses.length }).flatMap(() => [
{
code: 'custom',
message: 'Invalid address',
path: ['address'],
},
]),
);
});
});
32 changes: 32 additions & 0 deletions src/routes/safes/entities/caip-10-addresses.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod';
import { asError } from '@/logging/utils';
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema';

export const Caip10AddressesSchema = z.string().transform((str, ctx) => {
return str.split(',').map((item) => {
try {
const [chainId, address] = item.split(':');
return z
.object({
chainId: NumericStringSchema,
address: AddressSchema,
})
.parse({ chainId, address });
} catch (e) {
if (e instanceof z.ZodError) {
e.issues.forEach((issue) => {
ctx.addIssue(issue);
});
} else {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: asError(e).message,
});
}
return z.NEVER;
}
});
});

export type Caip10Addresses = z.infer<typeof Caip10AddressesSchema>;
101 changes: 0 additions & 101 deletions src/routes/safes/pipes/caip-10-addresses.pipe.spec.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/routes/safes/pipes/caip-10-addresses.pipe.ts

This file was deleted.

9 changes: 6 additions & 3 deletions src/routes/safes/safes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { SafeState } from '@/routes/safes/entities/safe-info.entity';
import { SafesService } from '@/routes/safes/safes.service';
import { SafeNonces } from '@/routes/safes/entities/nonces.entity';
import { SafeOverview } from '@/routes/safes/entities/safe-overview.entity';
import { Caip10AddressesPipe } from '@/routes/safes/pipes/caip-10-addresses.pipe';
import {
Caip10AddressesSchema,
type Caip10Addresses,
} from '@/routes/safes/entities/caip-10-addresses.entity';
import { ValidationPipe } from '@/validation/pipes/validation.pipe';
import { AddressSchema } from '@/validation/entities/schemas/address.schema';

Expand Down Expand Up @@ -50,8 +53,8 @@ export class SafesController {
@Get('safes')
async getSafeOverview(
@Query('currency') currency: string,
@Query('safes', new Caip10AddressesPipe())
addresses: Array<{ chainId: string; address: `0x${string}` }>,
@Query('safes', new ValidationPipe(Caip10AddressesSchema))
addresses: Caip10Addresses,
@Query('trusted', new DefaultValuePipe(false), ParseBoolPipe)
trusted: boolean,
@Query('exclude_spam', new DefaultValuePipe(true), ParseBoolPipe)
Expand Down
3 changes: 2 additions & 1 deletion src/routes/safes/safes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SafeOverview } from '@/routes/safes/entities/safe-overview.entity';
import { IConfigurationService } from '@/config/configuration.service.interface';
import { LoggingService, ILoggingService } from '@/logging/logging.interface';
import { asError } from '@/logging/utils';
import { Caip10Addresses } from '@/routes/safes/entities/caip-10-addresses.entity';

@Injectable()
export class SafesService {
Expand Down Expand Up @@ -129,7 +130,7 @@ export class SafesService {

async getSafeOverview(args: {
currency: string;
addresses: Array<{ chainId: string; address: `0x${string}` }>;
addresses: Caip10Addresses;
trusted: boolean;
excludeSpam: boolean;
walletAddress?: `0x${string}`;
Expand Down

0 comments on commit 83f24e9

Please sign in to comment.