-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add datasource for Safe Decoder Service
- Loading branch information
Showing
18 changed files
with
1,190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/datasources/data-decoder-api/data-decoder-api.manager.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { DataDecoderApiManager } from '@/datasources/data-decoder-api/data-decoder-api.manager'; | ||
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory'; | ||
import { IDataDecoderApiManager } from '@/domain/interfaces/data-decoder-api.manager.interface'; | ||
|
||
@Module({ | ||
providers: [ | ||
{ | ||
provide: IDataDecoderApiManager, | ||
useClass: DataDecoderApiManager, | ||
}, | ||
HttpErrorFactory, | ||
], | ||
exports: [IDataDecoderApiManager], | ||
}) | ||
export class DataDecoderApiManagerModule {} |
51 changes: 51 additions & 0 deletions
51
src/datasources/data-decoder-api/data-decoder-api.manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import { IConfigurationService } from '@/config/configuration.service.interface'; | ||
import { DataDecoderApi } from '@/datasources/data-decoder-api/data-decoder-api.service'; | ||
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory'; | ||
import { | ||
NetworkService, | ||
INetworkService, | ||
} from '@/datasources/network/network.service.interface'; | ||
import { IDataDecoderApiManager } from '@/domain/interfaces/data-decoder-api.manager.interface'; | ||
|
||
@Injectable() | ||
export class DataDecoderApiManager implements IDataDecoderApiManager { | ||
private decoderApiMap: Record<string, DataDecoderApi> = {}; | ||
|
||
private readonly baseUri: string; | ||
|
||
constructor( | ||
@Inject(IConfigurationService) | ||
private readonly configurationService: IConfigurationService, | ||
private readonly httpErrorFactory: HttpErrorFactory, | ||
@Inject(NetworkService) | ||
private readonly networkService: INetworkService, | ||
) { | ||
this.baseUri = this.configurationService.getOrThrow<string>( | ||
'safeDataDecoder.baseUri', | ||
); | ||
} | ||
|
||
async getApi(chainId: string): Promise<DataDecoderApi> { | ||
const decoderApi = this.decoderApiMap[chainId]; | ||
|
||
if (decoderApi !== undefined) { | ||
return Promise.resolve(decoderApi); | ||
} | ||
|
||
this.decoderApiMap[chainId] = new DataDecoderApi( | ||
chainId, | ||
this.baseUri, | ||
this.networkService, | ||
this.httpErrorFactory, | ||
); | ||
|
||
return Promise.resolve(this.decoderApiMap[chainId]); | ||
} | ||
|
||
// We don't need to destroy the API as it is not event-specific | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
destroyApi(_: string): void { | ||
throw new Error('Method not implemented'); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/datasources/data-decoder-api/data-decoder-api.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { DataDecoderApiManager } from '@/datasources/data-decoder-api/data-decoder-api.manager'; | ||
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory'; | ||
import { IDataDecoderApiManager } from '@/domain/interfaces/data-decoder-api.manager.interface'; | ||
|
||
@Module({ | ||
providers: [ | ||
HttpErrorFactory, | ||
{ provide: IDataDecoderApiManager, useClass: DataDecoderApiManager }, | ||
], | ||
exports: [IDataDecoderApiManager], | ||
}) | ||
export class DataDecodedApiModule {} |
163 changes: 163 additions & 0 deletions
163
src/datasources/data-decoder-api/data-decoder-api.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { faker } from '@faker-js/faker'; | ||
import { getAddress } from 'viem'; | ||
import { DataDecoderApi } from '@/datasources/data-decoder-api/data-decoder-api.service'; | ||
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory'; | ||
import { dataDecodedBuilder } from '@/domain/data-decoder/v2/entities/__tests__/data-decoded.builder'; | ||
import { contractBuilder } from '@/domain/data-decoder/v2/entities/__tests__/contract.builder'; | ||
import { pageBuilder } from '@/domain/entities/__tests__/page.builder'; | ||
import { DataSourceError } from '@/domain/errors/data-source.error'; | ||
import { NetworkResponseError } from '@/datasources/network/entities/network.error.entity'; | ||
import { rawify } from '@/validation/entities/raw.entity'; | ||
import type { INetworkService } from '@/datasources/network/network.service.interface'; | ||
|
||
const mockNetworkService = jest.mocked({ | ||
get: jest.fn(), | ||
post: jest.fn(), | ||
} as jest.MockedObjectDeep<INetworkService>); | ||
|
||
describe('DataDecoderApi', () => { | ||
const baseUrl = faker.internet.url({ appendSlash: false }); | ||
let chainId: string; | ||
let target: DataDecoderApi; | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
|
||
const httpErrorFactory = new HttpErrorFactory(); | ||
chainId = faker.string.numeric(); | ||
target = new DataDecoderApi( | ||
chainId, | ||
baseUrl, | ||
mockNetworkService, | ||
httpErrorFactory, | ||
); | ||
}); | ||
|
||
describe('getDataDecoded', () => { | ||
it('should return the decoded data', async () => { | ||
const dataDecoded = dataDecodedBuilder().build(); | ||
const to = getAddress(faker.finance.ethereumAddress()); | ||
const data = faker.string.hexadecimal() as `0x${string}`; | ||
const getDataDecodedUrl = `${baseUrl}/api/v1/data-decoder`; | ||
mockNetworkService.post.mockImplementation(({ url }) => { | ||
if (url === getDataDecodedUrl) { | ||
return Promise.resolve({ status: 200, data: rawify(dataDecoded) }); | ||
} | ||
throw new Error('Unexpected URL'); | ||
}); | ||
|
||
const actual = await target.getDecodedData({ data, to }); | ||
|
||
expect(actual).toStrictEqual(dataDecoded); | ||
expect(mockNetworkService.post).toHaveBeenCalledTimes(1); | ||
expect(mockNetworkService.post).toHaveBeenCalledWith({ | ||
url: getDataDecodedUrl, | ||
data: { chainId: Number(chainId), to, data }, | ||
}); | ||
}); | ||
|
||
it('should forward an error', async () => { | ||
const to = getAddress(faker.finance.ethereumAddress()); | ||
const data = faker.string.hexadecimal() as `0x${string}`; | ||
const errorMessage = faker.word.words(); | ||
const statusCode = faker.internet.httpStatusCode({ | ||
types: ['clientError', 'serverError'], | ||
}); | ||
const expected = new DataSourceError(errorMessage, statusCode); | ||
const getDataDecodedUrl = `${baseUrl}/api/v1/data-decoder`; | ||
mockNetworkService.post.mockImplementation(({ url }) => { | ||
if (url === getDataDecodedUrl) { | ||
return Promise.reject( | ||
new NetworkResponseError( | ||
new URL(getDataDecodedUrl), | ||
{ | ||
status: statusCode, | ||
} as Response, | ||
new Error(errorMessage), | ||
), | ||
); | ||
} | ||
throw new Error('Unexpected URL'); | ||
}); | ||
|
||
await expect(target.getDecodedData({ data, to })).rejects.toThrow( | ||
expected, | ||
); | ||
|
||
expect(mockNetworkService.post).toHaveBeenCalledTimes(1); | ||
expect(mockNetworkService.post).toHaveBeenCalledWith({ | ||
url: getDataDecodedUrl, | ||
data: { chainId: Number(chainId), to, data }, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getContracts', () => { | ||
it('should return the contracts', async () => { | ||
const contract = contractBuilder().build(); | ||
const contractPage = pageBuilder().with('results', [contract]).build(); | ||
const getContractsUrl = `${baseUrl}/api/v1/contracts/${contract.address}`; | ||
mockNetworkService.get.mockImplementation(({ url }) => { | ||
if (url === getContractsUrl) { | ||
return Promise.resolve({ status: 200, data: rawify(contractPage) }); | ||
} | ||
throw new Error('Unexpected URL'); | ||
}); | ||
|
||
const actual = await target.getContracts({ address: contract.address }); | ||
|
||
expect(actual).toStrictEqual(contractPage); | ||
expect(mockNetworkService.get).toHaveBeenCalledTimes(1); | ||
expect(mockNetworkService.get).toHaveBeenCalledWith({ | ||
url: getContractsUrl, | ||
networkRequest: { | ||
params: { | ||
chain_ids: Number(chainId), | ||
limit: undefined, | ||
offset: undefined, | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should forward an error', async () => { | ||
const contract = contractBuilder().build(); | ||
const errorMessage = faker.word.words(); | ||
const statusCode = faker.internet.httpStatusCode({ | ||
types: ['clientError', 'serverError'], | ||
}); | ||
const expected = new DataSourceError(errorMessage, statusCode); | ||
const getContractsUrl = `${baseUrl}/api/v1/contracts/${contract.address}`; | ||
mockNetworkService.get.mockImplementation(({ url }) => { | ||
if (url === getContractsUrl) { | ||
return Promise.reject( | ||
new NetworkResponseError( | ||
new URL(getContractsUrl), | ||
{ | ||
status: statusCode, | ||
} as Response, | ||
new Error(errorMessage), | ||
), | ||
); | ||
} | ||
throw new Error('Unexpected URL'); | ||
}); | ||
|
||
await expect( | ||
target.getContracts({ address: contract.address }), | ||
).rejects.toThrow(expected); | ||
|
||
expect(mockNetworkService.get).toHaveBeenCalledTimes(1); | ||
expect(mockNetworkService.get).toHaveBeenCalledWith({ | ||
url: getContractsUrl, | ||
networkRequest: { | ||
params: { | ||
chain_ids: Number(chainId), | ||
limit: undefined, | ||
offset: undefined, | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); |
63 changes: 63 additions & 0 deletions
63
src/datasources/data-decoder-api/data-decoder-api.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import type { HttpErrorFactory } from '@/datasources/errors/http-error-factory'; | ||
import type { INetworkService } from '@/datasources/network/network.service.interface'; | ||
import type { DataDecoded } from '@/domain/data-decoder/v2/entities/data-decoded.entity'; | ||
import type { Contract } from '@/domain/data-decoder/v2/entities/contract.entity'; | ||
import type { Page } from '@/domain/entities/page.entity'; | ||
import type { IDataDecoderApi } from '@/domain/interfaces/data-decoder-api.interface'; | ||
import type { Raw } from '@/validation/entities/raw.entity'; | ||
|
||
export class DataDecoderApi implements IDataDecoderApi { | ||
constructor( | ||
private readonly chainId: string, | ||
private readonly baseUrl: string, | ||
private readonly networkService: INetworkService, | ||
private readonly httpErrorFactory: HttpErrorFactory, | ||
) {} | ||
|
||
public async getDecodedData(args: { | ||
data: `0x${string}`; | ||
to: `0x${string}`; | ||
}): Promise<Raw<DataDecoded>> { | ||
try { | ||
const url = `${this.baseUrl}/api/v1/data-decoder`; | ||
const { data: dataDecoded } = await this.networkService.post<DataDecoded>( | ||
{ | ||
url, | ||
data: { | ||
chainId: Number(this.chainId), | ||
to: args.to, | ||
data: args.data, | ||
}, | ||
}, | ||
); | ||
return dataDecoded; | ||
} catch (error) { | ||
throw this.httpErrorFactory.from(error); | ||
} | ||
} | ||
|
||
public async getContracts(args: { | ||
address: `0x${string}`; | ||
limit?: number; | ||
offset?: number; | ||
}): Promise<Raw<Page<Contract>>> { | ||
try { | ||
const url = `${this.baseUrl}/api/v1/contracts/${args.address}`; | ||
const { data: contracts } = await this.networkService.get<Page<Contract>>( | ||
{ | ||
url, | ||
networkRequest: { | ||
params: { | ||
chain_ids: Number(this.chainId), | ||
limit: args.limit, | ||
offset: args.offset, | ||
}, | ||
}, | ||
}, | ||
); | ||
return contracts; | ||
} catch (error) { | ||
throw this.httpErrorFactory.from(error); | ||
} | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/domain/data-decoder/v2/data-decoder.repository.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Contract } from '@/domain/data-decoder/v2/entities/contract.entity'; | ||
import type { DataDecoded } from '@/domain/data-decoder/v2/entities/data-decoded.entity'; | ||
import type { Page } from '@/domain/entities/page.entity'; | ||
|
||
export const IDataDecoderRepository = Symbol('IDataDecoderRepository'); | ||
|
||
export interface IDataDecoderRepository { | ||
getDecodedData(args: { | ||
chainId: string; | ||
data: `0x${string}`; | ||
to: `0x${string}`; | ||
}): Promise<DataDecoded>; | ||
|
||
getContracts(args: { | ||
chainId: string; | ||
address: `0x${string}`; | ||
}): Promise<Page<Contract>>; | ||
} |
16 changes: 16 additions & 0 deletions
16
src/domain/data-decoder/v2/data-decoder.repository.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { DataDecoderApiManagerModule } from '@/datasources/data-decoder-api/data-decoder-api.manager.module'; | ||
import { DataDecoderRepository } from '@/domain/data-decoder/v2/data-decoder.repository'; | ||
import { IDataDecoderRepository } from '@/domain/data-decoder/v2/data-decoder.repository.interface'; | ||
|
||
@Module({ | ||
imports: [DataDecoderApiManagerModule], | ||
providers: [ | ||
{ | ||
provide: IDataDecoderRepository, | ||
useClass: DataDecoderRepository, | ||
}, | ||
], | ||
exports: [IDataDecoderRepository], | ||
}) | ||
export class DataDecoderRepositoryModule {} |
Oops, something went wrong.