Skip to content

Commit

Permalink
Add GET AddressBook and POST AddressBookItem
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorgomezv committed Nov 20, 2024
1 parent d72edee commit cc8e0bc
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IAddressBooksDataSource } from '@/domain/interfaces/address-books.datasource.interface';
import { Module } from '@nestjs/common';

const addressBooksDatasource = {
createAddressBookItem: jest.fn(),
getAddressBook: jest.fn(),
updateAddressBookItem: jest.fn(),
deleteAddressBook: jest.fn(),
deleteAddressBookItem: jest.fn(),
} as jest.MockedObjectDeep<IAddressBooksDataSource>;

@Module({
providers: [
{
provide: IAddressBooksDataSource,
useFactory: (): jest.MockedObjectDeep<IAddressBooksDataSource> => {
return jest.mocked(addressBooksDatasource);
},
},
],
exports: [IAddressBooksDataSource],
})
export class TestAddressBooksDataSourceModule {}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { AddressBooksDatasource } from '@/datasources/accounts/address-books/address-books.datasource';
import { AddressBookDbMapper } from '@/datasources/accounts/address-books/entities/address-book.db.mapper';
import { EncryptionApiManager } from '@/datasources/accounts/encryption/encryption-api.manager';
import { PostgresDatabaseModule } from '@/datasources/db/v1/postgres-database.module';
import { IAddressBooksDataSource } from '@/domain/interfaces/address-books.datasource.interface';
import { IEncryptionApiManager } from '@/domain/interfaces/encryption-api.manager.interface';
import { Module } from '@nestjs/common';

@Module({
imports: [PostgresDatabaseModule],
providers: [
AddressBookDbMapper,
{
provide: IAddressBooksDataSource,
useClass: AddressBooksDatasource,
},
{ provide: IAddressBooksDataSource, useClass: AddressBooksDatasource },
{ provide: IEncryptionApiManager, useClass: EncryptionApiManager },
],
exports: [IAddressBooksDataSource],
exports: [IAddressBooksDataSource, IEncryptionApiManager],
})
export class AddressBooksDatasourceModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AddressBooksDatasourceModule } from '@/datasources/accounts/address-books/address-books.datasource.module';
import { AccountsRepositoryModule } from '@/domain/accounts/accounts.repository.interface';
import { AddressBooksRepository } from '@/domain/accounts/address-books/address-books.repository';
import type {
AddressBook,
AddressBookItem,
} from '@/domain/accounts/address-books/entities/address-book.entity';
import { CreateAddressBookItemDto } from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity';
import { AuthPayload } from '@/domain/auth/entities/auth-payload.entity';
import { Module } from '@nestjs/common';

export const IAddressBooksRepository = Symbol.for('IAddressBooksRepository');

export interface IAddressBooksRepository {
getAddressBook(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
}): Promise<AddressBook>;

createAddressBookItem(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
createAddressBookItemDto: CreateAddressBookItemDto;
}): Promise<AddressBookItem>;
}

@Module({
imports: [AccountsRepositoryModule, AddressBooksDatasourceModule],
providers: [
{
provide: IAddressBooksRepository,
useClass: AddressBooksRepository,
},
],
exports: [IAddressBooksRepository],
})
export class AddressBooksRepositoryModule {}
124 changes: 124 additions & 0 deletions src/domain/accounts/address-books/address-books.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { IAccountsRepository } from '@/domain/accounts/accounts.repository.interface';
import type { IAddressBooksRepository } from '@/domain/accounts/address-books/address-books.repository.interface';
import type {
AddressBook,
AddressBookItem,
} from '@/domain/accounts/address-books/entities/address-book.entity';
import { CreateAddressBookItemDto } from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity';
import {
AccountDataType,
AccountDataTypeNames,
} from '@/domain/accounts/entities/account-data-type.entity';
import { AuthPayload } from '@/domain/auth/entities/auth-payload.entity';
import { IAddressBooksDataSource } from '@/domain/interfaces/address-books.datasource.interface';
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
import {
GoneException,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';

@Injectable()
export class AddressBooksRepository implements IAddressBooksRepository {
constructor(
@Inject(IAddressBooksDataSource)
private readonly dataSource: IAddressBooksDataSource,
@Inject(IAccountsRepository)
private readonly accountsRepository: IAccountsRepository,
@Inject(LoggingService) private readonly loggingService: ILoggingService,
) {}

/**
* Gets an AddressBook.
* Checks that the account has the AddressBooks Data Setting enabled.
*/
async getAddressBook(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
}): Promise<AddressBook> {
if (!args.authPayload.isForSigner(args.address)) {
throw new UnauthorizedException();
}
await this.checkAddressBooksIsEnabled({
authPayload: args.authPayload,
address: args.address,
});
const account = await this.accountsRepository.getAccount({
authPayload: args.authPayload,
address: args.address,
});
return await this.dataSource.getAddressBook({
account,
chainId: args.chainId,
});
}

/**
* Creates an AddressBookItem.
* Checks that the account has the AddressBooks Data Setting enabled.
*
* If an AddressBook for the Account and chainId does not exist, it's created.
*/
async createAddressBookItem(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
createAddressBookItemDto: CreateAddressBookItemDto;
}): Promise<AddressBookItem> {
if (!args.authPayload.isForSigner(args.address)) {
throw new UnauthorizedException();
}
await this.checkAddressBooksIsEnabled({
authPayload: args.authPayload,
address: args.address,
});
const account = await this.accountsRepository.getAccount({
authPayload: args.authPayload,
address: args.address,
});
return this.dataSource.createAddressBookItem({
account,
chainId: args.chainId,
createAddressBookItemDto: args.createAddressBookItemDto,
});
}

