Есть такой модуль для работы с API gmgn. Но мне хочется как-то разделить слой доступа к API. И бизнес слой. В частности: API возвращает данные не всегд в нужном виде. 
1. Например численные значения в виде строки.
2. timestamp'ы возвращает в виде числа, что правильно. Но в финальном ответе хотелось бы иметь сразу Date.
3. Вот такие структуры можно было бы упростить(нормализовать) тоже, это будет особенно важно, при сохранении данных в БД
```
  tags: WalletTag[];
  tag_rank: Record<WalletTag, number | undefined>;
```
Отсюда вопрос, как наиболее правильно разделить слой с данными и слой с бизнес-логикой. Что бы одновременно получить удобства, но сохранить текущий контракт gmgn, что бы он полностью соответствовал их API.

Мне нужен самый зрелый подход(ы) которые применяют для таких кейсов.

Так же я заранее выделяю такую проблему: как мне разделять модели GmgnAPI и доменные модели. Например можно разделить: доменная модель WalletInfo и модель API WalletInfoRaw. Но на мой взгляд не самое красивое решение, т.к. IDE всё равно будет предлагать импортировать WalletInfoRaw, хотя это внутрянняя кухня. Так же проблема усугубится, когда в дополнение к Gmgn появится новый провайдер, и нейминги будут вообще некрасивые: WalletInfo, GmgnWalletInfo, GmgnWalletInfoRaw. И во всем этом будет очень легко запутаться. Какой правильный подход будет?

