Skip to content

Commit

Permalink
Merge branch 'main' into raw-prices-api
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 8, 2024
1 parent 602051d commit af62ce5
Show file tree
Hide file tree
Showing 33 changed files with 246 additions and 119 deletions.
19 changes: 18 additions & 1 deletion src/app.module.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { Test } from '@nestjs/testing';
import { AppModule } from '@/app.module';
import { CacheKeyPrefix } from '@/datasources/cache/constants';
import { TestPostgresDatabaseModule } from '@/datasources/db/__tests__/test.postgres-database.module';
import { PostgresDatabaseModule } from '@/datasources/db/v1/postgres-database.module';
import { PostgresDatabaseModuleV2 } from '@/datasources/db/v2/postgres-database.module';
import { TestPostgresDatabaseModuleV2 } from '@/datasources/db/v2/test.postgres-database.module';
import { TestQueuesApiModule } from '@/datasources/queues/__tests__/test.queues-api.module';
import { QueuesApiModule } from '@/datasources/queues/queues-api.module';

describe('Application bootstrap', () => {
it('should init the app', async () => {
const cacheKeyPrefix = crypto.randomUUID();
const moduleRef = await Test.createTestingModule({
imports: [AppModule.register()],
}).compile();
})
.overrideProvider(CacheKeyPrefix)
.useValue(cacheKeyPrefix)
.overrideModule(PostgresDatabaseModule)
.useModule(TestPostgresDatabaseModule)
.overrideModule(PostgresDatabaseModuleV2)
.useModule(TestPostgresDatabaseModuleV2)
.overrideModule(QueuesApiModule)
.useModule(TestQueuesApiModule)
.compile();

const app = moduleRef.createNestApplication();
await app.init();
Expand Down
2 changes: 1 addition & 1 deletion src/datasources/balances-api/balances-api.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe('Balances API Manager Tests', () => {
else if (key === 'features.counterfactualBalances') return true;
throw new Error(`Unexpected key: ${key}`);
});
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));
dataSourceMock.get.mockResolvedValue([]);
coingeckoApiMock.getTokenPrices.mockResolvedValue(rawify([]));
const balancesApiManager = new BalancesApiManager(
Expand Down
5 changes: 4 additions & 1 deletion src/datasources/balances-api/balances-api.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { intersection } from 'lodash';
import { ITransactionApiManager } from '@/domain/interfaces/transaction-api.manager.interface';
import { z } from 'zod';
import { type Raw, rawify } from '@/validation/entities/raw.entity';
import { ChainSchema } from '@/domain/chains/entities/schemas/chain.schema';

@Injectable()
export class BalancesApiManager implements IBalancesApiManager {
Expand Down Expand Up @@ -85,7 +86,9 @@ export class BalancesApiManager implements IBalancesApiManager {
const safeBalancesApi = this.safeBalancesApiMap[chainId];
if (safeBalancesApi !== undefined) return safeBalancesApi;

const chain = await this.configApi.getChain(chainId);
const chain = await this.configApi
.getChain(chainId)
.then(ChainSchema.parse);
this.safeBalancesApiMap[chainId] = new SafeBalancesApi(
chainId,
this.useVpcUrl ? chain.vpcTransactionService : chain.transactionService,
Expand Down
11 changes: 6 additions & 5 deletions src/datasources/blockchain/blockchain-api.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { chainBuilder } from '@/domain/chains/entities/__tests__/chain.builder';
import { rpcUriBuilder } from '@/domain/chains/entities/__tests__/rpc-uri.builder';
import { RpcUriAuthentication } from '@/domain/chains/entities/rpc-uri-authentication.entity';
import type { IConfigApi } from '@/domain/interfaces/config-api.interface';
import { rawify } from '@/validation/entities/raw.entity';
import { faker } from '@faker-js/faker';
import { hexToNumber, toHex } from 'viem';

Expand Down Expand Up @@ -39,7 +40,7 @@ describe('BlockchainApiManager', () => {
describe('getApi', () => {
it('caches the API', async () => {
const chain = chainBuilder().build();
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));

const api = await target.getApi(chain.chainId);
const cachedApi = await target.getApi(chain.chainId);
Expand All @@ -58,7 +59,7 @@ describe('BlockchainApiManager', () => {
.build(),
)
.build();
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));

const api = await target.getApi(chain.chainId);

Expand All @@ -70,7 +71,7 @@ describe('BlockchainApiManager', () => {
const chain = chainBuilder()
.with('rpcUri', rpcUriBuilder().with('value', rpcUriValue).build())
.build();
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));

const api = await target.getApi(chain.chainId);

Expand All @@ -79,7 +80,7 @@ describe('BlockchainApiManager', () => {

it('should not include the INFURA_API_KEY in the RPC URI for a RPC provider different from Infura', async () => {
const chain = chainBuilder().build();
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));

const api = await target.getApi(chain.chainId);

Expand All @@ -90,7 +91,7 @@ describe('BlockchainApiManager', () => {
describe('destroyApi', () => {
it('clears the API', async () => {
const chain = chainBuilder().build();
configApiMock.getChain.mockResolvedValue(chain);
configApiMock.getChain.mockResolvedValue(rawify(chain));

const api = await target.getApi(chain.chainId);
target.destroyApi(chain.chainId);
Expand Down
5 changes: 4 additions & 1 deletion src/datasources/blockchain/blockchain-api.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/datasources/cache/cache.service.interface';
import { Chain as DomainChain } from '@/domain/chains/entities/chain.entity';
import { RpcUriAuthentication } from '@/domain/chains/entities/rpc-uri-authentication.entity';
import { ChainSchema } from '@/domain/chains/entities/schemas/chain.schema';
import { IBlockchainApiManager } from '@/domain/interfaces/blockchain-api.manager.interface';
import { IConfigApi } from '@/domain/interfaces/config-api.interface';
import { Inject, Injectable } from '@nestjs/common';
Expand Down Expand Up @@ -46,7 +47,9 @@ export class BlockchainApiManager implements IBlockchainApiManager {
return blockchainApi;
}

const chain = await this.configApi.getChain(chainId);
const chain = await this.configApi
.getChain(chainId)
.then(ChainSchema.parse);
this.blockchainApiMap[chainId] = this._createCachedRpcClient(chain);

return this.blockchainApiMap[chainId];
Expand Down
15 changes: 10 additions & 5 deletions src/datasources/config-api/config-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import { Page } from '@/domain/entities/page.entity';
import { IConfigApi } from '@/domain/interfaces/config-api.interface';
import { SafeApp } from '@/domain/safe-apps/entities/safe-app.entity';
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
import { Raw } from '@/validation/entities/raw.entity';
import { Inject, Injectable } from '@nestjs/common';

/**
* TODO: Move all usage of Raw to CacheFirstDataSource after fully migrated
* to "Raw" type implementation.
*/
@Injectable()
export class ConfigApi implements IConfigApi {
private readonly baseUri: string;
Expand Down Expand Up @@ -47,12 +52,12 @@ export class ConfigApi implements IConfigApi {
async getChains(args: {
limit?: number;
offset?: number;
}): Promise<Page<Chain>> {
}): Promise<Raw<Page<Chain>>> {
try {
const url = `${this.baseUri}/api/v1/chains`;
const params = { limit: args.limit, offset: args.offset };
const cacheDir = CacheRouter.getChainsCacheDir(args);
return await this.dataSource.get({
return await this.dataSource.get<Raw<Chain>>({
cacheDir,
url,
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
Expand All @@ -64,11 +69,11 @@ export class ConfigApi implements IConfigApi {
}
}

async getChain(chainId: string): Promise<Chain> {
async getChain(chainId: string): Promise<Raw<Chain>> {
try {
const url = `${this.baseUri}/api/v1/chains/${chainId}`;
const cacheDir = CacheRouter.getChainCacheDir(chainId);
return await this.dataSource.get({
return await this.dataSource.get<Raw<Chain>>({
cacheDir,
url,
notFoundExpireTimeSeconds: this.defaultNotFoundExpirationTimeSeconds,
Expand Down Expand Up @@ -98,7 +103,7 @@ export class ConfigApi implements IConfigApi {
clientUrl?: string;
onlyListed?: boolean;
url?: string;
}): Promise<SafeApp[]> {
}): Promise<Raw<SafeApp[]>> {
try {
const providerUrl = `${this.baseUri}/api/v1/safe-apps/`;
const params = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ export const mockPostgresDatabaseService = {
return callback(mockEntityManager);
},
),
getTransactionRunner: jest.fn().mockReturnValue(mockEntityManager),
} as jest.MockedObjectDeep<PostgresDatabaseService>;
15 changes: 2 additions & 13 deletions src/datasources/db/v2/postgres-database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {

@Injectable()
export class PostgresDatabaseService {
private transactionManager?: EntityManager = undefined;

public constructor(
@Inject(LoggingService) private readonly loggingService: ILoggingService,
@InjectDataSource()
Expand Down Expand Up @@ -88,18 +86,9 @@ export class PostgresDatabaseService {
callback: (transactionManager: EntityManager) => Promise<T>,
): Promise<T> {
return this.dataSource.transaction(
async (transactionalEntityManager): Promise<T> => {
this.transactionManager = transactionalEntityManager;
return await callback(this.transactionManager);
async (transactionalEntityManager: EntityManager): Promise<T> => {
return await callback(transactionalEntityManager);
},
);
}

public getTransactionRunner(): EntityManager {
if (!this.transactionManager) {
throw new Error('Query runner is not initialized...');
}

return this.transactionManager;
}
}
1 change: 0 additions & 1 deletion src/datasources/errors/http-error-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { get } from 'lodash';
@Injectable()
export class HttpErrorFactory {
from(source: unknown): DataSourceError {
// TODO: Handle instances of ZodError, returning issue from it
if (source instanceof NetworkResponseError) {
const errorMessage = get(source, 'data.message', 'An error occurred');
return new DataSourceError(errorMessage, source.response.status);
Expand Down
17 changes: 14 additions & 3 deletions src/datasources/jwt/jwt.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function jwtClientFactory() {
},
>(
payload: T,
options: { secretOrPrivateKey: string },
options: { secretOrPrivateKey: string; algorithm?: jwt.Algorithm },
): string => {
// All date-based claims should be second-based NumericDates
const { exp, iat, nbf, ...rest } = payload;
Expand All @@ -30,24 +30,35 @@ function jwtClientFactory() {
...rest,
},
options.secretOrPrivateKey,
{ algorithm: options.algorithm },
);
},
verify: <T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string },
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<jwt.Algorithm>;
},
): T => {
return jwt.verify(token, options.secretOrPrivateKey, {
algorithms: options.algorithms,
issuer: options.issuer,
// Return only payload without claims, e.g. no exp, nbf, etc.
complete: false,
}) as T;
},
decode: <T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string },
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<jwt.Algorithm>;
},
): JwtPayloadWithClaims<T> => {
// Client has `decode` method but we also want to verify the signature
const { payload } = jwt.verify(token, options.secretOrPrivateKey, {
algorithms: options.algorithms,
issuer: options.issuer,
// Return headers, payload (with claims) and signature
complete: true,
Expand Down
2 changes: 2 additions & 0 deletions src/datasources/jwt/jwt.service.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JwtPayloadWithClaims } from '@/datasources/jwt/jwt-claims.entity';
import type { Algorithm } from 'jsonwebtoken';

export const IJwtService = Symbol('IJwtService');

Expand All @@ -13,6 +14,7 @@ export interface IJwtService {
payload: T,
options?: {
secretOrPrivateKey: string;
algorithm?: Algorithm;
},
): string;

Expand Down
58 changes: 58 additions & 0 deletions src/datasources/jwt/jwt.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('JwtService', () => {
{ iss: configIssuer, ...payload },
{
secretOrPrivateKey: configSecret,
algorithm: 'HS256',
},
);
});
Expand All @@ -63,8 +64,27 @@ describe('JwtService', () => {
expect(jwtClientMock.sign).toHaveBeenCalledTimes(1);
expect(jwtClientMock.sign).toHaveBeenCalledWith(payload, {
secretOrPrivateKey: customSecret,
algorithm: 'HS256',
});
});

it('should sign a payload with RS256 algorithm', () => {
const payload = JSON.parse(fakeJson()) as object;

service.sign(payload, {
secretOrPrivateKey: configSecret,
algorithm: 'RS256',
});

expect(jwtClientMock.sign).toHaveBeenCalledTimes(1);
expect(jwtClientMock.sign).toHaveBeenCalledWith(
{ iss: configIssuer, ...payload },
{
secretOrPrivateKey: configSecret,
algorithm: 'RS256',
},
);
});
});

describe('verify', () => {
Expand Down Expand Up @@ -96,6 +116,25 @@ describe('JwtService', () => {
secretOrPrivateKey: customSecret,
});
});

it('should verify a token with RS256 algorithm', () => {
const token = faker.string.alphanumeric();
const customIssuer = faker.word.noun();
const customSecret = faker.string.alphanumeric();

service.verify(token, {
issuer: customIssuer,
secretOrPrivateKey: customSecret,
algorithms: ['RS256'],
});

expect(jwtClientMock.verify).toHaveBeenCalledTimes(1);
expect(jwtClientMock.verify).toHaveBeenCalledWith(token, {
issuer: customIssuer,
secretOrPrivateKey: customSecret,
algorithms: ['RS256'],
});
});
});

describe('decode', () => {
Expand Down Expand Up @@ -127,5 +166,24 @@ describe('JwtService', () => {
secretOrPrivateKey: customSecret,
});
});

it('should decode a token with RS256 Algorithm', () => {
const token = faker.string.alphanumeric();
const customIssuer = faker.word.noun();
const customSecret = faker.string.alphanumeric();

service.decode(token, {
issuer: customIssuer,
secretOrPrivateKey: customSecret,
algorithms: ['RS256'],
});

expect(jwtClientMock.decode).toHaveBeenCalledTimes(1);
expect(jwtClientMock.decode).toHaveBeenCalledWith(token, {
issuer: customIssuer,
secretOrPrivateKey: customSecret,
algorithms: ['RS256'],
});
});
});
});
Loading

0 comments on commit af62ce5

Please sign in to comment.