Skip to content

Commit ac19d97

Browse files
authored
Revert "fix: decoder service integration (#2589) (#2593) (#2595)" (#2596)
This PR adds the decoder service integration again. This reverts commit 0e38096.
1 parent 0e38096 commit ac19d97

File tree

47 files changed

+1817
-826
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1817
-826
lines changed

src/datasources/cache/cache.first.data.source.spec.ts

Lines changed: 529 additions & 229 deletions
Large diffs are not rendered by default.

src/datasources/cache/cache.first.data.source.ts

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,71 @@ export class CacheFirstDataSource {
7171
notFoundExpireTimeSeconds: number;
7272
networkRequest?: NetworkRequest;
7373
expireTimeSeconds?: number;
74+
}): Promise<Raw<T>> {
75+
return await this.tryCache({
76+
...args,
77+
queryFn: () => {
78+
return this._getFromNetworkAndWriteCache({
79+
...args,
80+
method: 'get',
81+
});
82+
},
83+
});
84+
}
85+
86+
/**
87+
* Gets the cached value behind {@link CacheDir}.
88+
* If the value is not present, it tries to get the respective JSON
89+
* payload from {@link url}.
90+
* 404 errors are cached with {@link notFoundExpireTimeSeconds} seconds expiration time.
91+
*
92+
* @param args.cacheDir - {@link CacheDir} containing the key and field to be used to retrieve from cache
93+
* @param args.url - the HTTP endpoint to retrieve the JSON payload
94+
* @param args.networkRequest - the HTTP request to be used if there is a cache miss
95+
* @param args.expireTimeSeconds - the time to live in seconds for the payload behind {@link CacheDir}
96+
* @param args.notFoundExpireTimeSeconds - the time to live in seconds for the error when the item is not found
97+
* @param args.data - the data to be sent in the body of the request
98+
*/
99+
async post<T>(args: {
100+
cacheDir: CacheDir;
101+
url: string;
102+
notFoundExpireTimeSeconds: number;
103+
networkRequest?: NetworkRequest;
104+
expireTimeSeconds?: number;
105+
data: object;
106+
}): Promise<Raw<T>> {
107+
return await this.tryCache({
108+
...args,
109+
queryFn: () => {
110+
return this._getFromNetworkAndWriteCache({
111+
...args,
112+
method: 'post',
113+
});
114+
},
115+
});
116+
}
117+
118+
/**
119+
* Gets the cached value behind {@link CacheDir}.
120+
* If the value is not present, it tries to get the respective JSON
121+
* payload from {@link queryFn}.
122+
* 404 errors are cached with {@link notFoundExpireTimeSeconds} seconds expiration time.
123+
*
124+
* @param args.cacheDir - {@link CacheDir} containing the key and field to be used to retrieve from cache
125+
* @param args.notFoundExpireTimeSeconds - the time to live in seconds for the error when the item is not found
126+
* @param args.fn - the function to be executed if the cache entry is not found
127+
* @returns the cached value or the result of the function
128+
*/
129+
private async tryCache<T>(args: {
130+
cacheDir: CacheDir;
131+
notFoundExpireTimeSeconds: number;
132+
queryFn: () => Promise<Raw<T>>;
74133
}): Promise<Raw<T>> {
75134
const cached = await this.cacheService.hGet(args.cacheDir);
76135
if (cached != null) return this._getFromCachedData(args.cacheDir, cached);
77136

78137
try {
79-
return await this._getFromNetworkAndWriteCache(args);
138+
return await args.queryFn();
80139
} catch (error) {
81140
if (
82141
error instanceof NetworkResponseError &&
@@ -111,20 +170,34 @@ export class CacheFirstDataSource {
111170
}
112171

113172
/**
114-
* Gets the data from the network and caches the result.
173+
* Gets/posts the data from the network and caches the result.
115174
*/
116-
private async _getFromNetworkAndWriteCache<T>(args: {
117-
cacheDir: CacheDir;
118-
url: string;
119-
networkRequest?: NetworkRequest;
120-
expireTimeSeconds?: number;
121-
}): Promise<Raw<T>> {
175+
private async _getFromNetworkAndWriteCache<T>(
176+
args:
177+
| {
178+
cacheDir: CacheDir;
179+
url: string;
180+
networkRequest?: NetworkRequest;
181+
expireTimeSeconds?: number;
182+
method: 'get';
183+
data?: never;
184+
}
185+
| {
186+
cacheDir: CacheDir;
187+
url: string;
188+
networkRequest?: NetworkRequest;
189+
expireTimeSeconds?: number;
190+
method: 'post';
191+
data: object;
192+
},
193+
): Promise<Raw<T>> {
122194
const { key, field } = args.cacheDir;
123195
this.loggingService.debug({ type: LogType.CacheMiss, key, field });
124196
const startTimeMs = Date.now();
125-
const { data } = await this.networkService.get<T>({
197+
const { data } = await this.networkService[args.method]<T>({
126198
url: args.url,
127199
networkRequest: args.networkRequest,
200+
data: args.data,
128201
});
129202

130203
const shouldBeCached = await this._shouldBeCached(key, startTimeMs);

src/datasources/cache/cache.router.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export class CacheRouter {
1616
private static readonly COUNTERFACTUAL_SAFE_KEY = 'counterfactual_safe';
1717
private static readonly COUNTERFACTUAL_SAFES_KEY = 'counterfactual_safes';
1818
private static readonly CREATION_TRANSACTION_KEY = 'creation_transaction';
19+
private static readonly DECODED_DATA_KEY = 'decoded_data';
20+
private static readonly DECODED_DATA_CONTRACTS_KEY = 'decoded_data_contracts';
1921
private static readonly DELEGATES_KEY = 'delegates';
2022
private static readonly FIREBASE_OAUTH2_TOKEN_KEY = 'firebase_oauth2_token';
2123
private static readonly INCOMING_TRANSFERS_KEY = 'incoming_transfers';
@@ -387,6 +389,43 @@ export class CacheRouter {
387389
);
388390
}
389391

392+
static getDecodedDataCacheKey(args: {
393+
chainId: string;
394+
data: `0x${string}`;
395+
to: `0x${string}`;
396+
}): string {
397+
return `${args.chainId}_${CacheRouter.DECODED_DATA_KEY}_${args.data}_${args.to}`;
398+
}
399+
400+
static getDecodedDataCacheDir(args: {
401+
chainId: string;
402+
data: `0x${string}`;
403+
to: `0x${string}`;
404+
}): CacheDir {
405+
return new CacheDir(CacheRouter.getDecodedDataCacheKey(args), '');
406+
}
407+
408+
static getDecodedDataContractsCacheKey(args: {
409+
chainIds: Array<string>;
410+
address: `0x${string}`;
411+
limit?: number;
412+
offset?: number;
413+
}): string {
414+
return `${args.chainIds.sort().join('_')}_${CacheRouter.DECODED_DATA_CONTRACTS_KEY}_${args.address}`;
415+
}
416+
417+
static getDecodedDataContractsCacheDir(args: {
418+
chainIds: Array<string>;
419+
address: `0x${string}`;
420+
limit?: number;
421+
offset?: number;
422+
}): CacheDir {
423+
return new CacheDir(
424+
CacheRouter.getDecodedDataContractsCacheKey(args),
425+
`${args.limit}_${args.offset}`,
426+
);
427+
}
428+
390429
static getAllTransactionsCacheDir(args: {
391430
chainId: string;
392431
safeAddress: `0x${string}`;

src/datasources/data-decoder-api/data-decoder-api.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
22
import { DataDecoderApi } from '@/datasources/data-decoder-api/data-decoder-api.service';
33
import { HttpErrorFactory } from '@/datasources/errors/http-error-factory';
44
import { IDataDecoderApi } from '@/domain/interfaces/data-decoder-api.interface';
5+
import { CacheFirstDataSourceModule } from '@/datasources/cache/cache.first.data.source.module';
56

67
@Module({
8+
imports: [CacheFirstDataSourceModule],
79
providers: [
810
HttpErrorFactory,
911
{ provide: IDataDecoderApi, useClass: DataDecoderApi },

src/datasources/data-decoder-api/data-decoder-api.service.spec.ts

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ import { DataSourceError } from '@/domain/errors/data-source.error';
99
import { NetworkResponseError } from '@/datasources/network/entities/network.error.entity';
1010
import { rawify } from '@/validation/entities/raw.entity';
1111
import type { IConfigurationService } from '@/config/configuration.service.interface';
12-
import type { INetworkService } from '@/datasources/network/network.service.interface';
12+
import type { CacheFirstDataSource } from '@/datasources/cache/cache.first.data.source';
1313

1414
const mockConfigurationService = jest.mocked({
1515
getOrThrow: jest.fn(),
1616
} as jest.MockedObjectDeep<IConfigurationService>);
1717

18-
const mockNetworkService = jest.mocked({
18+
const mockCacheFirstDataSource = jest.mocked({
1919
get: jest.fn(),
2020
post: jest.fn(),
21-
} as jest.MockedObjectDeep<INetworkService>);
21+
} as jest.MockedObjectDeep<CacheFirstDataSource>);
2222

2323
describe('DataDecoderApi', () => {
2424
const baseUrl = faker.internet.url({ appendSlash: false });
25+
const notFoundExpireTimeSeconds = faker.number.int();
2526
let target: DataDecoderApi;
2627

2728
beforeEach(() => {
@@ -31,12 +32,15 @@ describe('DataDecoderApi', () => {
3132
if (key === 'safeDataDecoder.baseUri') {
3233
return baseUrl;
3334
}
35+
if (key === 'expirationTimeInSeconds.notFound.default') {
36+
return notFoundExpireTimeSeconds;
37+
}
3438
throw new Error('Unexpected key');
3539
});
3640
const httpErrorFactory = new HttpErrorFactory();
3741
target = new DataDecoderApi(
3842
mockConfigurationService,
39-
mockNetworkService,
43+
mockCacheFirstDataSource,
4044
httpErrorFactory,
4145
);
4246
});
@@ -48,19 +52,24 @@ describe('DataDecoderApi', () => {
4852
const chainId = faker.string.numeric();
4953
const data = faker.string.hexadecimal() as `0x${string}`;
5054
const getDataDecodedUrl = `${baseUrl}/api/v1/data-decoder`;
51-
mockNetworkService.post.mockImplementation(({ url }) => {
55+
mockCacheFirstDataSource.post.mockImplementation(({ url }) => {
5256
if (url === getDataDecodedUrl) {
53-
return Promise.resolve({ status: 200, data: rawify(dataDecoded) });
57+
return Promise.resolve(rawify(dataDecoded));
5458
}
5559
throw new Error('Unexpected URL');
5660
});
5761

5862
const actual = await target.getDecodedData({ data, to, chainId });
5963

6064
expect(actual).toStrictEqual(dataDecoded);
61-
expect(mockNetworkService.post).toHaveBeenCalledTimes(1);
62-
expect(mockNetworkService.post).toHaveBeenCalledWith({
65+
expect(mockCacheFirstDataSource.post).toHaveBeenCalledTimes(1);
66+
expect(mockCacheFirstDataSource.post).toHaveBeenCalledWith({
67+
cacheDir: {
68+
field: '',
69+
key: `${chainId}_decoded_data_${data}_${to}`,
70+
},
6371
url: getDataDecodedUrl,
72+
notFoundExpireTimeSeconds,
6473
data: { chainId, to, data },
6574
});
6675
});
@@ -75,7 +84,7 @@ describe('DataDecoderApi', () => {
7584
});
7685
const expected = new DataSourceError(errorMessage, statusCode);
7786
const getDataDecodedUrl = `${baseUrl}/api/v1/data-decoder`;
78-
mockNetworkService.post.mockImplementation(({ url }) => {
87+
mockCacheFirstDataSource.post.mockImplementation(({ url }) => {
7988
if (url === getDataDecodedUrl) {
8089
return Promise.reject(
8190
new NetworkResponseError(
@@ -94,9 +103,14 @@ describe('DataDecoderApi', () => {
94103
target.getDecodedData({ data, to, chainId }),
95104
).rejects.toThrow(expected);
96105

97-
expect(mockNetworkService.post).toHaveBeenCalledTimes(1);
98-
expect(mockNetworkService.post).toHaveBeenCalledWith({
106+
expect(mockCacheFirstDataSource.post).toHaveBeenCalledTimes(1);
107+
expect(mockCacheFirstDataSource.post).toHaveBeenCalledWith({
108+
cacheDir: {
109+
field: '',
110+
key: `${chainId}_decoded_data_${data}_${to}`,
111+
},
99112
url: getDataDecodedUrl,
113+
notFoundExpireTimeSeconds,
100114
data: { chainId, to, data },
101115
});
102116
});
@@ -107,9 +121,9 @@ describe('DataDecoderApi', () => {
107121
const contract = contractBuilder().build();
108122
const contractPage = pageBuilder().with('results', [contract]).build();
109123
const getContractsUrl = `${baseUrl}/api/v1/contracts/${contract.address}`;
110-
mockNetworkService.get.mockImplementation(({ url }) => {
124+
mockCacheFirstDataSource.get.mockImplementation(({ url }) => {
111125
if (url === getContractsUrl) {
112-
return Promise.resolve({ status: 200, data: rawify(contractPage) });
126+
return Promise.resolve(rawify(contractPage));
113127
}
114128
throw new Error('Unexpected URL');
115129
});
@@ -120,9 +134,14 @@ describe('DataDecoderApi', () => {
120134
});
121135

122136
expect(actual).toStrictEqual(contractPage);
123-
expect(mockNetworkService.get).toHaveBeenCalledTimes(1);
124-
expect(mockNetworkService.get).toHaveBeenCalledWith({
137+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledTimes(1);
138+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledWith({
139+
cacheDir: {
140+
field: 'undefined_undefined',
141+
key: `${contract.chainId}_decoded_data_contracts_${contract.address}`,
142+
},
125143
url: getContractsUrl,
144+
notFoundExpireTimeSeconds,
126145
networkRequest: {
127146
params: {
128147
chain_ids: contract.chainId.toString(),
@@ -142,9 +161,9 @@ describe('DataDecoderApi', () => {
142161
];
143162
const contractPage = pageBuilder().with('results', [contract]).build();
144163
const getContractsUrl = `${baseUrl}/api/v1/contracts/${contract.address}`;
145-
mockNetworkService.get.mockImplementation(({ url }) => {
164+
mockCacheFirstDataSource.get.mockImplementation(({ url }) => {
146165
if (url === getContractsUrl) {
147-
return Promise.resolve({ status: 200, data: rawify(contractPage) });
166+
return Promise.resolve(rawify(contractPage));
148167
}
149168
throw new Error('Unexpected URL');
150169
});
@@ -155,9 +174,14 @@ describe('DataDecoderApi', () => {
155174
});
156175

157176
expect(actual).toStrictEqual(contractPage);
158-
expect(mockNetworkService.get).toHaveBeenCalledTimes(1);
159-
expect(mockNetworkService.get).toHaveBeenCalledWith({
177+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledTimes(1);
178+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledWith({
179+
cacheDir: {
180+
field: 'undefined_undefined',
181+
key: `${chainIds.sort().join('_')}_decoded_data_contracts_${contract.address}`,
182+
},
160183
url: getContractsUrl,
184+
notFoundExpireTimeSeconds,
161185
networkRequest: {
162186
params: {
163187
chain_ids: `${chainIds[0]}&chain_ids=${chainIds[1]}&chain_ids=${chainIds[2]}`,
@@ -176,7 +200,7 @@ describe('DataDecoderApi', () => {
176200
});
177201
const expected = new DataSourceError(errorMessage, statusCode);
178202
const getContractsUrl = `${baseUrl}/api/v1/contracts/${contract.address}`;
179-
mockNetworkService.get.mockImplementation(({ url }) => {
203+
mockCacheFirstDataSource.get.mockImplementation(({ url }) => {
180204
if (url === getContractsUrl) {
181205
return Promise.reject(
182206
new NetworkResponseError(
@@ -198,9 +222,14 @@ describe('DataDecoderApi', () => {
198222
}),
199223
).rejects.toThrow(expected);
200224

201-
expect(mockNetworkService.get).toHaveBeenCalledTimes(1);
202-
expect(mockNetworkService.get).toHaveBeenCalledWith({
225+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledTimes(1);
226+
expect(mockCacheFirstDataSource.get).toHaveBeenCalledWith({
227+
cacheDir: {
228+
field: 'undefined_undefined',
229+
key: `${contract.chainId}_decoded_data_contracts_${contract.address}`,
230+
},
203231
url: getContractsUrl,
232+
notFoundExpireTimeSeconds,
204233
networkRequest: {
205234
params: {
206235
chain_ids: contract.chainId.toString(),

0 commit comments

Comments
 (0)