// TODO: Extract this functionality in AccountsRepository['checkIsEnabled(DataType, Account)']
private async checkAddressBooksIsEnabled(args: {
authPayload: AuthPayload;
address: `0x${string}`;
}): Promise<void> {
const addressBookDataType = await this.checkAddressBookDataTypeIsActive();
const accountDataSettings =
await this.accountsRepository.getAccountDataSettings({
authPayload: args.authPayload,
address: args.address,
});
const addressBookSetting = accountDataSettings.find(
(setting) => setting.account_data_type_id === addressBookDataType.id,
);
if (!addressBookSetting?.enabled) {
this.loggingService.warn({
message: `Account ${args.address} does not have AddressBooks enabled`,
});
throw new GoneException();
}
}

// TODO: Extract this functionality in AccountsRepository['checkIsActive(DataType)']
private async checkAddressBookDataTypeIsActive(): Promise<AccountDataType> {
const dataTypes = await this.accountsRepository.getDataTypes();
const addressBookDataType = dataTypes.find(
(dataType) => dataType.name === AccountDataTypeNames.AddressBook,
);
if (!addressBookDataType?.is_active) {
this.loggingService.warn({
message: `${AccountDataTypeNames.AddressBook} data type is not active`,
});
throw new GoneException();
}
return addressBookDataType;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { RowSchema } from '@/datasources/db/v1/entities/row.entity';
import { AccountSchema } from '@/domain/accounts/entities/account.entity';
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
import { z } from 'zod';

export const AddressBookItemSchema = z.object({
id: z.number(),
name: z.string(),
address: z.string(),
address: AddressSchema,
});

export const AddressBookSchema = RowSchema.extend({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export class CounterfactualSafesRepository
return this.datasource.deleteCounterfactualSafesForAccount(account);
}

// TODO: Extract this functionality in AccountsRepository['checkIsEnabled(DataType, Account)']
private async checkCounterfactualSafesIsEnabled(args: {
authPayload: AuthPayload;
address: `0x${string}`;
Expand All @@ -165,6 +166,7 @@ export class CounterfactualSafesRepository
}
}

// TODO: Extract this functionality in AccountsRepository['checkIsActive(DataType)']
private async checkCounterfactualSafeDataTypeIsActive(): Promise<AccountDataType> {
const dataTypes = await this.accountsRepository.getDataTypes();
const counterfactualSafeDataType = dataTypes.find(
Expand Down
4 changes: 4 additions & 0 deletions src/routes/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AppModule } from '@/app.module';
import configuration from '@/config/entities/__tests__/configuration';
import { TestAccountsDataSourceModule } from '@/datasources/accounts/__tests__/test.accounts.datasource.module';
import { AccountsDatasourceModule } from '@/datasources/accounts/accounts.datasource.module';
import { TestAddressBooksDataSourceModule } from '@/datasources/accounts/address-books/__tests__/test.address-books.datasource.module';
import { AddressBooksDatasourceModule } from '@/datasources/accounts/address-books/address-books.datasource.module';
import { TestCounterfactualSafesDataSourceModule } from '@/datasources/accounts/counterfactual-safes/__tests__/test.counterfactual-safes.datasource.module';
import { CounterfactualSafesDatasourceModule } from '@/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.module';
import { TestCacheModule } from '@/datasources/cache/__tests__/test.cache.module';
Expand Down Expand Up @@ -70,6 +72,8 @@ describe('AccountsController', () => {
.useModule(TestPostgresDatabaseModule)
.overrideModule(AccountsDatasourceModule)
.useModule(TestAccountsDataSourceModule)
.overrideModule(AddressBooksDatasourceModule)
.useModule(TestAddressBooksDataSourceModule)
.overrideModule(CounterfactualSafesDatasourceModule)
.useModule(TestCounterfactualSafesDataSourceModule)
.overrideModule(TargetedMessagingDatasourceModule)
Expand Down
2 changes: 2 additions & 0 deletions src/routes/accounts/accounts.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccountsRepositoryModule } from '@/domain/accounts/accounts.repository.interface';
import { AddressBooksRepositoryModule } from '@/domain/accounts/address-books/address-books.repository.interface';
import { CounterfactualSafesRepositoryModule } from '@/domain/accounts/counterfactual-safes/counterfactual-safes.repository.interface';
import { AuthRepositoryModule } from '@/domain/auth/auth.repository.interface';
import { AccountsController } from '@/routes/accounts/accounts.controller';
Expand All @@ -12,6 +13,7 @@ import { Module } from '@nestjs/common';
@Module({
imports: [
AccountsRepositoryModule,
AddressBooksRepositoryModule,
AuthRepositoryModule,
CounterfactualSafesRepositoryModule,
],
Expand Down
35 changes: 33 additions & 2 deletions src/routes/accounts/address-books/address-books.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {
CreateAddressBookItemDto,
CreateAddressBookItemDtoSchema,
} from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity';
import { AuthPayload } from '@/domain/auth/entities/auth-payload.entity';
import { AddressBooksService } from '@/routes/accounts/address-books/address-books.service';
import { AddressBook } from '@/routes/accounts/address-books/entities/address-book.entity';
import {
AddressBook,
AddressBookItem,
} from '@/routes/accounts/address-books/entities/address-book.entity';
import { Auth } from '@/routes/auth/decorators/auth.decorator';
import { AuthGuard } from '@/routes/auth/guards/auth.guard';
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema';
import { ValidationPipe } from '@/validation/pipes/validation.pipe';
import { Controller, Get, Param } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';

@ApiTags('accounts')
Expand All @@ -13,13 +23,34 @@ export class AddressBooksController {

@ApiOkResponse({ type: AddressBook })
@Get(':address/address-books/:chainId')
@UseGuards(AuthGuard)
async getAddressBook(
@Auth() authPayload: AuthPayload,
@Param('address', new ValidationPipe(AddressSchema)) address: `0x${string}`,
@Param('chainId', new ValidationPipe(NumericStringSchema)) chainId: string,
): Promise<AddressBook> {
return this.service.getAddressBook({
authPayload,
address,
chainId,
});
}

@ApiOkResponse({ type: AddressBookItem })
@Post(':address/address-books/:chainId')
@UseGuards(AuthGuard)
async createAddressBookItem(
@Auth() authPayload: AuthPayload,
@Param('address', new ValidationPipe(AddressSchema)) address: `0x${string}`,
@Param('chainId', new ValidationPipe(NumericStringSchema)) chainId: string,
@Body(new ValidationPipe(CreateAddressBookItemDtoSchema))
createAddressBookItemDto: CreateAddressBookItemDto,
): Promise<AddressBookItem> {
return this.service.createAddressBookItem({
authPayload,
address,
chainId,
createAddressBookItemDto,
});
}
}
52 changes: 47 additions & 5 deletions src/routes/accounts/address-books/address-books.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,59 @@ import {
AddressBook,
AddressBookItem,
} from '@/routes/accounts/address-books/entities/address-book.entity';
import { Injectable } from '@nestjs/common';
import { AddressBook as DomainAddressBook } from '@/domain/accounts/address-books/entities/address-book.entity';
import { AddressBookItem as DomainAddressBookItem } from '@/domain/accounts/address-books/entities/address-book.entity';
import { Inject, Injectable } from '@nestjs/common';
import { IAddressBooksRepository } from '@/domain/accounts/address-books/address-books.repository.interface';
import { AuthPayload } from '@/domain/auth/entities/auth-payload.entity';
import { CreateAddressBookItemDto } from '@/domain/accounts/address-books/entities/create-address-book-item.dto.entity';

@Injectable()
export class AddressBooksService {
constructor(
@Inject(IAddressBooksRepository)
private readonly repository: IAddressBooksRepository,
) {}

async getAddressBook(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
}): Promise<AddressBook> {
await new Promise((resolve) => setTimeout(resolve, 1000));
return new AddressBook(args.address, [
new AddressBookItem('1', 'name', '0x01'),
]);
const domainAddressBook = await this.repository.getAddressBook(args);
return this.mapAddressBook(domainAddressBook);
}

async createAddressBookItem(args: {
authPayload: AuthPayload;
address: `0x${string}`;
chainId: string;
createAddressBookItemDto: CreateAddressBookItemDto;
}): Promise<AddressBookItem> {
const domainAddressBookItem =
await this.repository.createAddressBookItem(args);
return this.mapAddressBookItem(domainAddressBookItem);
}

private mapAddressBook(domainAddressBook: DomainAddressBook): AddressBook {
return {
accountId: domainAddressBook.accountId.toString(),
chainId: domainAddressBook.chainId,
data: domainAddressBook.data.map((item) => ({
id: item.id.toString(),
name: item.name,
address: item.address,
})),
};
}

private mapAddressBookItem(
domainAddressBookItem: DomainAddressBookItem,
): AddressBookItem {
return {
id: domainAddressBookItem.id.toString(),
name: domainAddressBookItem.name,
address: domainAddressBookItem.address,
};
}
}
Loading

0 comments on commit cc8e0bc

Please sign in to comment.