```
import {
  Inject,
  Injectable,
  Logger,
  OnModuleInit,
  ServiceUnavailableException,
} from '@nestjs/common';
import type {
  ExtendedOptionsOfTextResponseBody,
  SearchParameters,
} from 'got-scraping' assert { 'resolution-mode': 'import' };
import { GmgnResponse } from './interfaces/gmgn-response';
import { GetTrendingTokensDto } from './dto/get-trending-tokens.dto';
import { TrandingTokens } from './interfaces/responses/tranding-tokens';
import { GetTrendingWalletsDto } from './dto/get-trending-wallets.dto';
import { TrandingWallets } from './interfaces/responses/tranding-wallets';
import { HttpScraperService } from '../../http-scraper/http-scraper.service';
import { createHash } from 'crypto';
import { TTL } from './consts';
import { GetWalletStatDto } from './dto/get-wallet-stat.dto';
import { WalletStat } from './interfaces/responses/wallet-stat';
import { GetDailyProfitDto } from './dto/get-daily-profit.dto';
import qs from 'qs';
import { DailyProfit } from './interfaces/responses/daily-profit';
import type { ICacheService } from '../../cache/interfaces/cache.interface';
import { CACHE_SERVICE } from '../../cache/cache.constants';
import { GetDevCreatedTokensDto } from './dto/get-dev-created-tokens.dto';
import { DevCreatedTokens } from './interfaces/responses/dev-created-tokens';
import { GetMutilWindowTokenLinkRugVoteDto } from './dto/get-mutil-window-token-link-rug-vote.dto';
import { MutilWindowTokenLinkRugVote } from './interfaces/responses/mutil-window-token-link-rug-vote';
import { GetMutilWindowTokenInfoDto } from './dto/get-mutil-window-token-info.dto';
import { MutilWindowTokenInfo } from './interfaces/responses/mutil-window-token-info';
import { GetWalletstatBatchDto } from './dto/get-walletstat-batch.dto';
import { WalletstatBatch } from './interfaces/responses/walletstat-batch';
import { GetTokenCandlesDto } from './dto/get-token-candles.dto';
import {
  AgedTokenTrade,
  AggedTokenTrades,
} from './interfaces/responses/agged-token-trades';

@Injectable()
export class GmgnService implements OnModuleInit {
  private readonly logger = new Logger(GmgnService.name);

  constructor(
    private readonly httpScraperService: HttpScraperService,
    @Inject(CACHE_SERVICE)
    private readonly cacheService: ICacheService,
  ) {}

  async onModuleInit() {}

  private hashKey(dto: object): string {
    const raw = JSON.stringify(dto);
    const hash = createHash('sha256').update(raw).digest('hex');
    return hash;
  }

  private async request<T extends object>(
    options: Omit<ExtendedOptionsOfTextResponseBody, 'searchParams'> & {
      searchParams?: SearchParameters | Record<string, any>;
    },
  ): Promise<T> {
    const { url, method, json } = options;

    const queryString = qs
      .stringify(options.searchParams, {
        arrayFormat: 'repeat',
        skipNulls: true,
      })
      .toLowerCase();

    const hash = this.hashKey({
      url: url,
      searchParams: queryString,
      method,
      json,
    });

    const cached = await this.cacheService.getJson<T>(hash);

    if (cached) {
      const method = options.method ?? 'GET';
      this.logger.verbose(`Кэш: ${method} ${options.url}`);
      return cached;
    }

    const { code, data, msg } = await this.httpScraperService.getAsJson<
      GmgnResponse<T>
    >({ ...options, searchParams: queryString });

    if (code !== 0) {
      throw new ServiceUnavailableException(msg);
    }

    if (!data) {
      throw new ServiceUnavailableException(
        'В ответе сервера отсуствует поле data',
      );
    }

    await this.cacheService.setJson<T>(hash, data, TTL);

    return data;
  }

  async getTrendingWallets(
    dto: GetTrendingWalletsDto,
  ): Promise<TrandingWallets> {
    const { chain, timeframe = '7d', ...query } = dto;

    const {
      orderBy = 'pnl_7d',
      direction = 'desc',
      limit = 10,
      ...filters
    } = query;

    const searchParams = {
      orderBy,
      direction,
      limit,
      ...filters,
    };

    try {
      const trandingWallets: TrandingWallets =
        await this.request<TrandingWallets>({
          url: `https://gmgn.ai/defi/quotation/v1/rank/${chain}/wallets/${timeframe}`,
          timeout: { request: 10_000 },
          searchParams,
        });

      return trandingWallets;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getTrendingTokens(dto: GetTrendingTokensDto): Promise<TrandingTokens> {
    const { chain, timeframe = '7d', ...query } = dto;

    const { orderBy = 'swaps', direction = 'desc', limit, ...filters } = query;

    const searchParams = {
      orderBy,
      direction,
      limit,
      ...filters,
    };

    try {
      const trandingTokens: TrandingTokens = await this.request<TrandingTokens>(
        {
          url: `https://gmgn.ai/defi/quotation/v1/rank/${chain}/swaps/${timeframe}`,
          searchParams,
        },
      );

      return trandingTokens;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getWalletStat(dto: GetWalletStatDto): Promise<WalletStat> {
    const { chain, wallet, period } = dto;

    try {
      const walletStat: WalletStat = await this.request<WalletStat>({
        url: `https://gmgn.ai/api/v1/wallet_stat/${chain}/${wallet}/${period}`,
        searchParams: {
          period,
        },
      });

      return walletStat;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getDailyProfit(dto: GetDailyProfitDto): Promise<DailyProfit[]> {
    const { chain, wallet, period = '7d' } = dto;

    try {
      const dailyProfit: DailyProfit[] = await this.request<DailyProfit[]>({
        url: `https://gmgn.ai/api/v1/daily_profit/${chain}/${wallet}/${period}`,
        searchParams: {
          period,
        },
      });

      return dailyProfit;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getDevCreatedTokens(
    dto: GetDevCreatedTokensDto,
  ): Promise<DevCreatedTokens> {
    const { chain, address } = dto;

    try {
      const devCreatedTokens: DevCreatedTokens =
        await this.request<DevCreatedTokens>({
          url: `https://gmgn.ai/api/v1/dev_created_tokens/${chain}/${address}`,
          searchParams: {
            address,
          },
        });

      return devCreatedTokens;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getMutilWindowTokenLinkRugVote(
    dto: GetMutilWindowTokenLinkRugVoteDto,
  ): Promise<MutilWindowTokenLinkRugVote> {
    const { chain, address } = dto;

    try {
      const mutilWindowTokenLinkRugVote: MutilWindowTokenLinkRugVote =
        await this.request<MutilWindowTokenLinkRugVote>({
          url: `https://gmgn.ai/api/v1/mutil_window_token_link_rug_vote/${chain}/${address}`,
        });

      return mutilWindowTokenLinkRugVote;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getMutilWindowTokenInfo(
    dto: GetMutilWindowTokenInfoDto,
  ): Promise<MutilWindowTokenInfo[]> {
    const { chain, addresses } = dto;

    try {
      const mutilWindowTokenInfo: MutilWindowTokenInfo[] = await this.request<
        MutilWindowTokenInfo[]
      >({
        url: `https://gmgn.ai/api/v1/mutil_window_token_info`,
        method: 'POST',
        json: {
          chain,
          addresses,
        },
      });

      return mutilWindowTokenInfo;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getWalletstatBatch(
    dto: GetWalletstatBatchDto,
  ): Promise<WalletstatBatch> {
    const { chain, token_wallet_map } = dto;

    try {
      const walletstatBatch: WalletstatBatch =
        await this.request<WalletstatBatch>({
          url: `https://gmgn.ai/api/v1/wallet/${chain}/walletstat`,
          method: 'POST',
          json: {
            token_wallet_map,
          },
        });

      return walletstatBatch;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }

  async getTokenCandles(dto: GetTokenCandlesDto): Promise<AgedTokenTrade[]> {
    const {
      chain,
      token,
      from,
      to,
      limit,
      pool_type = 'unified',
      resolution = '5m',
    } = dto;

    try {
      const { list: aggedTokenTrades } = await this.request<AggedTokenTrades>({
        url: `https://gmgn.ai/api/v1/token_candles/${chain}/${token}`,
        method: 'GET',
        searchParams: {
          from,
          to,
          limit,
          pool_type,
          resolution,
        },
      });

      return aggedTokenTrades;
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }
}
```